diff --git a/README.md b/README.md index 9e480ea..f7d8838 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,9 @@ To enable OpenID Connect, follow these simple steps ```php $privateKeyPath = 'tmp/private.key'; +$currentRequestService = new CurrentRequestService(); +$currentRequestService->setRequest(ServerRequestFactory::fromGlobals()); + // create the response_type $responseType = new IdTokenResponse( new IdentityRepository(), @@ -44,6 +47,8 @@ $responseType = new IdTokenResponse( new Sha256(), InMemory::file($privateKeyPath), ), + $currentRequestService, + $encryptionKey, ); $server = new \League\OAuth2\Server\AuthorizationServer( @@ -62,6 +67,17 @@ Provide more scopes (e.g. `openid profile email`) to receive additional claims i For a complete implementation, visit [the OAuth2 Server example](https://github.com/ronvanderheijden/openid-connect/tree/main/example). +## Nonce support + +To prevent replay attacks, some clients can provide a "nonce" in the authorization request. If a client does so, the +server MUST include back a `nonce` claim in the `id_token`. + +To enable this feature, when registering an AuthCodeGrant, you need to use the `\OpenIDConnect\Grant\AuthCodeGrant` +instead of `\League\OAuth2\Server\Grant\AuthCodeGrant`. + +> ![NOTE] +> If you are using Laravel, the `AuthCodeGrant` is already registered for you by the service provider. + ## Laravel Passport You can use this package with Laravel Passport in 2 simple steps. diff --git a/src/IdTokenResponse.php b/src/IdTokenResponse.php index 4f7a15a..6904d7f 100644 --- a/src/IdTokenResponse.php +++ b/src/IdTokenResponse.php @@ -6,33 +6,43 @@ use DateInterval; use DateTimeImmutable; +use Defuse\Crypto\Key; use Lcobucci\JWT\Builder; use Lcobucci\JWT\Configuration; +use League\OAuth2\Server\CryptTrait; use League\OAuth2\Server\Entities\AccessTokenEntityInterface; use League\OAuth2\Server\Entities\ScopeEntityInterface; use League\OAuth2\Server\ResponseTypes\BearerTokenResponse; +use OpenIDConnect\Interfaces\CurrentRequestServiceInterface; use OpenIDConnect\Interfaces\IdentityEntityInterface; use OpenIDConnect\Interfaces\IdentityRepositoryInterface; class IdTokenResponse extends BearerTokenResponse { + use CryptTrait; + protected IdentityRepositoryInterface $identityRepository; protected ClaimExtractor $claimExtractor; private Configuration $config; - private ?string $issuer; + private ?CurrentRequestServiceInterface $currentRequestService; + /** + * @param string|Key|null $encryptionKey + */ public function __construct( IdentityRepositoryInterface $identityRepository, ClaimExtractor $claimExtractor, Configuration $config, - string $issuer = null, + CurrentRequestServiceInterface $currentRequestService = null, + $encryptionKey = null, ) { $this->identityRepository = $identityRepository; $this->claimExtractor = $claimExtractor; $this->config = $config; - $this->issuer = $issuer; + $this->currentRequestService = $currentRequestService; + $this->encryptionKey = $encryptionKey; } protected function getBuilder( @@ -41,10 +51,17 @@ protected function getBuilder( ): Builder { $dateTimeImmutableObject = new DateTimeImmutable(); + if ($this->currentRequestService) { + $uri = $this->currentRequestService->getRequest()->getUri(); + $issuer = $uri->getScheme() . '://' . $uri->getHost() . ($uri->getPort() ? ':' . $uri->getPort() : ''); + } else { + $issuer = 'https://' . $_SERVER['HTTP_HOST']; + } + return $this->config ->builder() ->permittedFor($accessToken->getClient()->getIdentifier()) - ->issuedBy($this->issuer ?? 'https://' . $_SERVER['HTTP_HOST']) + ->issuedBy($issuer) ->issuedAt($dateTimeImmutableObject) ->expiresAt($dateTimeImmutableObject->add(new DateInterval('PT1H'))) ->relatedTo($userEntity->getIdentifier()); @@ -71,6 +88,17 @@ protected function getExtraParams(AccessTokenEntityInterface $accessToken): arra $builder = $builder->withClaim($claimName, $claimValue); } + if ($this->currentRequestService) { + // If the request contains a code, we look into the code to find the nonce. + $body = $this->currentRequestService->getRequest()->getParsedBody(); + if (isset($body['code'])) { + $authCodePayload = json_decode($this->decrypt($body['code']), true, 512, JSON_THROW_ON_ERROR); + if (isset($authCodePayload['nonce'])) { + $builder = $builder->withClaim('nonce', $authCodePayload['nonce']); + } + } + } + $token = $builder->getToken( $this->config->signer(), $this->config->signingKey(), diff --git a/src/Laravel/PassportServiceProvider.php b/src/Laravel/PassportServiceProvider.php index 85258d6..4fbda80 100644 --- a/src/Laravel/PassportServiceProvider.php +++ b/src/Laravel/PassportServiceProvider.php @@ -11,8 +11,11 @@ use Lcobucci\JWT\Configuration; use Lcobucci\JWT\Signer\Key\InMemory; use League\OAuth2\Server\AuthorizationServer; +use League\OAuth2\Server\CryptTrait; +use Nyholm\Psr7\Response; use OpenIDConnect\ClaimExtractor; use OpenIDConnect\Claims\ClaimSet; +use OpenIDConnect\Grant\AuthCodeGrant; use OpenIDConnect\IdTokenResponse; class PassportServiceProvider extends Passport\PassportServiceProvider @@ -45,6 +48,7 @@ public function boot() public function makeAuthorizationServer(): AuthorizationServer { $cryptKey = $this->makeCryptKey('private'); + $encryptionKey = app(Encrypter::class)->getKey(); $responseType = new IdTokenResponse( app(config('openid.repositories.identity')), @@ -53,7 +57,8 @@ public function makeAuthorizationServer(): AuthorizationServer app(config('openid.signer')), InMemory::file($cryptKey->getKeyPath()), ), - app('request')->getSchemeAndHttpHost(), + app(LaravelCurrentRequestService::class), + $encryptionKey, ); return new AuthorizationServer( @@ -61,11 +66,27 @@ public function makeAuthorizationServer(): AuthorizationServer app(AccessTokenRepository::class), app(config('openid.repositories.scope')), $cryptKey, - app(Encrypter::class)->getKey(), + $encryptionKey, $responseType, ); } + /** + * Build the Auth Code grant instance. + * + * @return AuthCodeGrant + */ + protected function buildAuthCodeGrant() + { + return new AuthCodeGrant( + $this->app->make(Passport\Bridge\AuthCodeRepository::class), + $this->app->make(Passport\Bridge\RefreshTokenRepository::class), + new \DateInterval('PT10M'), + new Response(), + $this->app->make(LaravelCurrentRequestService::class), + ); + } + public function registerClaimExtractor() { $this->app->singleton(ClaimExtractor::class, function () { $customClaimSets = config('openid.custom_claim_sets');