-
Notifications
You must be signed in to change notification settings - Fork 216
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
Add ID token validation #285
Changes from all commits
a9c487b
d00be63
e5e5163
e155c56
6070dda
3eb1762
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,104 +1,69 @@ | ||
<?php namespace Auth0\SDK\API\Helpers; | ||
<?php | ||
namespace Auth0\SDK\API\Helpers; | ||
|
||
use Firebase\JWT\JWT; | ||
|
||
/** | ||
* Class TokenGenerator. | ||
* Generates HS256 ID tokens. | ||
* | ||
* @package Auth0\SDK\API\Helpers | ||
*/ | ||
class TokenGenerator | ||
{ | ||
|
||
/** | ||
* | ||
* @var string | ||
* Default token expiration time. | ||
*/ | ||
protected $client_id; | ||
const DEFAULT_LIFETIME = 3600; | ||
|
||
/** | ||
* Audience for the ID token. | ||
* | ||
* @var string | ||
*/ | ||
protected $client_secret; | ||
protected $audience; | ||
|
||
/** | ||
* Secret used to encode the token. | ||
* | ||
* @var string | ||
*/ | ||
protected $secret_base64_encoded; | ||
|
||
/** | ||
* TokenGenerator Constructor. | ||
* | ||
* Configuration: | ||
* - client_id (String) Required. The id of the application, you can get this in the | ||
* auth0 console | ||
* - client_secret (String) Required. The application secret, same comment as above | ||
* - secret_base64_encoded (Bool) Required. Is the secret Base64 encoded? | ||
* | ||
* @param array $credentials | ||
*/ | ||
public function __construct($credentials) | ||
{ | ||
if (! isset($credentials['secret_base64_encoded'])) { | ||
$credentials['secret_base64_encoded'] = true; | ||
} | ||
|
||
$this->client_id = $credentials['client_id']; | ||
$this->client_secret = $credentials['client_secret']; | ||
$this->secret_base64_encoded = $credentials['secret_base64_encoded']; | ||
} | ||
protected $secret; | ||
|
||
/** | ||
* TokenGenerator constructor. | ||
* | ||
* @param string $input | ||
* @return string | ||
* @param string $audience ID token audience to set. | ||
* @param string $secret Token encryption secret to encode the token. | ||
*/ | ||
protected function bstr2bin($input) | ||
public function __construct($audience, $secret) | ||
{ | ||
// Unpack as a hexadecimal string | ||
$value = $this->str2hex($input); | ||
|
||
// Output binary representation | ||
return base_convert($value, 16, 2); | ||
$this->audience = $audience; | ||
$this->secret = $secret; | ||
} | ||
|
||
/** | ||
* Create the ID token. | ||
* | ||
* @param string $input | ||
* @return mixed | ||
*/ | ||
protected function str2hex($input) | ||
{ | ||
$data = unpack('H*', $input); | ||
return $data[1]; | ||
} | ||
|
||
/** | ||
* @param array $scopes Array of scopes to include. | ||
* @param integer $lifetime Lifetime of the token, in seconds. | ||
* @param boolean $secret_encoded True to base64 decode the client secret. | ||
* | ||
* @param $scopes | ||
* @param integer $lifetime | ||
* @return string | ||
*/ | ||
public function generate($scopes, $lifetime = 36000) | ||
public function generate(array $scopes, $lifetime = self::DEFAULT_LIFETIME, $secret_encoded = true) | ||
{ | ||
$time = time(); | ||
|
||
$payload = [ | ||
$time = time(); | ||
$payload = [ | ||
'iat' => $time, | ||
'scopes' => $scopes | ||
'scopes' => $scopes, | ||
'exp' => $time + $lifetime, | ||
'aud' => $this->audience, | ||
]; | ||
$payload['jti'] = md5(json_encode($payload)); | ||
|
||
$jti = md5(json_encode($payload)); | ||
|
||
$payload['jti'] = $jti; | ||
$payload['exp'] = $time + $lifetime; | ||
$payload['aud'] = $this->client_id; | ||
|
||
if ($this->secret_base64_encoded) { | ||
$secret = base64_decode(strtr($this->client_secret, '-_', '+/')); | ||
} else { | ||
$secret = $this->client_secret; | ||
} | ||
|
||
$jwt = JWT::encode($payload, $secret); | ||
$secret = $secret_encoded ? base64_decode(strtr($this->secret, '-_', '+/')) : $this->secret; | ||
|
||
return $jwt; | ||
return JWT::encode($payload, $secret); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -16,6 +16,7 @@ | |
use Auth0\SDK\API\Helpers\State\StateHandler; | ||
use Auth0\SDK\API\Helpers\State\SessionStateHandler; | ||
use Auth0\SDK\API\Helpers\State\DummyStateHandler; | ||
use Firebase\JWT\JWT; | ||
|
||
/** | ||
* Class Auth0 | ||
|
@@ -71,6 +72,14 @@ class Auth0 | |
*/ | ||
protected $clientSecret; | ||
|
||
/** | ||
* True if the client secret is base64 encoded, false if not. | ||
* This information can be found in your Auth0 Application settings below the Client Secret field. | ||
* | ||
* @var boolean | ||
*/ | ||
protected $clientSecretEncoded; | ||
|
||
/** | ||
* Response mode | ||
* | ||
|
@@ -173,6 +182,28 @@ class Auth0 | |
*/ | ||
protected $guzzleOptions; | ||
|
||
/** | ||
* Algorithm used for ID token validation. | ||
* Can be "HS256" or "RS256" only. | ||
* | ||
* @var string | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I guess there are a set of allowed values? |
||
*/ | ||
protected $idTokenAlg = null; | ||
|
||
/** | ||
* Valid audiences for ID tokens. | ||
* | ||
* @var array | ||
*/ | ||
protected $idTokenAud = []; | ||
|
||
/** | ||
* Valid issuer(s) for ID tokens. | ||
* | ||
* @var array | ||
*/ | ||
protected $idTokenIss = []; | ||
|
||
/** | ||
* State Handler. | ||
* | ||
|
@@ -225,10 +256,11 @@ public function __construct(array $config) | |
throw new CoreException('Invalid redirect_uri'); | ||
} | ||
|
||
$this->domain = $config['domain']; | ||
$this->clientId = $config['client_id']; | ||
$this->clientSecret = $config['client_secret']; | ||
$this->redirectUri = $config['redirect_uri']; | ||
$this->domain = $config['domain']; | ||
$this->clientId = $config['client_id']; | ||
$this->clientSecret = $config['client_secret']; | ||
$this->clientSecretEncoded = ! empty( $config['secret_base64_encoded'] ); | ||
$this->redirectUri = $config['redirect_uri']; | ||
|
||
if (isset($config['audience'])) { | ||
$this->audience = $config['audience']; | ||
|
@@ -250,6 +282,33 @@ public function __construct(array $config) | |
$this->guzzleOptions = $config['guzzle_options']; | ||
} | ||
|
||
// If a token algorithm is passed, make sure it's a specific string. | ||
if (! empty($config['id_token_alg'])) { | ||
if (! in_array( $config['id_token_alg'], ['HS256', 'RS256'] )) { | ||
throw new CoreException('Invalid id_token_alg; must be "HS256" or "RS256"'); | ||
} | ||
|
||
$this->idTokenAlg = $config['id_token_alg']; | ||
} | ||
|
||
// If a token audience is passed, make sure it's an array. | ||
if (! empty($config['id_token_aud'])) { | ||
if (! is_array( $config['id_token_aud'] )) { | ||
throw new CoreException('Invalid id_token_aud; must be an array of string values'); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What if I pass an array of e.g. integers? This exception won't be thrown as the message says There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Or an array of objects or anything else but I think this message is helpful none-the-less. |
||
} | ||
|
||
$this->idTokenAud = $config['id_token_aud']; | ||
} | ||
|
||
// If a token issuer is passed, make sure it's an array. | ||
if (! empty($config['id_token_iss'])) { | ||
if (! is_array( $config['id_token_iss'] )) { | ||
throw new CoreException('Invalid id_token_iss; must be an array of string values'); | ||
} | ||
|
||
$this->idTokenIss = $config['id_token_iss']; | ||
} | ||
|
||
$this->debugMode = isset($config['debug']) ? $config['debug'] : false; | ||
|
||
// User info is persisted by default. | ||
|
@@ -552,14 +611,27 @@ public function setAccessToken($accessToken) | |
} | ||
|
||
/** | ||
* Sets and persists the ID token. | ||
* Sets, validates, and persists the ID token. | ||
* | ||
* @param string $idToken - ID token returned from the code exchange. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'll mention validation in the short desc but I don't want to go too crazy with the documentation here, might as well look at the implementation. |
||
* | ||
* @return \Auth0\SDK\Auth0 | ||
* | ||
* @throws CoreException | ||
* @throws Exception\InvalidTokenException | ||
*/ | ||
public function setIdToken($idToken) | ||
{ | ||
$jwtVerifier = new JWTVerifier([ | ||
'valid_audiences' => ! empty($this->idTokenAud) ? $this->idTokenAud : [ $this->clientId ], | ||
'supported_algs' => $this->idTokenAlg ? [ $this->idTokenAlg ] : [ 'HS256', 'RS256' ], | ||
'authorized_iss' => $this->idTokenIss ? $this->idTokenIss : [ 'https://'.$this->domain.'/' ], | ||
'client_secret' => $this->clientSecret, | ||
'secret_base64_encoded' => $this->clientSecretEncoded, | ||
'guzzle_options' => $this->guzzleOptions, | ||
]); | ||
$jwtVerifier->verifyAndDecode( $idToken ); | ||
|
||
if (in_array('id_token', $this->persistantMap)) { | ||
$this->store->set('id_token', $idToken); | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I imagine this class is only used on tests, right? If so, wouldn't the correct namespace be under a
test
folder?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's how it's used in the SDK, yes, but it's already in a public namespace so removing would be breaking. Making a note of this for the major.