@cef-ebsi/siop-auth
TypeScript icon, indicating that this package has built-in type declarations

4.0.1 • Public • Published

EBSI Logo

EBSI SIOP Auth Library

EBSI Auth library for natural persons and legal entities.

Table of Contents

  1. Installation
  2. Usage
  3. License

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-test.ebsi.eu/trusted-apps-registry/v4/apps/test-appj2",
  redirectUri: "http://localhost:3000",
  didRegistry: "https://api-test.ebsi.eu/did-registry/v5/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-test.ebsi.eu/trusted-apps-registry/v4/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-test.ebsi.eu/did-registry/v5/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-test.ebsi.eu/trusted-apps-registry/v4/apps/test-appj2'
// }
  1. The agent verifies the ake response and gets the access token
const accessToken = await Agent.verifyAkeResponse(akeAccessToken, {
  nonce,
  privateEncryptionKeyJwk,
  trustedAppsRegistry: "https://api-test.ebsi.eu/trusted-apps-registry/v4/apps",
  alg: "ES256K",
});
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-test.ebsi.eu/trusted-apps-registry/v4/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 DID Method version 2 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",
  },
);

License

Copyright (c) 2019 European Commission
Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the European Commission - subsequent versions of the EUPL (the "Licence"); You may not use this work except in compliance with the Licence. You may obtain a copy of the Licence at:

Unless required by applicable law or agreed to in writing, software distributed under the Licence is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the Licence for the specific language governing permissions and limitations under the Licence.

Readme

Keywords

none

Package Sidebar

Install

npm i @cef-ebsi/siop-auth

Weekly Downloads

1,112

Version

4.0.1

License

EUPL-1.2

Unpacked Size

111 kB

Total Files

38

Last publish

Collaborators

  • iamtxena
  • yhuard
  • joticajulian
  • zgorizzo
  • maurolucc