@microsoft/useragent-sdk
Version:
SDK for building decentralized identity wallets and enterprise agents.
212 lines • 10.4 kB
JavaScript
;
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
Object.defineProperty(exports, "__esModule", { value: true });
const Identifier_1 = require("../Identifier");
const UserAgentError_1 = require("../UserAgentError");
const ClaimObject_1 = require("../credentials/ClaimObject");
const UserAgentOptions_1 = require("../UserAgentOptions");
const HttpResolver_1 = require("../resolvers/HttpResolver");
const CryptoOptions_1 = require("../CryptoOptions");
const JwsToken_1 = require("../crypto/protocols/jose/jws/JwsToken");
const PublicKey_1 = require("../crypto/keys/PublicKey");
const node_fetch_1 = require("node-fetch");
/**
* Standard iss for SIOP Response.
*/
const iss = 'https://self-issued.me';
class OidcResponse {
/**
* 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.
*/
constructor(sender, nonce, redirectUrl, options) {
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}
*/
static async create(oidcRequest, sender) {
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.
*/
async signAndSend(keyReference, claimObjects) {
// check if sender has keystore
if (!this.sender.options || !this.sender.options.keyStore) {
throw new UserAgentError_1.default(`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();
// 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_1.default.getThumbprint(publicKey),
aud: this.redirectUrl,
nonce: this.nonce,
did: this.sender.id,
sub_jwk: publicKey,
iat,
exp
};
const claimSources = {};
const claimNames = {};
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.
*/
static async verifyAndParse(signedResponse, optionalCryptoFactory) {
// get cryptoFactory or create one
let cryptoFactory;
if (!optionalCryptoFactory) {
cryptoFactory = new CryptoOptions_1.default().cryptoFactory;
}
else {
cryptoFactory = optionalCryptoFactory;
}
// get identifier id from did.
const token = await JwsToken_1.default.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_1.default();
options.resolver = new HttpResolver_1.default('https://beta.discover.did.microsoft.com');
const sender = new Identifier_1.default(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_1.default(`Token from id: ${senderId} has expired`);
}
const claimNames = response['_claim_names'];
const claimSources = response['_claim_sources'];
let claimObjects = [];
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.
*/
static async getClaimObjects(sourceArray, sender, cryptoFactory) {
console.log(sender.id);
const claimObjects = [];
// 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 = await JwsToken_1.default.deserialize(signedClaim, { cryptoFactory });
const claimObject = await ClaimObject_1.default.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_1.default();
options.resolver = new HttpResolver_1.default('https://beta.discover.did.microsoft.com');
// 4. create new issuer Identifier.
const issuer = new Identifier_1.default(issuerId, options);
// 5. Verify that issuer signed the claimDetails.
const claimDetailsToken = await JwsToken_1.default.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
*/
async sendResponse(response, url, state) {
let responseBody = `id_token=${response}`;
if (state) {
responseBody = `${responseBody}&state=${state}`;
}
const oidcResponse = await node_fetch_1.default(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();
}
}
exports.default = OidcResponse;
//# sourceMappingURL=OidcResponse.js.map