Skip to content

Commit

Permalink
Verifications OIDC (idtoken et nonce)
Browse files Browse the repository at this point in the history
  • Loading branch information
pierrelemee committed Jan 21, 2025
1 parent 6543eec commit 82f83c7
Show file tree
Hide file tree
Showing 7 changed files with 391 additions and 250 deletions.
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"php": ">=8.3",
"ext-ctype": "*",
"ext-iconv": "*",
"ext-openssl": "*",
"acsiomatic/device-detector-bundle": "^0.6.0",
"api-platform/core": "*",
"doctrine/dbal": "^3",
Expand Down
451 changes: 218 additions & 233 deletions composer.lock

Large diffs are not rendered by default.

16 changes: 8 additions & 8 deletions src/Security/Authenticator/ProConnectAuthenticator.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,18 +53,18 @@ public function authenticate(Request $request): Passport
// User info
$userInfo = $this->oidcClient->fetchUserInfo($token);

$agent = $this->agentRepository->findOneBy(['identifiant' => $userInfo->sub]);
$agent = $this->agentRepository->findOneBy(['identifiant' => $userInfo['sub']]);

if (null === $agent) {
$agent = (new Agent())
->setIdentifiant($userInfo->sub)
->setEmail($userInfo->email)
->setPrenom($userInfo->usual_name)
->setNom($userInfo->given_name)
->setIdentifiant($userInfo['sub'])
->setEmail($userInfo['email'])
->setPrenom($userInfo['usual_name'])
->setNom($userInfo['given_name'])
->addRole(Agent::ROLE_AGENT)
->setUid($userInfo->uid)
->setFournisseurIdentite($userInfo->idp_id)
->setDonnesAuthentification((array) $userInfo)
->setUid($userInfo['uid'])
->setFournisseurIdentite($userInfo['idp_id'])
->setDonnesAuthentification($userInfo)
;

$this->agentRepository->save($agent);
Expand Down
54 changes: 54 additions & 0 deletions src/Security/Jwt/Jwk.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?php

namespace MonIndemnisationJustice\Security\Jwt;

enum JwkKeyType: string
{
case RSA = 'RSA';
case EC = 'EC';
}

enum JwkUseType: string
{
case SIG = 'sig';
case ENC = 'enc';
}

enum JwkEncryptionAlgorithm: string
{
case ES256 = 'ES256';
case RS256 = 'RS256';
}

/**
* JSON Web Key (see [official RFC](https://datatracker.ietf.org/doc/html/rfc7517)).
*/
class Jwk
{
/**
* @var string the Key Type parameter
*/
public readonly JwkKeyType $kty;
public readonly JwkUseType $use;
public readonly JwkEncryptionAlgorithm $alg;
/**
* @var string the key ID
*/
public readonly string $kid;
public readonly array $data;

public static function fromArray(array $values): Jwk
{
$jwk = new Jwk();
$jwk->kty = JwkKeyType::from($values['kty']);
$jwk->use = JwkUseType::from($values['use']);
$jwk->alg = JwkEncryptionAlgorithm::from($values['alg']);
$jwk->kid = $values['kid'];
// Send as key data the other values
$jwk->data = array_filter($values, function ($k) {
return !in_array($k, ['kty', 'use', 'alg', 'kid']);
}, ARRAY_FILTER_USE_KEY);

return $jwk;
}
}
84 changes: 84 additions & 0 deletions src/Security/Jwt/Jwt.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
<?php

namespace MonIndemnisationJustice\Security\Jwt;

use Firebase\JWT\JWK as FirebaseJWK;
use Firebase\JWT\JWT as FirebaseJWT;
use Firebase\JWT\SignatureInvalidException;

class Jwt
{
protected readonly string $value;
protected readonly array $header;
protected readonly array $payload;
protected readonly string $message;
protected readonly string $signature;

public function __construct(
string $value,
) {
$this->value = $value;
list($header, $payload, $signature) = explode('.', $value);

$this->header = json_decode(base64_decode(urldecode($header)), true);
$this->payload = json_decode(base64_decode(urldecode($payload)), true);
$this->message = "{$header}.{$payload}";
$this->signature = $signature;
}

public function getPayload(): array
{
return $this->payload;
}

public function getValue(string $name): mixed
{
return $this->payload[$name] ?? null;
}

protected function extractJwkForAlgo(array $jwks, string $kid)
{
$key = array_search($kid, array_column($jwks, 'kid'));

return $jwks[$key];
}

protected function buildPem(array $jwk): string
{
// Ça ne fonctionne pas, voire vendor/firebase/php-jwt/src/JWK.php:231

return "-----BEGIN PUBLIC KEY-----\n".
chunk_split(base64_encode('MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA'.strtr($jwk['n'].'ID'.$jwk['e'], '-_', '+/')), 64).
'-----END PUBLIC KEY-----';
}

public function verify(array $jwks): bool
{
$jwk = $this->extractJwkForAlgo($jwks, $this->header['kid']);

try {
/*
return 1 === openssl_verify(
$this->message,
base64_decode($this->signature),
$this->buildPem($jwk))
OPENSSL_ALGO_SHA256
); */
FirebaseJWT::decode($this->value, FirebaseJWK::parseKey($jwk));

return true;
} catch (SignatureInvalidException) {
return false;
}
}

private static function base64Encode(string $value): string
{
return str_replace(['+', '/', '='], ['-', '_', ''], base64_encode($value));
}

public static function parse(string $jwt): Jwt
{
return new Jwt($jwt);
}
}
33 changes: 25 additions & 8 deletions src/Security/Oidc/OidcClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use GuzzleHttp\Client as HttpClient;
use GuzzleHttp\Exception\GuzzleException;
use GuzzleHttp\Exception\RequestException;
use MonIndemnisationJustice\Security\Jwt\Jwt;
use Ramsey\Uuid\Uuid;
use Symfony\Component\DependencyInjection\Attribute\Target;
use Symfony\Component\HttpFoundation\Request;
Expand All @@ -23,6 +24,7 @@ class OidcClient
{
protected HttpClient $client;
protected ?array $configuration = null;
protected ?array $jwks = null;

public function __construct(
protected readonly string $wellKnownUrl,
Expand All @@ -48,6 +50,18 @@ protected function configure(): void
}
});
}

