Skip to content

Commit

Permalink
Merge pull request #305 from ricklambrechts/handle-application-jwt
Browse files Browse the repository at this point in the history
Added userInfo response type check to handle signed and encrypted res…
  • Loading branch information
DeepDiver1975 authored Dec 14, 2022
2 parents 2d78c15 + e3c3f9a commit 102a5bb
Show file tree
Hide file tree
Showing 2 changed files with 100 additions and 33 deletions.
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

0 comments on commit 102a5bb

Please sign in to comment.