Skip to main content

SIOP Authentication

The @cef-ebsi/siop-auth library provides authentication capabilities for natural persons and legal entities.

Installation

npm install @cef-ebsi/siop-auth

or if you use yarn

yarn add @cef-ebsi/siop-auth

Usage

The current EBSI SIOP Auth implementation follows RFC - DID-OIDC for NP/LE Authentication to EBSI & Relying Party in EBSI V2, which uses two JSON Web Tokens (JWT), where the Agent uses a DID and it is validated in the DID Registry, and the Relying Party uses an App which is validated in the Trusted Apps Registry.

The current version supports ES256K, ES256, RS256, and EdDSA algorithms.

Note: This version does have support for custom claims. (i.e. using VerifiableID).

The creation of a Relying Party is as follows:

import { RP, Agent, verifyJwtTar, verifyJwtDid } from "@cef-ebsi/siop-auth";
import { generateKeyPair } from "jose";

const privateKeyRP = (await generateKeyPair("ES256K")).privateKey;
const rp = new RP({
privateKey: privateKeyRP,
alg: "ES256K",
name: "test-appj2",
kid: "https://api-pilot.ebsi.eu/trusted-apps-registry/v3/apps/test-appj2",
redirectUri: "http://localhost:3000",
didRegistry: "https://api-pilot.ebsi.eu/did-registry/v3/identifiers",
});

The creation of an Agent for Natural Person or Legal Entity is as follows:

const privateKeyAgent = (await generateKeyPair("ES256K")).privateKey;
const agent = new Agent({
privateKey: privateKeyAgent,
alg: "ES256K",
kid: "did:ebsi:z21oU6xvBhsUQM49nw8KydE6#keys-1",
siopV2: true,
});

The Authentication flow has the following steps involving a Natural Person or Legal Entity (NP/LE) and a relying party (RP):

  1. The RP creates an authentication request
const uri = await rp.createRequest({
claims: { ... },
extraField: "extra data",
});

console.log(uri);

// openid://?response_type=id_token&client_id=http%253A%252F%252Flocalho
// st%253A3000&scope=openid%2520did_authn&nonce=33e7518b-b329-4824-809d-
// d1f548be850d&request=eyJhbGciOiJFUzI1NksiLCJ0eXAiOiJKV1QiLCJraWQiOiJo
// dHRwczovL2FwaS50ZXN0LmludGVic2kueHl6L3RydXN0ZWQtYXBwcy1yZWdpc3RyeS92M
// i9hcHBzLzB4MDQ0ODNiOWJlMWUxODdhYWE2YmUwMzRkNjM0ZmRmMDgyNWRmYWYzNWI4Y2
// UyMjI5YTRjZDNmM2U4ZTg1ZjM0NSJ9.eyJzY29wZSI6Im9wZW5pZCBkaWRfYXV0aG4iLC
// JyZXNwb25zZV90eXBlIjoiaWRfdG9rZW4iLCJyZXNwb25zZV9tb2RlIjoicG9zdCIsImN
// saWVudF9pZCI6Imh0dHA6Ly9sb2NhbGhvc3Q6MzAwMCIsInJlZGlyZWN0X3VyaSI6Imh0
// dHA6Ly9sb2NhbGhvc3Q6MzAwMCIsIm5vbmNlIjoiMzNlNzUxOGItYjMyOS00ODI0LTgwO
// WQtZDFmNTQ4YmU4NTBkIiwiY2xhaW1zIjp7fSwiZXh0cmFGaWVsZCI6ImV4dHJhIGRhdG
// EiLCJpYXQiOjE2NDQ4MzY3MDEsImlzcyI6InRlc3QtYXBwajIiLCJleHAiOjE2NDQ4Mzc
// wMDF9.AZCS5WartvILNs5pBhIXPlmVi8ZI65obdBM36ZPLY5FxnQF6d7sRodsXqKbIAvX
// wTxCdh024bXeK6yNVzH4dfg
  1. The agent verifies the authentication request
const urlParams = new URLSearchParams(uri.replace("openid://?", ""));
const { payload: payloadReq } = await verifyJwtTar(urlParams.get("request"), {
trustedAppsRegistry:
"https://api-pilot.ebsi.eu/trusted-apps-registry/v3/apps",
});
console.log(payloadReq);