if (null === $this->jwks) {
$this->jwks = $this->cache->get('oidc_jwks', function () {
try {
$response = $this->client->get($this->configuration['jwks_uri']);

return json_decode($response->getBody()->getContents(), true)['keys'];
} catch (GuzzleException $e) {
throw new AuthenticationException('Fetch of OIDC JWKs failed.');
}
});
}
}

protected function getRedirectUri(): string
Expand Down Expand Up @@ -115,20 +129,25 @@ public function authenticate(Request $request): string
$context = json_decode($e->getResponse()->getBody()->getContents());
throw new AuthenticationException("$context->error - $context->error_description", previous: $e);
} catch (GuzzleException $e) {
dump($e->getMessage(), $e->getTraceAsString());
throw new AuthenticationException('Authorization failed.', previous: $e);
}

$credentials = json_decode($response->getBody()->getContents());
$accessToken = $credentials->access_token ?? null;
$idToken = $credentials->id_token ?? null;
$idToken = Jwt::parse($credentials->id_token);

// TODO verifier le JWT et le nonce
if (!$idToken->verify($this->jwks)) {
throw new AuthenticationException('Authorization failed (invalid id token).');
}

if ($idToken->getValue('nonce') !== $context['nonce']) {
throw new AuthenticationException('Authorization failed (nonce does not match).');
}

return $accessToken;
}

public function fetchUserInfo(string $token): object
public function fetchUserInfo(string $token): array
{
$this->configure();

Expand All @@ -142,10 +161,8 @@ public function fetchUserInfo(string $token): object
throw new AuthenticationException('User info fetching failed.');
}

$jwt = $response->getBody()->getContents();

list($header, $payload, $signature) = explode('.', $jwt);
$jwt = Jwt::parse($response->getBody()->getContents());

return json_decode(base64_decode($payload));
return $jwt->getPayload();
}
}
2 changes: 1 addition & 1 deletion templates/agent/connexion.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
<div class="fr-container fr-my-3w">
<h1>Connexion à l'espace agent</h1>

{% if erreur is not null %}
{% if erreur is defined and erreur is not null %}
<div class="fr-grid-row fr-my-1w">
<div class="fr-alert fr-alert--error">
<h3 class="fr-alert__title">{{ erreur }}</h3>
Expand Down

0 comments on commit 82f83c7

Please sign in to comment.