Skip to content

Commit

Permalink
Merge pull request #13 from GewoonYorick/nonce
Browse files Browse the repository at this point in the history
Adding support for a nonce
  • Loading branch information
jeremy379 authored Apr 25, 2024
2 parents 9570521 + 7041c46 commit 76ac2bd
Show file tree
Hide file tree
Showing 6 changed files with 230 additions and 10 deletions.
97 changes: 97 additions & 0 deletions src/Grant/AuthCodeGrant.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
<?php

namespace OpenIDConnect\Grant;

use DateInterval;
use League\OAuth2\Server\Repositories\AuthCodeRepositoryInterface;
use League\OAuth2\Server\Repositories\RefreshTokenRepositoryInterface;
use League\OAuth2\Server\RequestTypes\AuthorizationRequest;
use OpenIDConnect\Interfaces\CurrentRequestServiceInterface;
use Psr\Http\Message\ResponseInterface;

/**
* This class extends the default AuthCodeGrant class to add support for the nonce parameter.
*
* The nonce parameter is
*/
class AuthCodeGrant extends \League\OAuth2\Server\Grant\AuthCodeGrant
{
private ResponseInterface $psr7Response;
private CurrentRequestServiceInterface $currentRequestService;

/**
* @param AuthCodeRepositoryInterface $authCodeRepository
* @param RefreshTokenRepositoryInterface $refreshTokenRepository
* @param DateInterval $authCodeTTL
* @param ResponseInterface $psr7Response An empty PSR-7 Response object
* @param CurrentRequestServiceInterface
* $currentRequestService A service that returns the current request. Used to get the nonce parameter.
* @throws \Exception
*/
public function __construct(
AuthCodeRepositoryInterface $authCodeRepository,
RefreshTokenRepositoryInterface $refreshTokenRepository,
DateInterval $authCodeTTL,
ResponseInterface $psr7Response,
CurrentRequestServiceInterface $currentRequestService)
{
parent::__construct($authCodeRepository, $refreshTokenRepository, $authCodeTTL);
$this->psr7Response = $psr7Response;
$this->currentRequestService = $currentRequestService;
}

/**
* {@inheritdoc}
*/
public function completeAuthorizationRequest(AuthorizationRequest $authorizationRequest)
{
// See https://github.com/steverhoades/oauth2-openid-connect-server/issues/47#issuecomment-1228370632

/** @var RedirectResponse $response */
$response = parent::completeAuthorizationRequest($authorizationRequest);

$queryParams = $this->currentRequestService->getRequest()->getQueryParams();

if (isset($queryParams['nonce'])) {
// The only way to get the redirect URI is to generate the PSR7 response
// (The RedirectResponse class does not have a getter for the redirect URI)
$httpResponse = $response->generateHttpResponse($this->psr7Response);
$redirectUri = $httpResponse->getHeader('Location')[0];
$parsed = parse_url($redirectUri);

parse_str($parsed['query'], $query);

$authCodePayload = json_decode($this->decrypt($query['code']), true, 512, JSON_THROW_ON_ERROR);

$authCodePayload['nonce'] = $queryParams['nonce'];

$query['code'] = $this->encrypt(json_encode($authCodePayload, JSON_THROW_ON_ERROR));

$parsed['query'] = http_build_query($query);

$response->setRedirectUri($this->unparse_url($parsed));
}

return $response;
}

/**
* Inverse of parse_url
*
* @param mixed $parsed_url
* @return string
*/
private function unparse_url($parsed_url)
{
$scheme = isset($parsed_url['scheme']) ? $parsed_url['scheme'] . '://' : '';
$host = isset($parsed_url['host']) ? $parsed_url['host'] : '';
$port = isset($parsed_url['port']) ? ':' . $parsed_url['port'] : '';
$user = isset($parsed_url['user']) ? $parsed_url['user'] : '';
$pass = isset($parsed_url['pass']) ? ':' . $parsed_url['pass'] : '';
$pass = ($user || $pass) ? "$pass@" : '';
$path = isset($parsed_url['path']) ? $parsed_url['path'] : '';
$query = isset($parsed_url['query']) ? '?' . $parsed_url['query'] : '';
$fragment = isset($parsed_url['fragment']) ? '#' . $parsed_url['fragment'] : '';
return "$scheme$user$pass$host$port$path$query$fragment";
}
}
40 changes: 32 additions & 8 deletions src/IdTokenResponse.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,23 @@
use DateTimeImmutable;
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
{
class IdTokenResponse extends BearerTokenResponse {
use CryptTrait;

protected IdentityRepositoryInterface $identityRepository;

protected ClaimExtractor $claimExtractor;

private Configuration $config;
private ?CurrentRequestServiceInterface $currentRequestService;

private array $tokenHeaders;

Expand All @@ -29,13 +33,17 @@ public function __construct(
ClaimExtractor $claimExtractor,
Configuration $config,
array $tokenHeaders = [],
bool $useMicroseconds = true
bool $useMicroseconds = true,
CurrentRequestServiceInterface $currentRequestService = null,
$encryptionKey = null,
) {
$this->identityRepository = $identityRepository;
$this->claimExtractor = $claimExtractor;
$this->config = $config;
$this->tokenHeaders = $tokenHeaders;
$this->useMicroseconds = $useMicroseconds;
$this->currentRequestService = $currentRequestService;
$this->encryptionKey = $encryptionKey;
}

protected function getBuilder(
Expand All @@ -47,17 +55,23 @@ protected function getBuilder(
($this->useMicroseconds ? microtime(true) : time())
);

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('https://' . $_SERVER['HTTP_HOST'])
->issuedBy($issuer)
->issuedAt($dateTimeImmutableObject)
->expiresAt($dateTimeImmutableObject->add(new DateInterval('PT1H')))
->relatedTo($userEntity->getIdentifier());
}

protected function getExtraParams(AccessTokenEntityInterface $accessToken): array
{
protected function getExtraParams(AccessTokenEntityInterface $accessToken): array {
if (!$this->hasOpenIDScope(...$accessToken->getScopes())) {
return [];
}
Expand All @@ -72,6 +86,17 @@ protected function getExtraParams(AccessTokenEntityInterface $accessToken): arra
$builder = $builder->withHeader($key, $value);
}

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']);
}
}
}