// {
// scope: 'openid did_authn',
// response_type: 'id_token',
// response_mode: 'post',
// client_id: 'http://localhost:3000',
// redirect_uri: 'http://localhost:3000',
// nonce: '33e7518b-b329-4824-809d-d1f548be850d',
// claims: {},
// extraField: 'extra data',
// iat: 1644836701,
// iss: 'test-appj2',
// exp: 1644837001
// }
  1. The agent creates an authentication response
const encryptionKeyPair =
alg === "EdDSA"
? crypto.generateKeyPairSync("x25519")
: await generateKeyPair(alg);
const publicEncryptionKeyJwk = await exportJWK(encryptionKeyPair.publicKey);
const privateEncryptionKeyJwk = await exportJWK(encryptionKeyPair.privateKey);
const nonce = uuidv4();
const { urlEncoded } = await agent.createResponse({
nonce,
redirectUri: "http://localhost:3000",
claims: {
encryption_key: publicEncryptionKeyJwk,
},
extraField: "extra data",
});
console.log(urlEncoded);

// http://localhost:3000#id_token=eyJhbGciOiJFUzI1NksiLCJ0eXAiOiJKV1QiLC
// JraWQiOiJkaWQ6ZWJzaTp6MjFvVTZ4dkJoc1VRTTQ5bnc4S3lkRTYja2V5cy0xIn0.eyJ
// zdWIiOiI2dTVlUW0zVVZnMTBoa0VOcGMzVVhxQ3lIbVp2WDhaVG16MHo0LW1lcnJvIiwi
// YXVkIjoiaHR0cDovL2xvY2FsaG9zdDozMDAwIiwic3ViX2p3ayI6eyJrdHkiOiJFQyIsI
// mNydiI6InNlY3AyNTZrMSIsIngiOiJJR292OXFSV2Q5M1E4S0ZaSWtsaUNSaGtwZmZTVW
// 1hejZjS2JJM0txc0lNIiwieSI6ImE3WFVGSFBCYjRSOVBXUkg1TXY5UWdVLXlHOW51YTF
// lbFJPTFVxMk5lZ2cifSwibm9uY2UiOiI5NWYyZjUxNi1jYjIwLTQwMzMtOTU1Yy1mNjc5
// MWU5Mzk0NjAiLCJjbGFpbXMiOnsiZW5jcnlwdGlvbl9rZXkiOnsia3R5IjoiRUMiLCJjc
// nYiOiJzZWNwMjU2azEiLCJ4IjoiSU1QbFdTSS14TkloQkFBNjRaZ3ZYZFlxWXl4U3dfYW
// wtMHZlWkFsbFVEQSIsInkiOiJHRThEcEdhRExobEs4MlJxQVVHZHVBOHhGS3RMUDQ2SUd
// TdWJnNndSVC1nIn19LCJleHRyYUZpZWxkIjoiZXh0cmEgZGF0YSIsImlhdCI6MTY0NDgz
// NjcwMiwiaXNzIjoiaHR0cHM6Ly9zZWxmLWlzc3VlZC5tZS92MiIsImV4cCI6MTY0NDgzN
// zAwMn0.9XIcv31iw0FqJ6AEmYMmAcf7s3kBTeA_S1vWf8231Qrg6bXILeezBzdHiIOexN
// EnuwtRvvtuS0EycS7B1Ke_mw
  1. The RP verifies the authentication response It expects a callback to validate custom claims.
const idTokenAuthResponse = new URLSearchParams(
urlEncoded.substring(urlEncoded.indexOf("#") + 1)
).get("id_token") as string;

const resVerification = await RP.verifyResponse(
idTokenAuthResponse,
async (claims) => {
if (!claims || !claims.encryption_key)
throw new Error("no encryption_key found in the claims");

const { didDocument } = await verifyJwtDid(idTokenAuthResponse, {
didRegistry: "https://api-pilot.ebsi.eu/did-registry/v3/identifiers",
});

const did = didDocument?.id ?? "";

return { ...claims, did };
}
);
console.log(resVerification);

