UNPKG

@microsoft/useragent-sdk

Version:

SDK for building decentralized identity wallets and enterprise agents.

278 lines (246 loc) 10.9 kB
/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import Identifier from '../Identifier'; import UserAgentError from '../UserAgentError'; import ClaimObject from '../credentials/ClaimObject'; import OidcRequest from './OidcRequest'; import UserAgentOptions from '../UserAgentOptions'; import HttpResolver from '../resolvers/HttpResolver'; import VerifyHelper from './VerifyHelper'; import CryptoFactory from '../crypto/plugin/CryptoFactory'; import CryptoOptions from '../CryptoOptions'; import JwsToken from '../crypto/protocols/jose/jws/JwsToken'; import OIDCAuthenticationResponse from '../crypto/protocols/did/responses/OIDCAuthenticationResponse'; import PublicKey from '../crypto/keys/PublicKey'; import nodeFetch from 'node-fetch'; /** * Optional Parameters to add to OIDCRequests. */ export type OptionalOIDCResponseParams = { /** * Opaque value to represent state on serverside. */ state?: string; /** * ClaimObject if OIDC Request involves issuance of credential. */ presentedClaimObjects?: ClaimObject[]; }; /** * Standard iss for SIOP Response. */ const iss = 'https://self-issued.me'; export default class OidcResponse { /** * Redirect URL for SIOP. */ public redirectUrl: string; /** * Nonce for SIOP. */ public nonce: string; /** * Sender Identifier of the request. */ public sender: Identifier; /** * Optional State */ public state?: string; /** * claimObjects that were requested in the OIDC Request. */ public presentedClaimObjects?: ClaimObject[]; /** * Private Constructor for creating OidcResponse Object. * @param {Identifier} sender Identifier who is sending response. * @param {string} nonce Nonce from request. * @param {string} redirectUrl RedirectURL from request. * @param {OptionalOIDCResponseParams} options optional parameters for specific responses. */ private constructor(sender: Identifier, nonce: string, redirectUrl: string, options?: OptionalOIDCResponseParams) { this.redirectUrl = redirectUrl; this.nonce = nonce; this.sender = sender; this.state = options!.state; this.presentedClaimObjects = options!.presentedClaimObjects; } /** * Create an OIDC Response from an OIDC Request Object. * @param {OidcRequest} oidcRequest OIDC Request that response is responding to. * @param {Identifier} sender The Sender Identifier of the OIDC Response. * @returns {OidcResponse} */ public static async create(oidcRequest: OidcRequest, sender: Identifier) { if (!oidcRequest.state) { return new OidcResponse(sender, oidcRequest.nonce, oidcRequest.redirectUrl); } return new OidcResponse(sender, oidcRequest.nonce, oidcRequest.redirectUrl, { state: oidcRequest.state }); } /** * sign oidcRequest and send compact JWT to redirectUrl. * @param {string} keyReference reference to signing key. * @param {ClaimObject[]} claimObjects any claims that were asked for. * @returns the body of the httpResponse if status is 200. */ public async signAndSend(keyReference: string, claimObjects?: ClaimObject[]): Promise<any> { // check if sender has keystore if (!this.sender.options || !this.sender.options.keyStore) { throw new UserAgentError(`No KeyStore specified for '${this.sender.id}`); } // get public key of key referenced by keyReference in keystore const publicKey = (await this.sender.options.keyStore.get(keyReference, true)).getKey<PublicKey>(); // milliseconds to seconds const milliseconds = 1000; const expirationTimeOffsetInMinutes = 5; const expiration = new Date(Date.now() - milliseconds * 60 * expirationTimeOffsetInMinutes); // 5 minutes from now const exp = Math.floor(expiration.getTime() / milliseconds); const iat = Math.floor(Date.now() / milliseconds); // ms to seconds const response = { iss, sub: await PublicKey.getThumbprint(publicKey), aud: this.redirectUrl, nonce: this.nonce, did: this.sender.id, sub_jwk: publicKey, iat, exp }; const claimSources: {[key: string]: any} = {}; const claimNames: {[key: string]: string} = {}; let index = 1; // if state exists, add it to response. if (this.state) { Object.assign(response, {state: this.state}); } // for presentation of claims. if (claimObjects) { const claim = claimObjects[0]; const signedPresentationDetails = await this.sender.sign(claim.serialize(), keyReference); const src = `src${index}`; claimNames[claim.claimClass] = src; claimSources[src] = [{ 'JWT': signedPresentationDetails }] index++; Object.assign(response, { '_claim_names': claimNames }); Object.assign(response, { '_claim_sources': claimSources }); } const signedResponse = await this.sender.sign(response, keyReference); return this.sendResponse(signedResponse, this.redirectUrl, this.state); } /** * Parses and Verifies signed JWT. * @param {string} signedResponse signed JWT containing OIDC response. * @returns {OidcResponse} Object if verified. */ public static async verifyAndParse(signedResponse: string, optionalCryptoFactory?: CryptoFactory): Promise<OidcResponse> { // get cryptoFactory or create one let cryptoFactory: CryptoFactory; if (!optionalCryptoFactory) { cryptoFactory = new CryptoOptions().cryptoFactory; } else { cryptoFactory = optionalCryptoFactory; } // get identifier id from did. const token : JwsToken = await JwsToken.deserialize(signedResponse, {cryptoFactory}); const response = JSON.parse(token.payload.toString()); const senderId = response.did; // create new identifier and verify Auth Response const options = new UserAgentOptions(); options.resolver = new HttpResolver('https://beta.discover.did.microsoft.com'); const sender = new Identifier(senderId, options); // if (!await VerifyHelper.verify(sender, token)) { console.log('not verifying for now'); // throw new UserAgentError(`Invalid signature for token issued by: ${response.did}`); // } // check exp const milliseconds = 1000; // set a leeway of 5 minutes. const expirationCheckTimeOffsetInMinutes = 5; const expirationCheck = new Date(Date.now() - milliseconds * 60 * expirationCheckTimeOffsetInMinutes); // 5 minutes from now const expCheck = Math.floor(expirationCheck.getTime() / milliseconds); if (response.exp < expCheck) { throw new UserAgentError(`Token from id: ${senderId} has expired`); } const claimNames: {[key: string]: string} = response['_claim_names']; const claimSources: {[key: string]: {JWT: string}[]} = response['_claim_sources']; let claimObjects: ClaimObject[] = []; for (var name in claimNames) { // 1. get src name (ex. src1) const src = claimNames[name]; const srcArray = claimSources[src]; const claimObjectsFromSource = await OidcResponse.getClaimObjects(srcArray, sender, cryptoFactory); claimObjects = claimObjects.concat(claimObjectsFromSource); } if (claimObjects !== []) { return new OidcResponse(sender, response.nonce, response.redirectUrl, { state: response.state, presentedClaimObjects: claimObjects }); } return new OidcResponse(sender, response.nonce, response.redirectUrl, { state: response.state }); } /** * Helper method to parse for ClaimObjects in OIDC Response. * @param sourceArray source array that contain claims * @param {Identifier} sender Identifer that represents entity that sent request. * @param {CryptoFactory} cryptoFactory cryptoFactory used for crypto operations. */ private static async getClaimObjects(sourceArray: {JWT: string}[], sender: Identifier, cryptoFactory: CryptoFactory): Promise<ClaimObject[]> { console.log(sender.id); const claimObjects: ClaimObject[] = []; // Assuming only asking for one claim for now. const jwtBinding = sourceArray[0]; // 1. JWT is signed by entity that owns claim (same as entity that sent auth response). const signedClaim = jwtBinding.JWT; const claimToken : JwsToken = await JwsToken.deserialize(signedClaim, {cryptoFactory}); const claimObject = await ClaimObject.deserialize(claimToken.payload.toString()); // 2. Verify signature. // if (!await VerifyHelper.verify(sender, claimToken)) { console.log('not verifying for now'); // throw new UserAgentError(`Presented Claim: ${claimObject.claimClass} not verified`); // } // 3. get issuer id from claimObject. const issuerId = claimObject.issuer; const options = new UserAgentOptions(); options.resolver = new HttpResolver('https://beta.discover.did.microsoft.com'); // 4. create new issuer Identifier. const issuer = new Identifier(issuerId, options); // 5. Verify that issuer signed the claimDetails. const claimDetailsToken : JwsToken = await JwsToken.deserialize(signedClaim, {cryptoFactory}); // if (!await VerifyHelper.verify(issuer, claimDetailsToken)) { // console.log('not verifying for now'); // throw new UserAgentError(`Presented Claim: ${claimObject.claimClass} not verified`); // } // 6. Add claimObject to array of presentedClaims. claimObjects.push(claimObject); return claimObjects; } /** * Send response to redirect url and * * @param {string} response OIDC Response * @param {string} url url to send the response to. * @param {string} state optional state parameter */ private async sendResponse(response: string, url: string, state?: string) { let responseBody = `id_token=${response}`; if (state) { responseBody = `${responseBody}&state=${state}`; } const oidcResponse = await nodeFetch(url, { method: 'POST', body: responseBody, headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Content-Length': responseBody.length.toString() }, }); if (oidcResponse.status !== 200) { throw new Error(`OpenID Connect response failed to send: ${oidcResponse.text()}`); } return oidcResponse.json(); } }