Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added userInfo response type check to handle signed and encrypted res… #305

Merged
merged 8 commits into from
Dec 14, 2022
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).

## [unreleased]
* Support for signed and encrypted UserInfo response. #305
* Support for signed and encrypted ID Token. #305

## [0.9.10]

## Fixed
Expand Down
129 changes: 96 additions & 33 deletions src/OpenIDConnectClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,11 @@ class OpenIDConnectClient
*/
private $responseCode;

/**
* @var string|null Content type from the server
*/
private $responseContentType;

/**
* @var array holds response types
*/
Expand Down Expand Up @@ -344,22 +349,20 @@ public function authenticate() {
throw new OpenIDConnectClientException('User did not authorize openid scope.');
}

$claims = $this->decodeJWT($token_json->id_token, 1);
$id_token = $token_json->id_token;
$idTokenHeaders = $this->decodeJWT($id_token);
if (isset($idTokenHeaders->enc)) {
// Handle JWE
$id_token = $this->handleJweResponse($id_token);
}

$claims = $this->decodeJWT($id_token, 1);

// Verify the signature
if ($this->canVerifySignatures()) {
if (!$this->getProviderConfigValue('jwks_uri')) {
throw new OpenIDConnectClientException ('Unable to verify signature due to no jwks_uri being defined');
}
if (!$this->verifyJWTsignature($token_json->id_token)) {
throw new OpenIDConnectClientException ('Unable to verify signature');
}
} else {
user_error('Warning: JWT signature verification unavailable.');
}
$this->verifySignatures($id_token);

// Save the id token
$this->idToken = $token_json->id_token;
$this->idToken = $id_token;