// {
// payload: {
// did: 'did:ebsi:z21oU6xvBhsUQM49nw8KydE6',
// sub: '6u5eQm3UVg10hkENpc3UXqCyHmZvX8ZTmz0z4-merro',
// aud: 'http://localhost:3000',
// sub_jwk: {
// kty: 'EC',
// crv: 'secp256k1',
// x: 'IGov9qRWd93Q8KFZIkliCRhkpffSUmaz6cKbI3KqsIM',
// y: 'a7XUFHPBb4R9PWRH5Mv9QgU-yG9nua1elROLUq2Negg'
// },
// nonce: '95f2f516-cb20-4033-955c-f6791e939460',
// claims: { encryption_key: { ... } },
// extraField: 'extra data',
// iat: 1644836702,
// iss: 'https://self-issued.me/v2',
// exp: 1644837002
// },
// header: {
// alg: 'ES256K',
// typ: 'JWT',
// kid: 'did:ebsi:z21oU6xvBhsUQM49nw8KydE6#keys-1'
// },
// resultClaims: {
// encryption_key: {
// kty: 'EC',
// crv: 'secp256k1',
// x: 'IMPlWSI-xNIhBAA64ZgvXdYqYyxSw_al-0veZAllUDA',
// y: 'GE8DpGaDLhlK82RqAUGduA8xFKtLP46IGSubg6wRT-g'
// },
// did: 'did:ebsi:z21oU6xvBhsUQM49nw8KydE6'
// }
// }
  1. The RP creates an access token
const akeAccessToken = await rp.createAccessToken(resVerification);
console.log(akeAccessToken);

