Table des matières
JSON Web Token
Le JSON Web Token (JWT, prononcé jot) est un standard d'authentification.
http://self-issued.info/docs/draft-ietf-oauth-json-web-token.html
Packages node
- jose et documentation (panva, moins utilisée, plus complet)
- jose (version cisco, plus utilisée, moins complet)
Outils
Spécifications
JWT
JWKS
Structure
Header
Payload
iss | Issuer |
sub | Subject |
exp | Expiration |
Exemple d'ID Token Cognito
Header:
{ "kid": "S/2sYDHJPpXvQcl8tbKE5A+AESbxu/g6JfQWXH7Jj60=", "alg": "RS256" }
Payload
{ "sub": "39d769aa-f1a5-4e47-b6e0-a302f819ba82", "aud": "7diiepl30kilf5fhgk1b2jf7c5", "email_verified": true, "event_id": "d1f41e61-5fdd-4815-a333-c5b86ca9c16e", "token_use": "id", "auth_time": 1595250673, "iss": "https://cognito-idp.ca-central-1.amazonaws.com/ca-central-1_dWajkYc7s", "cognito:username": "johndoe@gmail.com", "exp": 1595254273, "iat": 1595250673, "email": "johndoe@gmail.com" }
Packages npm
jsonwebtoken
npm install jsonwebtoken -S npm install @types/jsonwebtoken -D
Import:
import * as jwt from 'jsonwebtoken'; // ou import jwt from 'jsonwebtoken';
Signer:
const jwtToken = jwt.sign( { userId }, this.secretsConfig.jwt, { algorithm: 'HS256', expiresIn: '14d' } );
Avec RS256:
const privateKey = fs.readFileSync('private.key'); const token = jwt.sign({ foo: 'bar' }, privateKey, { algorithm: 'RS256' });
Vérifier:
const authHeader = req.header('x-auth-token'); const token = jwt.verify( authHeader, secret, { algorithms: ['HS256'] } );
Verify asymetric :
import jwks from 'jwks-rsa’; const client = jwksClient({ jwksUri: 'https://sandrino.auth0.com/.well-known/jwks.json' }); function getKey(header, callback){ client.getSigningKey(header.kid, function(err, key) { var signingKey = key.publicKey || key.rsaPublicKey; callback(null, signingKey); }); } jwt.verify(token, getKey, options, function(err, decoded) { console.log(decoded.foo) // bar });
jwks-rsa
Importer:
import jwks from 'jwks-rsa’; // ou import * as jwks from 'jwks-rsa'; <code> <code> const jwksClient = jwksClient({ cache: true, jwksUri: 'https://appleid.apple.com/auth/keys', });
JOSE
Clé privée
Générer la clé privée:
$ openssl genrsa -out rsa-2048.pem 2048
Une clé de longueur 2048 bits semble suffisante jusqu'en 2030. Sinon utiliser 3072 ou 4096.
Avec ssh-keygen:
ssh-keygen -t rsa -b 4096 -m PEM -f jwtRS256.key # Don't add passphrase openssl rsa -in jwtRS256.key -pubout -outform PEM -out jwtRS256.key.pub cat jwtRS256.key cat jwtRS256.key.pub
Créer un JWT
const privateKey = readFileSync('./path/to/rsa-2048.pem'); const rsaKey = JWK.asKey(privateKey); const payload = { 'urn:example:claim': 'foo' }; const token = JWT.sign(payload, rsaKey, { algorithm: 'RS256', audience: ['urn:example:client'], issuer: 'https://op.example.com', expiresIn: '2 hours', header: { typ: 'JWT' } });
Créer le endpoint JWKS
Quand on importe la clé privée RSA, on peut demander la clé publique:
const privateKey = readFileSync('./path/to/rsa-2048.pem'); const rsaKey = JWK.asKey(privateKey); const publicKey = JSON.stringify(rsaKey.toJWK());
Ce qui donne quelque chose de ce genre:
{ "e":"AQAB", "n":"5ColF8Lypyud9iKJjOeaTG7yP-KxdJ...9FMDEBQ", "kty":"RSA", "kid":"O06jlZQ_2JW0UXF6qvbMIFdaXFNnIoKHS9aVbv5-Mvc" }
Et comme on le veut dans un set
(JWKS), on peut faire ceci:
{ "keys": [ { "e":"AQAB", "n":"5ColF8Lypyud9iKJjOeaTG7yP-KxdJ...9FMDEBQ", "kty":"RSA", "kid":"O06jlZQ_2JW0UXF6qvbMIFdaXFNnIoKHS9aVbv5-Mvc" } ] }
On peut ajouter la propriété “alg”: “RS256”,
, mais n'est pas nécessaire pour valider la signature plus tard.
Validation avec JWKS
Pour importer les fonctions nécessaires de jose
:
import { JWK, JWT, JWKS } from 'jose';
On récupère les clés publiques via un endpoint JWKS, normalement sous une URL /.well-known/jwks.json
.
const jwksEndpoint = 'https://c1058d53fc6f.ngrok.io/.well-known/jwks.json'; // Exemple d'URL de JWKS const { data } = await axios.get<JWKSKeys>( jwksEndpoint, ); const jwksKeys = data;
Interface de JWKSKeys:
export interface JWKSKeys { keys: [ { kty: string; e: string; use?: string; kid: string; alg?: string; n: string; } ] }
On transforme le data reçu en clés jose
:
const rsaKeys = jwksKeys.keys.map((key) => JWK.asKey(key)); const keystore = new JWKS.KeyStore(rsaKeys);
Ensuite la validation en donnant le keystore.
let tokenVerification: TokenPayload; try { tokenDecoded = JWT.verify(payload.jwt, keystore) as TokenPayload; } catch (e) { console.error('Token Verification failed.', e); } console.log(tokenDecoded);
Le TokenPayload
dépend de ce qu'on veut mettre dans le payload du JWT, ceci est un exemple:
export interface TokenPayload { name: string; nickname: string; picture: string; email: string; // eslint-disable-next-line camelcase email_verified: boolean; }
jwks-rsa
Exemple d’utilisation de JWKS-RSA.
import * as jwksClient from 'jwks-rsa'; // *** skipped code *** const client = jwksClient({ strictSsl: true, // Default value jwksUri: 'https://7426c678d401.ngrok.io/.well-known/jwks.json', }); const kid = 'id12345'; const agetSigningKey = promisify(client.getSigningKey).bind(client); return agetSigningKey(kid);