// Save the access token
$this->accessToken = $token_json->access_token;
Expand Down Expand Up @@ -408,16 +411,7 @@ public function authenticate() {
$claims = $this->decodeJWT($id_token, 1);

// Verify the signature
if ($this->canVerifySignatures()) {
if (!$this->getProviderConfigValue('jwks_uri')) {
throw new OpenIDConnectClientException ('Unable to verify signature due to no jwks_uri being defined');
}
if (!$this->verifyJWTsignature($id_token)) {
throw new OpenIDConnectClientException ('Unable to verify signature');
}
} else {
user_error('Warning: JWT signature verification unavailable.');
}
$this->verifySignatures($id_token);

// Save the id token
$this->idToken = $id_token;
Expand Down Expand Up @@ -927,7 +921,7 @@ protected function requestTokens($code, $headers = array()) {
if ($this->supportsAuthMethod('client_secret_basic', $token_endpoint_auth_methods_supported)) {
$authorizationHeader = 'Authorization: Basic ' . base64_encode(urlencode($this->clientID) . ':' . urlencode($this->clientSecret));
unset($token_params['client_secret']);
unset($token_params['client_id']);
unset($token_params['client_id']);
}

// When there is a private key jwt generator and it is supported then use it as client authentication
Expand All @@ -945,7 +939,7 @@ protected function requestTokens($code, $headers = array()) {
else{
$client_assertion = $this->getJWTClientAssertion($this->getProviderConfigValue('token_endpoint'));
}

$token_params['client_assertion_type'] = $client_assertion_type;
$token_params['client_assertion'] = $client_assertion;
unset($token_params['client_secret']);
Expand Down Expand Up @@ -1053,15 +1047,15 @@ public function refreshToken($refresh_token) {
if ($this->supportsAuthMethod('client_secret_jwt', $token_endpoint_auth_methods_supported)) {
$client_assertion_type = $this->getProviderConfigValue('client_assertion_type');
$client_assertion = $this->getJWTClientAssertion($this->getProviderConfigValue('token_endpoint'));

$token_params["grant_type"] = "urn:ietf:params:oauth:grant-type:token-exchange";
$token_params["subject_token"] = $refresh_token;
$token_params["audience"] = $this->clientID;
$token_params["subject_token_type"] = "urn:ietf:params:oauth:token-type:refresh_token";
$token_params["requested_token_type"] = "urn:ietf:params:oauth:token-type:access_token";
$token_params['client_assertion_type']=$client_assertion_type;
$token_params['client_assertion'] = $client_assertion;

unset($token_params['client_secret']);
unset($token_params['client_id']);
}
Expand Down Expand Up @@ -1177,7 +1171,7 @@ private function verifyRSAJWTsignature($hashtype, $key, $payload, $signature, $s

/**
* @param string $hashtype
* @param object $key
* @param string $key
* @param $payload
* @param $signature
* @return bool
Expand Down Expand Up @@ -1236,7 +1230,7 @@ public function verifyJWTsignature($jwt) {
$jwk = $header->jwk;
$this->verifyJWKHeader($jwk);
} else {
$jwks = json_decode($this->fetchURL($this->getProviderConfigValue('jwks_uri')));
$jwks = json_decode($this->fetchURL($this->getProviderConfigValue('jwks_uri')), false);
if ($jwks === NULL) {
throw new OpenIDConnectClientException('Error decoding JSON from jwks_uri');
}
Expand All @@ -1259,6 +1253,25 @@ public function verifyJWTsignature($jwt) {
return $verified;
}

/**
* @param string $jwt encoded JWT
* @return void
* @throws OpenIDConnectClientException
*/
public function verifySignatures($jwt)
{
if ($this->canVerifySignatures()) {
if (!$this->getProviderConfigValue('jwks_uri')) {
throw new OpenIDConnectClientException ('Unable to verify signature due to no jwks_uri being defined');
}
if (!$this->verifyJWTsignature($jwt)) {
throw new OpenIDConnectClientException ('Unable to verify signature');
}
} else {
user_error('Warning: JWT signature verification unavailable.');
}
}

/**
* @param string $iss
* @return bool
Expand Down Expand Up @@ -1359,10 +1372,39 @@ public function requestUserInfo($attribute = null) {
$headers = ["Authorization: Bearer {$this->accessToken}",
'Accept: application/json'];

$user_json = json_decode($this->fetchURL($user_info_endpoint,null,$headers));
$response = $this->fetchURL($user_info_endpoint,null,$headers);
if ($this->getResponseCode() <> 200) {
throw new OpenIDConnectClientException('The communication to retrieve user data has failed with status code '.$this->getResponseCode());
}

// When we receive application/jwt, the UserInfo Response is signed and/or encrypted.
if ($this->getResponseContentType() === 'application/jwt' ) {
// Check if the response is encrypted
$jwtHeaders = $this->decodeJWT($response);
if (isset($jwtHeaders->enc)) {
// Handle JWE
$jwt = $this->handleJweResponse($response);
} else {
// If the response is not encrypted then it must be signed
$jwt = $response;
}

// Verify the signature
$this->verifySignatures($jwt);

// Get claims from JWT
$claims = $this->decodeJWT($jwt, 1);

// Verify the JWT claims
if (!$this->verifyJWTclaims($claims)) {
throw new OpenIDConnectClientException('Invalid JWT signature');
}

$user_json = $claims;
} else {
$user_json = json_decode($response);
}

$this->userInfo = $user_json;

if($attribute === null) {
Expand Down Expand Up @@ -1490,6 +1532,7 @@ protected function fetchURL($url, $post_body = null, $headers = []) {
// HTTP Response code from server may be required from subclass
$info = curl_getinfo($ch);
$this->responseCode = $info['http_code'];
$this->responseContentType = $info['content_type'];

if ($output === false) {
throw new OpenIDConnectClientException('Curl error: (' . curl_errno($ch) . ') ' . curl_error($ch));
Expand Down Expand Up @@ -1744,7 +1787,7 @@ public function introspectToken($token, $token_type_hint = '', $clientId = null,
if ($this->supportsAuthMethod('client_secret_jwt', $token_endpoint_auth_methods_supported)) {
$client_assertion_type = $this->getProviderConfigValue('client_assertion_type');
$client_assertion = $this->getJWTClientAssertion($this->getProviderConfigValue('introspection_endpoint'));

$post_data['client_assertion_type']=$client_assertion_type;
$post_data['client_assertion'] = $client_assertion;
$headers = ['Accept: application/json'];
Expand Down Expand Up @@ -1984,6 +2027,16 @@ public function getResponseCode() {
return $this->responseCode;
}

/**
* Get the content type from last action/curl request.
*
* @return string|null
*/
public function getResponseContentType()
{
return $this->responseContentType;
}

/**
* Set timeout (seconds)
*
Expand Down Expand Up @@ -2085,7 +2138,7 @@ protected function getJWTClientAssertion($aud) {
]);
// Encode Header to Base64Url String
$base64UrlHeader = $this->urlEncode($header);


// Encode Payload to Base64Url String
$base64UrlPayload = $this->urlEncode($payload);
Expand All @@ -2100,12 +2153,12 @@ protected function getJWTClientAssertion($aud) {

// Encode Signature to Base64Url String
$base64UrlSignature = $this->urlEncode($signature);

$jwt = $base64UrlHeader . "." . $base64UrlPayload . "." . $base64UrlSignature;

return $jwt;
}

public function setUrlEncoding($curEncoding) {
switch ($curEncoding)
{
Expand Down Expand Up @@ -2188,6 +2241,16 @@ protected function verifyJWKHeader($jwk)
throw new OpenIDConnectClientException('Self signed JWK header is not valid');
}

/**
* @param string $jwe The JWE to decrypt
* @return string the JWT payload
* @throws OpenIDConnectClientException
*/
protected function handleJweResponse($jwe)
{
throw new OpenIDConnectClientException('JWE response is not supported, please extend the class and implement this method');
}

/*
* @return string
*/
Expand Down