// {
// ake1_enc_payload: '72da437d06839e054726e13df14fec940286ad1985026d50
// e8fdd652402378c1986d1505f336765f1ae1be114b347225b298c4a4ab9c8031c
// 614e01ec96c4edcc9f0353b8bfee8a16220adf3f4a71e50f2b1a326c8ca1f6750
// dcf4929ff19b0f1685e83533dd12d5c9db88636ba39175e2532e47a526edc612f
// d7b56008a6a49f6bd52c96dcf38065a644e613772bb15037381a755d7663dace9
// bb39abc2a3e8e4e0d59f86260d6043bc6171d97df581ca612239cd8007521b2d9
// ccf3dc807189b25dfa4b645f5daa911c434e273ab2d82dd07efe44e16580248e5
// 360801ffbdc9e6e84795ea97c4c3cff3d187dc50c3e1d3f9b3d4b1ab5e0f01123
// de96c1ef3a6fccd824ebdd5dd5d1a0238e25df3c1d3287cbd240071489d307edd
// c4452e3e88a141b70e470e517af30fbb8aa0a75f74898e7af25cf51f7676de9b5
// 2cf2ee67033c27e6e94d9592d2a87a7c3b937b7f17b568060d4dbc42995a3da5d
// 2179876d12baf664e1b1d2bddb2c80f491a4a33949a3b3e2f1bb50a1a1b90b656
// 1470eace49149564b34ea9adad45c6f51dc6985a928546cea9b20679ebb797bc2
// 13b5e5773bc870c143cbbe82b34f5ba3479b45c53c0c96ed66da08b173b5fce82
// 9feed4d45d3187d37a87ef77a5f9d55bb226b8c8c31065bf8d7f66d4051ddc26c
// 6dd288859db9cd49258f9da9d72ca10f69050d554a30c4112d562db6773424237
// 24abfd608105931a75fedf483b2dd67d0316138f68e6b7f6b9ba0ea8ffc1e0ea3
// b482810de56192b0d6af7adcc6905cc767a5f58d3c25e5c48b353bc14d2d0f964
// a2b475bcfddcd6774d9d1c9cda7ab37b0f5056f031001e76c524606c0a9ed1f1c
// 01ec3d00c4fb3b7e6af6f72cb0eb7e85c3cb8ee1cd65b9fbcff6a975ecd6b4c6a
// 113cc8ec2ff58bfb3c0fbb7e68cdbea82398a040ddb2a89cb0da32561e5cb4837
// 618c33eaa753dd8e438eac938206e3a6e9020c98525aa5715580bbbc7169ecfd9
// 30269eae30be3c06c3fdfc24a7c64c9358c1239a073a880535dd788eba3c4b458
// 9b2db371d9ffd93ae7401ebe52ad71f8454ce36c7bbb2b346208db2c72ee3577e
// 70b0e1f8b70e40a0ca216220bb74d697f207bcca562d956380247e9fbfbdc68ad
// 90f5648d2cdeff24aa9889d3625979adef35a1941c34b01d5282cc3880dbc3105
// f50bb4a80f4cccf7d155b02a618dfc87cf9f23dd15f4d03b509a16e356514922b
// 865858368a359aca260fe84969e3c6282c7c7442f0e3a690804e68d1ba9f9f73b
// b61dd15864b72a7e27b4018',
// ake1_sig_payload: {
// ake1_nonce: '95f2f516-cb20-4033-955c-f6791e939460',
// ake1_enc_payload: '72da437d06839e054726e13df14fec940286ad1985026d
// 50e8fdd652402378c1986d1505f336765f1ae1be114b347225b298c4a4ab9c8
// 031c614e01ec96c4edcc9f0353b8bfee8a16220adf3f4a71e50f2b1a326c8ca
// 1f6750dcf4929ff19b0f1685e83533dd12d5c9db88636ba39175e2532e47a52
// 6edc612fd7b56008a6a49f6bd52c96dcf38065a644e613772bb15037381a755
// d7663dace9bb39abc2a3e8e4e0d59f86260d6043bc6171d97df581ca612239c
// d8007521b2d9ccf3dc807189b25dfa4b645f5daa911c434e273ab2d82dd07ef
// e44e16580248e5360801ffbdc9e6e84795ea97c4c3cff3d187dc50c3e1d3f9b
// 3d4b1ab5e0f01123de96c1ef3a6fccd824ebdd5dd5d1a0238e25df3c1d3287c
// bd240071489d307eddc4452e3e88a141b70e470e517af30fbb8aa0a75f74898
// e7af25cf51f7676de9b52cf2ee67033c27e6e94d9592d2a87a7c3b937b7f17b
// 568060d4dbc42995a3da5d2179876d12baf664e1b1d2bddb2c80f491a4a3394
// 9a3b3e2f1bb50a1a1b90b6561470eace49149564b34ea9adad45c6f51dc6985
// a928546cea9b20679ebb797bc213b5e5773bc870c143cbbe82b34f5ba3479b4
// 5c53c0c96ed66da08b173b5fce829feed4d45d3187d37a87ef77a5f9d55bb22
// 6b8c8c31065bf8d7f66d4051ddc26c6dd288859db9cd49258f9da9d72ca10f6
// 9050d554a30c4112d562db677342423724abfd608105931a75fedf483b2dd67
// d0316138f68e6b7f6b9ba0ea8ffc1e0ea3b482810de56192b0d6af7adcc6905
// cc767a5f58d3c25e5c48b353bc14d2d0f964a2b475bcfddcd6774d9d1c9cda7
// ab37b0f5056f031001e76c524606c0a9ed1f1c01ec3d00c4fb3b7e6af6f72cb
// 0eb7e85c3cb8ee1cd65b9fbcff6a975ecd6b4c6a113cc8ec2ff58bfb3c0fbb7
// e68cdbea82398a040ddb2a89cb0da32561e5cb4837618c33eaa753dd8e438ea
// c938206e3a6e9020c98525aa5715580bbbc7169ecfd930269eae30be3c06c3f
// dfc24a7c64c9358c1239a073a880535dd788eba3c4b4589b2db371d9ffd93ae
// 7401ebe52ad71f8454ce36c7bbb2b346208db2c72ee3577e70b0e1f8b70e40a
// 0ca216220bb74d697f207bcca562d956380247e9fbfbdc68ad90f5648d2cdef
// f24aa9889d3625979adef35a1941c34b01d5282cc3880dbc3105f50bb4a80f4
// cccf7d155b02a618dfc87cf9f23dd15f4d03b509a16e356514922b865858368
// a359aca260fe84969e3c6282c7c7442f0e3a690804e68d1ba9f9f73bb61dd15
// 864b72a7e27b4018',
// did: 'did:ebsi:z21oU6xvBhsUQM49nw8KydE6',
// iat: 1644836702,
// iss: 'test-appj2',
// exp: 1644837602
// },
// ake1_jws_detached: 'eyJhbGciOiJFUzI1NksiLCJ0eXAiOiJKV1QiLCJraWQiOiJ
// odHRwczovL2FwaS50ZXN0LmludGVic2kueHl6L3RydXN0ZWQtYXBwcy1yZWdpc3Ry
// eS92Mi9hcHBzLzB4MDQ0ODNiOWJlMWUxODdhYWE2YmUwMzRkNjM0ZmRmMDgyNWRmY
// WYzNWI4Y2UyMjI5YTRjZDNmM2U4ZTg1ZjM0NSJ9..FDIeLfGMw6rehGvvIMu7ybpf
// g7BNDBDBHMMIfLm-9kfe9HNwFYh8jgjI7Z5bdl-u7-e9HPzfGMXuEgOHv15t3g',
// kid: 'https://api-pilot.ebsi.eu/trusted-apps-registry/v3/apps/test-appj2'
// }
  1. The agent verifies the ake response and gets the access token
