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
Simple secret
Si on veut créer un simple secret (qu'on utilise pas JWKS), on peut exécuter:
openssl rand -hex 32
Ajuster la longueur du secret avec le dernier paramètre (ex: 64 versus 32).
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);
