@microsoft/useragent-sdk
Version:
SDK for building decentralized identity wallets and enterprise agents.
278 lines (246 loc) • 10.9 kB
text/typescript
/*---------------------------------------------------------------------------------------------
* 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();
}
}