$claims = $this->claimExtractor->extract(
$accessToken->getScopes(),
$user->getClaims(),
Expand All @@ -89,8 +114,7 @@ protected function getExtraParams(AccessTokenEntityInterface $accessToken): arra
return ['id_token' => $token->toString()];
}

private function hasOpenIDScope(ScopeEntityInterface ...$scopes): bool
{
private function hasOpenIDScope(ScopeEntityInterface ...$scopes): bool {
foreach ($scopes as $scope) {
if ($scope->getIdentifier() === 'openid') {
return true;
Expand Down
19 changes: 19 additions & 0 deletions src/Interfaces/CurrentRequestServiceInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

namespace OpenIDConnect\Interfaces;

use Psr\Http\Message\ServerRequestInterface;

/**
* A service in charge of returning the current request.
*
* This should be implemented by the application using this package (a default Laravel implementation is provided)
*
* We need this because due to the architecture of the League package, the request is not available in the
* grant classes. But we need access to the "nonce" parameter in the request to be able to include it in the
* ID token.
*/
interface CurrentRequestServiceInterface
{
public function getRequest(): ServerRequestInterface;
}
21 changes: 21 additions & 0 deletions src/Laravel/LaravelCurrentRequestService.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

namespace OpenIDConnect\Laravel;

use Nyholm\Psr7\Factory\Psr17Factory;
use OpenIDConnect\Interfaces\CurrentRequestServiceInterface;
use Psr\Http\Message\ServerRequestInterface;
use Symfony\Bridge\PsrHttpMessage\Factory\PsrHttpFactory;

class LaravelCurrentRequestService implements CurrentRequestServiceInterface
{
public function getRequest(): ServerRequestInterface
{
return (new PsrHttpFactory(
new Psr17Factory,
new Psr17Factory,
new Psr17Factory,
new Psr17Factory
))->createRequest(request());
}
}
39 changes: 37 additions & 2 deletions src/Laravel/PassportServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@
use Lcobucci\JWT\Configuration;
use Lcobucci\JWT\Signer\Key\InMemory;
use League\OAuth2\Server\AuthorizationServer;
use Nyholm\Psr7\Response;
use OpenIDConnect\ClaimExtractor;
use OpenIDConnect\Claims\ClaimSet;
use OpenIDConnect\Grant\AuthCodeGrant;
use OpenIDConnect\IdTokenResponse;

class PassportServiceProvider extends Passport\PassportServiceProvider
Expand All @@ -34,11 +36,14 @@ public function boot()
], ['openid', 'openid-config']);

$this->loadRoutesFrom(__DIR__."/routes/web.php");

$this->registerClaimExtractor();
}

public function makeAuthorizationServer(): AuthorizationServer
{
$cryptKey = $this->makeCryptKey('private');
$encryptionKey = app(Encrypter::class)->getKey();

$customClaimSets = config('openid.custom_claim_sets');

Expand All @@ -54,16 +59,46 @@ public function makeAuthorizationServer(): AuthorizationServer
InMemory::plainText($cryptKey->getKeyContents(), $cryptKey->getPassPhrase() ?? '')
),
config('openid.token_headers'),
config('openid.use_microseconds')
config('openid.use_microseconds'),
app(LaravelCurrentRequestService::class),
$encryptionKey,
);

return new AuthorizationServer(
app(ClientRepository::class),
app(AccessTokenRepository::class),
app(Passport\Bridge\ScopeRepository::class),
$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');

$claimSets = array_map(function ($claimSet, $name) {
return new ClaimSet($name, $claimSet);
}, $customClaimSets, array_keys($customClaimSets));

return new ClaimExtractor(...$claimSets);
});
}
}
24 changes: 24 additions & 0 deletions src/Services/CurrentRequestService.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

namespace OpenIDConnect\Services;

use OpenIDConnect\Interfaces\CurrentRequestServiceInterface;
use Psr\Http\Message\ServerRequestInterface;

class CurrentRequestService implements CurrentRequestServiceInterface
{
private ?ServerRequestInterface $request;

public function getRequest(): ServerRequestInterface
{
if ($this->request === null) {
throw new \RuntimeException('Request not set in CurrentRequestService');
}
return $this->request;
}

public function setRequest(ServerRequestInterface $request): void
{
$this->request = $request;
}
}

0 comments on commit 76ac2bd

Please sign in to comment.