const accessToken = await Agent.verifyAkeResponse(akeAccessToken, {
nonce,
privateEncryptionKeyJwk,
alg: "ES256K",
trustedAppsRegistry:
"https://api-pilot.ebsi.eu/trusted-apps-registry/v3/apps",
});
console.log(accessToken);

// eyJhbGciOiJFUzI1NksiLCJ0eXAiOiJKV1QiLCJraWQiOiJodHRwczovL2FwaS50ZXN0L
// mludGVic2kueHl6L3RydXN0ZWQtYXBwcy1yZWdpc3RyeS92Mi9hcHBzLzB4MDQ0ODNiOW
// JlMWUxODdhYWE2YmUwMzRkNjM0ZmRmMDgyNWRmYWYzNWI4Y2UyMjI5YTRjZDNmM2U4ZTg
// 1ZjM0NSJ9.eyJzdWIiOiJkaWQ6ZWJzaTp6MjFvVTZ4dkJoc1VRTTQ5bnc4S3lkRTYiLCJ
// kaWQiOiJkaWQ6ZWJzaTp6MjFvVTZ4dkJoc1VRTTQ5bnc4S3lkRTYiLCJhdWQiOiJlYnNp
// LWNvcmUtc2VydmljZXMiLCJub25jZSI6IjQ3M2M3N2VkLTZiOWItNDVjNi1hNThhLTgyM
// WI3NjA0NThmZiIsImxvZ2luX2hpbnQiOiJkaWRfc2lvcCIsImlhdCI6MTY0NDgzNjcwMi
// wiaXNzIjoidGVzdC1hcHBqMiIsImV4cCI6MTY0NDgzNzYwMn0.tlDsRB3w78DRdPhsL95
// mkOjB3x4Kmj7MHJisphOtUiM-v2_EoFLSACGBVPRd_YK9DWvNQ2bxR1BQbgRBgjdQtg
  1. A different service verifies the access token
await verifyJwtTar(accessToken, {
trustedAppsRegistry:
"https://api-pilot.ebsi.eu/trusted-apps-registry/v3/apps",
audience: "ebsi-core-services",
});

Prerequisites

It is assumed that the RP has an app registered in the Trusted Apps Registry.

Agent for natural persons

The EBSI DID Method for Natural Persons is also supported. Here is an example to create an agent for Natural Persons:

import { Agent } from "@cef-ebsi/siop-auth";
import { calculateJwkThumbprint, exportJWK, generateKeyPair, JWK } from "jose";

const keyPair = await generateKeyPair(alg);
const publicKeyJwkAgent = await exportJWK(keyPair.publicKey);
const thumbprint = await calculateJwkThumbprint(publicKeyJwkAgent, "sha256");
const subjectIdentifier = Buffer.from(thumbprint, "base64");
const kidAgent = `${EbsiWallet.createDid(
"NATURAL_PERSON",
subjectIdentifier
)}#${thumbprint}`;
const agent = new Agent({
privateKey: keyPair.privateKey,
alg,
kid: kidAgent,
siopV2: true,
});

When creating the authentication response, set the syntaxType to "did_subject" in the options in order to create the ID Token with the JWK in the headers. Example:

const { idToken } = await agent.createResponse(
{
nonce,
redirectUri: callbackUrl,
claims: {
encryption_key: publicKeyEncryptionJwk,
},
responseMode: "form_post",
},
{
syntaxType: "did_subject",
}
);