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

Add ID token validation #285

Merged
merged 6 commits into from
Oct 8, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,7 @@
"test": "SHELL_INTERACTIVE=1 vendor/bin/phpunit --colors=always --coverage-text",
"test-ci": "vendor/bin/phpunit --colors=always --coverage-clover=build/coverage.xml",
"phpcs": "\"vendor/bin/phpcs\"",
"phpcs-path": "SHELL_INTERACTIVE=1 ./vendor/bin/phpcs",
"phpcbf": "\"vendor/bin/phpcbf\"",
"phpcbf-path": "SHELL_INTERACTIVE=1 ./vendor/bin/phpcbf",
"sniffs": "\"vendor/bin/phpcs\" -e",
"config-phpcs": "Dealerdirect\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\Plugin::run"
}
Expand Down
101 changes: 33 additions & 68 deletions src/API/Helpers/TokenGenerator.php
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
Copy link
Contributor

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?

Copy link
Contributor Author

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.

*/
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);
}
}
82 changes: 77 additions & 5 deletions src/Auth0.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
*
Expand Down Expand Up @@ -173,6 +182,28 @@ class Auth0
*/
protected $guzzleOptions;

/**
* Algorithm used for ID token validation.
* Can be "HS256" or "RS256" only.
*
* @var string
Copy link
Contributor

Choose a reason for hiding this comment

The 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.
*
Expand Down Expand Up @@ -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'];
Expand All @@ -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');
Copy link
Contributor

Choose a reason for hiding this comment

The 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

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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.
Expand Down Expand Up @@ -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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The setAccessToken above doesn't do any verification, that's OK. But maybe it's worth mentioning in the docblock that this setIdToken does verify the token using x,y,z conditions/expected values

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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);
}
Expand Down
Loading