UNPKG

@web5/agent

Version:
858 lines (748 loc) 29.9 kB
import { Convert, logger, RequireOnly } from '@web5/common'; import { Ed25519, EdDsaAlgorithm, JoseHeaderParams, Jwk, Sha256, X25519, CryptoUtils, } from '@web5/crypto'; import { concatenateUrl } from './utils.js'; import { xchacha20poly1305 } from '@noble/ciphers/chacha'; import type { ConnectPermissionRequest } from './connect.js'; import { DidDocument, DidJwk, PortableDid, type BearerDid } from '@web5/dids'; import { DwnDataEncodedRecordsWriteMessage, DwnInterface, DwnPermissionScope, DwnProtocolDefinition } from './types/dwn.js'; import { AgentPermissionsApi } from './permissions-api.js'; import type { Web5Agent } from './types/agent.js'; import { isRecordPermissionScope } from './dwn-api.js'; import { DwnInterfaceName, DwnMethodName } from '@tbd54566975/dwn-sdk-js'; /** * Sent to an OIDC server to authorize a client. Allows clients * to securely send authorization request parameters directly to * the server via POST. This avoids exposing sensitive data in URLs * and ensures the server validates the request before user interaction. * * @see {@link https://www.rfc-editor.org/rfc/rfc9126.html | OAuth 2.0 Pushed Authorization Requests} */ export type PushedAuthRequest = { /** The JWT which contains the {@link Web5ConnectAuthRequest} */ request: string; }; /** * Sent back by OIDC server in response to {@link PushedAuthRequest} * The server generates a TTL and a unique request_uri. The request_uri can be shared * with the Provider using a link or a QR code along with additional params * to access the url and decrypt the payload. */ export type PushedAuthResponse = { request_uri: string; expires_in: number; }; /** * Used in decentralized apps. The SIOPv2 Auth Request is created by a client relying party (RP) * often a web service or an app who wants to obtain information from a provider * The contents of this are inserted into a JWT inside of the {@link PushedAuthRequest}. * @see {@link https://github.com/TBD54566975/known-customer-credential | TBD OIDC Documentation for SIOPv2 } */ export type SIOPv2AuthRequest = { /** The DID of the client (RP) */ client_id: string; /** The scope of the access request (e.g., `openid profile`). */ scope: string; /** The type of response desired (e.g. `id_token`) */ response_type: string; /** the URL to which the Identity Provider will post the Authorization Response */ redirect_uri: string; /** The URI to which the SIOPv2 Authorization Response will be sent (Tim's note: not used with encrypted request JWT)*/ response_uri?: string; /** * An opaque value used to maintain state between the request and the callback. * Recommended for security to prevent CSRF attacks. */ state: string; /** * A string value used to associate a client session with an ID token to mitigate replay attacks. * Recommended when requesting ID tokens. */ nonce: string; /** * The PKCE code challenge. * Required if `code_challenge_method` is used. Enhances security for public clients (e.g., single-page apps, * mobile apps) by requiring an additional verification step during token exchange. */ code_challenge?: string; /** The method used for the PKCE challenge (typically `S256`). Must be present if `code_challenge` is included. */ code_challenge_method?: 'S256'; /** * An ID token previously issued to the client, passed as a hint about the end-user’s current or past authenticated * session with the client. Can streamline user experience if already logged in. */ id_token_hint?: string; /** A hint to the authorization server about the login identifier the user might use. Useful for pre-filling login information. */ login_hint?: string; /** Requested Authentication Context Class Reference values. Specifies the authentication context requirements. */ acr_values?: string; /** When using a PAR for secure cross device flows we use a "form_post" rather than a "direct_post" */ response_mode: 'direct_post'; /** Used by PFI to request VCs as input to IDV process. If present, `response_type: "vp_token""` MUST also be present */ presentation_definition?: any; /** A JSON object containing the Verifier metadata values (Tim's note: from TBD KCC Repo) */ client_metadata?: { /** Array of strings, each a DID method supported for the subject of ID Token */ subject_syntax_types_supported: string[]; /** Human-readable string name of the client to be presented to the end-user during authorization */ client_name?: string; /** URI of a web page providing information about the client */ client_uri?: string; /** URI of an image logo for the client */ logo_uri?: string; /** Array of strings representing ways to contact people responsible for this client, typically email addresses */ contacts?: string[]; /** URI that points to a terms of service document for the client */ tos_uri?: string; /** URI that points to a privacy policy document */ policy_uri?: string; }; }; /** * An auth request that is compatible with both Web5 Connect and (hopefully, WIP) OIDC SIOPv2 * The contents of this are inserted into a JWT inside of the {@link PushedAuthRequest}. */ export type Web5ConnectAuthRequest = { /** The user friendly name of the client/app to be displayed when prompting end-user with permission requests. */ displayName: string; /** PermissionGrants that are to be sent to the provider */ permissionRequests: ConnectPermissionRequest[]; } & SIOPv2AuthRequest; /** The fields for an OIDC SIOPv2 Auth Repsonse */ export type SIOPv2AuthResponse = { /** Issuer MUST match the value of sub (Applicant's DID) */ iss: string; /** Subject Identifier. A locally unique and never reassigned identifier * within the Issuer for the End-User, which is intended to be consumed * by the Client. */ sub: string; /** Audience(s) that this ID Token is intended for. It MUST contain the * OAuth 2.0 client_id of the Relying Party as an audience value. */ aud: string; /** Time at which the JWT was issued. */ iat: number; /** Expiration time on or after which the ID Token MUST NOT be accepted * for processing. */ exp: number; /** Time when the End-User authentication occurred. */ auth_time?: number; /** b64url encoded nonce used to associate a Client session with an ID Token, and to * mitigate replay attacks. */ nonce?: string; /** Custom claims. */ [key: string]: any; }; /** An auth response that is compatible with both Web5 Connect and (hopefully, WIP) OIDC SIOPv2 */ export type Web5ConnectAuthResponse = { delegateGrants: DwnDataEncodedRecordsWriteMessage[]; delegatePortableDid: PortableDid; } & SIOPv2AuthResponse; /** Represents the different OIDC endpoint types. * 1. `pushedAuthorizationRequest`: client sends {@link PushedAuthRequest} receives {@link PushedAuthResponse} * 2. `authorize`: provider gets the {@link Web5ConnectAuthRequest} JWT that was stored by the PAR * 3. `callback`: provider sends {@link Web5ConnectAuthResponse} to this endpoint * 4. `token`: client gets {@link Web5ConnectAuthResponse} from this endpoint */ type OidcEndpoint = | 'pushedAuthorizationRequest' | 'authorize' | 'callback' | 'token'; /** * Gets the correct OIDC endpoint out of the {@link OidcEndpoint} options provided. * Handles a trailing slash on baseURL * * @param {Object} options the options object * @param {string} options.baseURL for example `http://foo.com/connect/ * @param {OidcEndpoint} options.endpoint the OIDC endpoint desired * @param {string} options.authParam this is the unique id which must be provided when getting the `authorize` endpoint * @param {string} options.tokenParam this is the random state as b64url which must be provided with the `token` endpoint */ function buildOidcUrl({ baseURL, endpoint, authParam, tokenParam, }: { baseURL: string; endpoint: OidcEndpoint; authParam?: string; tokenParam?: string; }) { switch (endpoint) { /** 1. client sends {@link PushedAuthRequest} & client receives {@link PushedAuthResponse} */ case 'pushedAuthorizationRequest': return concatenateUrl(baseURL, 'par'); /** 2. provider gets {@link Web5ConnectAuthRequest} */ case 'authorize': if (!authParam) throw new Error( `authParam must be providied when building a token URL` ); return concatenateUrl(baseURL, `authorize/${authParam}.jwt`); /** 3. provider sends {@link Web5ConnectAuthResponse} */ case 'callback': return concatenateUrl(baseURL, `callback`); /** 4. client gets {@link Web5ConnectAuthResponse */ case 'token': if (!tokenParam) throw new Error( `tokenParam must be providied when building a token URL` ); return concatenateUrl(baseURL, `token/${tokenParam}.jwt`); // TODO: metadata endpoints? default: throw new Error(`No matches for endpoint specified: ${endpoint}`); } } /** * Generates a cryptographically random "code challenge" in * accordance with the RFC 7636 PKCE specification. * * @see {@link https://datatracker.ietf.org/doc/html/rfc7636#section-4.2 | RFC 7636 } */ async function generateCodeChallenge() { const codeVerifierBytes = CryptoUtils.randomBytes(32); const codeChallengeBytes = await Sha256.digest({ data: codeVerifierBytes }); const codeChallengeBase64Url = Convert.uint8Array(codeChallengeBytes).toBase64Url(); return { codeChallengeBytes, codeChallengeBase64Url }; } /** Client creates the {@link Web5ConnectAuthRequest} */ async function createAuthRequest( options: RequireOnly< Web5ConnectAuthRequest, 'client_id' | 'scope' | 'redirect_uri' | 'permissionRequests' | 'displayName' > ) { // Generate a random state value to associate the authorization request with the response. const stateBytes = CryptoUtils.randomBytes(16); // Generate a random nonce value to associate the ID Token with the authorization request. const nonceBytes = CryptoUtils.randomBytes(16); const requestObject: Web5ConnectAuthRequest = { ...options, nonce : Convert.uint8Array(nonceBytes).toBase64Url(), response_type : 'id_token', response_mode : 'direct_post', state : Convert.uint8Array(stateBytes).toBase64Url(), client_metadata : { subject_syntax_types_supported: ['did:dht', 'did:jwk'], }, }; return requestObject; } /** Encrypts the auth request with the key which will be passed through QR code */ async function encryptAuthRequest({ jwt, encryptionKey, }: { jwt: string; encryptionKey: Uint8Array; }) { const protectedHeader = { alg : 'dir', cty : 'JWT', enc : 'XC20P', typ : 'JWT', }; const nonce = CryptoUtils.randomBytes(24); const additionalData = Convert.object(protectedHeader).toUint8Array(); const jwtBytes = Convert.string(jwt).toUint8Array(); const chacha = xchacha20poly1305(encryptionKey, nonce, additionalData); const ciphertextAndTag = chacha.encrypt(jwtBytes); /** The cipher output concatenates the encrypted data and tag * so we need to extract the values for use in the JWE. */ const ciphertext = ciphertextAndTag.subarray(0, -16); const authenticationTag = ciphertextAndTag.subarray(-16); const compactJwe = [ Convert.object(protectedHeader).toBase64Url(), '', // Empty string since there is no wrapped key. Convert.uint8Array(nonce).toBase64Url(), Convert.uint8Array(ciphertext).toBase64Url(), Convert.uint8Array(authenticationTag).toBase64Url(), ].join('.'); return compactJwe; } /** Create a response object compatible with Web5 Connect and OIDC SIOPv2 */ async function createResponseObject( options: RequireOnly< Web5ConnectAuthResponse, 'iss' | 'sub' | 'aud' | 'delegateGrants' | 'delegatePortableDid' > ) { const currentTimeInSeconds = Math.floor(Date.now() / 1000); const responseObject: Web5ConnectAuthResponse = { ...options, iat : currentTimeInSeconds, exp : currentTimeInSeconds + 600, // Expires in 10 minutes. }; return responseObject; } /** sign an object and transform it into a jwt using a did */ async function signJwt({ did, data, }: { did: BearerDid; data: Record<string, unknown>; }) { const header = Convert.object({ alg : 'EdDSA', kid : did.document.verificationMethod![0].id, typ : 'JWT', }).toBase64Url(); const payload = Convert.object(data).toBase64Url(); // signs using ed25519 EdDSA const signer = await did.getSigner(); const signature = await signer.sign({ data: Convert.string(`${header}.${payload}`).toUint8Array(), }); const signatureBase64Url = Convert.uint8Array(signature).toBase64Url(); const jwt = `${header}.${payload}.${signatureBase64Url}`; return jwt; } /** Take the decrypted JWT and verify it was signed by its public DID. Return parsed object. */ async function verifyJwt({ jwt }: { jwt: string }) { const [headerB64U, payloadB64U, signatureB64U] = jwt.split('.'); // Convert the header back to a JOSE object and verify that the 'kid' header value is present. const header: JoseHeaderParams = Convert.base64Url(headerB64U).toObject(); if (!header.kid) throw new Error( `OIDC: Object could not be verified due to missing 'kid' header value.` ); // Resolve the Client DID document. const { didDocument } = await DidJwk.resolve(header.kid.split('#')[0]); if (!didDocument) throw new Error( 'OIDC: Object could not be verified due to Client DID resolution issue.' ); // Get the public key used to sign the Object from the DID document. const { publicKeyJwk } = didDocument.verificationMethod?.find((method: any) => { return method.id === header.kid; }) ?? {}; if (!publicKeyJwk) throw new Error( 'OIDC: Object could not be verified due to missing public key in DID document.' ); const EdDsa = new EdDsaAlgorithm(); const isValid = await EdDsa.verify({ key : publicKeyJwk, signature : Convert.base64Url(signatureB64U).toUint8Array(), data : Convert.string(`${headerB64U}.${payloadB64U}`).toUint8Array(), }); if (!isValid) throw new Error( 'OIDC: Object failed verification due to invalid signature.' ); const object = Convert.base64Url(payloadB64U).toObject(); return object; } /** * Fetches the {@Web5ConnectAuthRequest} from the authorize endpoint and decrypts it * using the encryption key passed via QR code. */ const getAuthRequest = async (request_uri: string, encryption_key: string) => { const authRequest = await fetch(request_uri); const jwe = await authRequest.text(); const jwt = decryptAuthRequest({ jwe, encryption_key, }); const web5ConnectAuthRequest = (await verifyJwt({ jwt, })) as Web5ConnectAuthRequest; return web5ConnectAuthRequest; }; /** Take the encrypted JWE, decrypt using the code challenge and return a JWT string which will need to be verified */ function decryptAuthRequest({ jwe, encryption_key, }: { jwe: string; encryption_key: string; }) { const [ protectedHeaderB64U, , nonceB64U, ciphertextB64U, authenticationTagB64U, ] = jwe.split('.'); const encryptionKeyBytes = Convert.base64Url(encryption_key).toUint8Array(); const protectedHeader = Convert.base64Url(protectedHeaderB64U).toUint8Array(); const additionalData = protectedHeader; const nonce = Convert.base64Url(nonceB64U).toUint8Array(); const ciphertext = Convert.base64Url(ciphertextB64U).toUint8Array(); const authenticationTag = Convert.base64Url( authenticationTagB64U ).toUint8Array(); // The cipher expects the encrypted data and tag to be concatenated. const ciphertextAndTag = new Uint8Array([ ...ciphertext, ...authenticationTag, ]); const chacha = xchacha20poly1305(encryptionKeyBytes, nonce, additionalData); const decryptedJwtBytes = chacha.decrypt(ciphertextAndTag); const jwt = Convert.uint8Array(decryptedJwtBytes).toString(); return jwt; } /** * The client uses to decrypt the jwe obtained from the auth server which contains * the {@link Web5ConnectAuthResponse} that was sent by the provider to the auth server. * * @async * @param {BearerDid} clientDid - The did that was initially used by the client for ECDH at connect init. * @param {string} jwe - The encrypted data as a jwe. * @param {string} pin - The pin that was obtained from the user. */ async function decryptAuthResponse( clientDid: BearerDid, jwe: string, pin: string ) { const [ protectedHeaderB64U, , nonceB64U, ciphertextB64U, authenticationTagB64U, ] = jwe.split('.'); // get the delegatedid public key from the header const header = Convert.base64Url(protectedHeaderB64U).toObject() as Jwk; const delegateResolvedDid = await DidJwk.resolve(header.kid!.split('#')[0]); // derive ECDH shared key using the provider's public key and our clientDid private key const sharedKey = await Oidc.deriveSharedKey( clientDid, delegateResolvedDid.didDocument! ); // add the pin to the AAD const additionalData = { ...header, pin: pin }; const AAD = Convert.object(additionalData).toUint8Array(); const nonce = Convert.base64Url(nonceB64U).toUint8Array(); const ciphertext = Convert.base64Url(ciphertextB64U).toUint8Array(); const authenticationTag = Convert.base64Url( authenticationTagB64U ).toUint8Array(); // The cipher expects the encrypted data and tag to be concatenated. const ciphertextAndTag = new Uint8Array([ ...ciphertext, ...authenticationTag, ]); // decrypt using the sharedKey const chacha = xchacha20poly1305(sharedKey, nonce, AAD); const decryptedJwtBytes = chacha.decrypt(ciphertextAndTag); const jwt = Convert.uint8Array(decryptedJwtBytes).toString(); return jwt; } /** Derives a shared ECDH private key in order to encrypt the {@link Web5ConnectAuthResponse} */ async function deriveSharedKey( privateKeyDid: BearerDid, publicKeyDid: DidDocument ) { const privatePortableDid = await privateKeyDid.export(); const publicJwk = publicKeyDid.verificationMethod?.[0].publicKeyJwk!; const privateJwk = privatePortableDid.privateKeys?.[0]!; publicJwk.alg = 'EdDSA'; const publicX25519 = await Ed25519.convertPublicKeyToX25519({ publicKey: publicJwk, }); const privateX25519 = await Ed25519.convertPrivateKeyToX25519({ privateKey: privateJwk, }); const sharedKey = await X25519.sharedSecret({ privateKeyA : privateX25519, publicKeyB : publicX25519, }); const derivedKey = await crypto.subtle.importKey( 'raw', sharedKey, { name: 'HKDF' }, false, ['deriveBits'] ); const derivedKeyBits = await crypto.subtle.deriveBits( { name : 'HKDF', hash : 'SHA-256', info : new Uint8Array(), salt : new Uint8Array(), }, derivedKey, 256 ); const sharedEncryptionKey = new Uint8Array(derivedKeyBits); return sharedEncryptionKey; } /** * Encrypts the auth response jwt. Requires a randomPin is added to the AAD of the * encryption algorithm in order to prevent man in the middle and eavesdropping attacks. * The keyid of the delegate did is used to pass the public key to the client in order * for the client to derive the shared ECDH private key. */ function encryptAuthResponse({ jwt, encryptionKey, delegateDidKeyId, randomPin, }: { jwt: string; encryptionKey: Uint8Array; delegateDidKeyId: string; randomPin: string; }) { const protectedHeader = { alg : 'dir', cty : 'JWT', enc : 'XC20P', typ : 'JWT', kid : delegateDidKeyId, }; const nonce = CryptoUtils.randomBytes(24); const additionalData = Convert.object({ ...protectedHeader, pin: randomPin, }).toUint8Array(); const jwtBytes = Convert.string(jwt).toUint8Array(); const chacha = xchacha20poly1305(encryptionKey, nonce, additionalData); const ciphertextAndTag = chacha.encrypt(jwtBytes); /** The cipher output concatenates the encrypted data and tag * so we need to extract the values for use in the JWE. */ const ciphertext = ciphertextAndTag.subarray(0, -16); const authenticationTag = ciphertextAndTag.subarray(-16); const compactJwe = [ Convert.object(protectedHeader).toBase64Url(), '', // Empty string since there is no wrapped key. Convert.uint8Array(nonce).toBase64Url(), Convert.uint8Array(ciphertext).toBase64Url(), Convert.uint8Array(authenticationTag).toBase64Url(), ].join('.'); return compactJwe; } function shouldUseDelegatePermission(scope: DwnPermissionScope): boolean { // Currently all record permissions are treated as delegated permissions // In the future only methods that modify state will be delegated and the rest will be normal permissions if (isRecordPermissionScope(scope)) { return true; } else if (scope.interface === DwnInterfaceName.Protocols && scope.method === DwnMethodName.Configure) { // ProtocolConfigure messages are also delegated, as they modify state return true; } // All other permissions are not treated as delegated return false; } /** * Creates the permission grants that assign to the selectedDid the level of * permissions that the web app requested in the {@link Web5ConnectAuthRequest} */ async function createPermissionGrants( selectedDid: string, delegateBearerDid: BearerDid, agent: Web5Agent, scopes: DwnPermissionScope[], ) { const permissionsApi = new AgentPermissionsApi({ agent }); // TODO: cleanup all grants if one fails by deleting them from the DWN: https://github.com/TBD54566975/web5-js/issues/849 logger.log(`Creating permission grants for ${scopes.length} scopes given...`); const permissionGrants = await Promise.all( scopes.map((scope) => { // check if the scope is a records permission scope, or a protocol configure scope, if so it should use a delegated permission. const delegated = shouldUseDelegatePermission(scope); return permissionsApi.createGrant({ delegated, store : true, grantedTo : delegateBearerDid.uri, scope, dateExpires : '2040-06-25T16:09:16.693356Z', // TODO: make dateExpires optional author : selectedDid, }); }) ); logger.log(`Sending ${permissionGrants.length} permission grants to remote DWN...`); const messagePromises = permissionGrants.map(async (grant) => { // Quirk: we have to pull out encodedData out of the message the schema validator doesn't want it there const { encodedData, ...rawMessage } = grant.message; const data = Convert.base64Url(encodedData).toUint8Array(); const { reply } = await agent.sendDwnRequest({ author : selectedDid, target : selectedDid, messageType : DwnInterface.RecordsWrite, dataStream : new Blob([data]), rawMessage, }); // check if the message was sent successfully, if the remote returns 409 the message may have come through already via sync if (reply.status.code !== 202 && reply.status.code !== 409) { logger.error(`Error sending RecordsWrite: ${reply.status.detail}`); logger.error(`RecordsWrite message: ${rawMessage}`); throw new Error( `Could not send the message. Error details: ${reply.status.detail}` ); } return grant.message; }); try { const messages = await Promise.all(messagePromises); return messages; } catch (error) { logger.error(`Error during batch-send of permission grants: ${error}`); throw error; } } /** * Installs the protocol required by the Client on the Provider if it doesn't already exist. */ async function prepareProtocol( selectedDid: string, agent: Web5Agent, protocolDefinition: DwnProtocolDefinition ): Promise<void> { const queryMessage = await agent.processDwnRequest({ author : selectedDid, messageType : DwnInterface.ProtocolsQuery, target : selectedDid, messageParams : { filter: { protocol: protocolDefinition.protocol } }, }); if ( queryMessage.reply.status.code !== 200) { // if the query failed, throw an error throw new Error( `Could not fetch protocol: ${queryMessage.reply.status.detail}` ); } else if (queryMessage.reply.entries === undefined || queryMessage.reply.entries.length === 0) { logger.log(`Protocol does not exist, creating: ${protocolDefinition.protocol}`); // send the protocol definition to the remote DWN first, if it passes we can process it locally const { reply: sendReply, message: configureMessage } = await agent.sendDwnRequest({ author : selectedDid, target : selectedDid, messageType : DwnInterface.ProtocolsConfigure, messageParams : { definition: protocolDefinition }, }); // check if the message was sent successfully, if the remote returns 409 the message may have come through already via sync if (sendReply.status.code !== 202 && sendReply.status.code !== 409) { throw new Error(`Could not send protocol: ${sendReply.status.detail}`); } // process the protocol locally, we don't have to check if it exists as this is just a convenience over waiting for sync. await agent.processDwnRequest({ author : selectedDid, target : selectedDid, messageType : DwnInterface.ProtocolsConfigure, rawMessage : configureMessage }); } else { logger.log(`Protocol already exists: ${protocolDefinition.protocol}`); // the protocol already exists, let's make sure it exists on the remote DWN as the requesting app will need it const configureMessage = queryMessage.reply.entries![0]; const { reply: sendReply } = await agent.sendDwnRequest({ author : selectedDid, target : selectedDid, messageType : DwnInterface.ProtocolsConfigure, rawMessage : configureMessage, }); if (sendReply.status.code !== 202 && sendReply.status.code !== 409) { throw new Error(`Could not send protocol: ${sendReply.status.detail}`); } } } /** * Creates a delegate did which the web app will use as its future indentity. * Assigns to that DID the level of permissions that the web app requested in * the {@link Web5ConnectAuthRequest}. Encrypts via ECDH key that the web app * will have access to because the web app has the public key which it provided * in the {@link Web5ConnectAuthRequest}. Then sends the ciphertext of this * {@link Web5ConnectAuthResponse} to the callback endpoint. Which the * web app will need to retrieve from the token endpoint and decrypt with the pin to access. */ async function submitAuthResponse( selectedDid: string, authRequest: Web5ConnectAuthRequest, randomPin: string, agent: Web5Agent ) { const delegateBearerDid = await DidJwk.create(); const delegatePortableDid = await delegateBearerDid.export(); // TODO: roll back permissions and protocol configurations if an error occurs. Need a way to delete protocols to achieve this. const delegateGrantPromises = authRequest.permissionRequests.map( async (permissionRequest) => { const { protocolDefinition, permissionScopes } = permissionRequest; // We validate that all permission scopes match the protocol uri of the protocol definition they are provided with. const grantsMatchProtocolUri = permissionScopes.every(scope => 'protocol' in scope && scope.protocol === protocolDefinition.protocol); if (!grantsMatchProtocolUri) { throw new Error('All permission scopes must match the protocol uri they are provided with.'); } await prepareProtocol(selectedDid, agent, protocolDefinition); const permissionGrants = await Oidc.createPermissionGrants( selectedDid, delegateBearerDid, agent, permissionScopes ); return permissionGrants; } ); const delegateGrants = (await Promise.all(delegateGrantPromises)).flat(); logger.log('Generating auth response object...'); const responseObject = await Oidc.createResponseObject({ //* the IDP's did that was selected to be connected iss : selectedDid, //* the client's new identity sub : delegateBearerDid.uri, //* the client's temporary ephemeral did used for connect aud : authRequest.client_id, //* the nonce of the original auth request nonce : authRequest.nonce, delegateGrants, delegatePortableDid, }); // Sign the Response Object using the ephemeral DID's signing key. logger.log('Signing auth response object...'); const responseObjectJwt = await Oidc.signJwt({ did : delegateBearerDid, data : responseObject, }); const clientDid = await DidJwk.resolve(authRequest.client_id); const sharedKey = await Oidc.deriveSharedKey( delegateBearerDid, clientDid?.didDocument! ); logger.log('Encrypting auth response object...'); const encryptedResponse = Oidc.encryptAuthResponse({ jwt : responseObjectJwt!, encryptionKey : sharedKey, delegateDidKeyId : delegateBearerDid.document.verificationMethod![0].id, randomPin, }); const formEncodedRequest = new URLSearchParams({ id_token : encryptedResponse, state : authRequest.state, }).toString(); logger.log(`Sending auth response object to Web5 Connect server: ${authRequest.redirect_uri}`); await fetch(authRequest.redirect_uri, { body : formEncodedRequest, method : 'POST', headers : { 'Content-Type': 'application/x-www-form-urlencoded', }, }); } export const Oidc = { createAuthRequest, encryptAuthRequest, getAuthRequest, decryptAuthRequest, createPermissionGrants, createResponseObject, encryptAuthResponse, decryptAuthResponse, deriveSharedKey, signJwt, verifyJwt, buildOidcUrl, generateCodeChallenge, submitAuthResponse, };