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

Outils

Spécifications

JWT

JWKS

Structure

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);

Ressources