@microsoft/useragent-sdk
Version:
SDK for building decentralized identity wallets and enterprise agents.
318 lines • 15.1 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 JwsToken_1 = require("../crypto/protocols/jose/jws/JwsToken");
const UserAgentError_1 = require("../UserAgentError");
const PublicKey_1 = require("../crypto/keys/PublicKey");
const node_fetch_1 = require("node-fetch");
const url_1 = require("url");
const IPermissionGrant_1 = require("../hubSession/objects/IPermissionGrant");
const Permissions_1 = require("../hubInterfaces/Permissions");
const HubInterface_1 = require("../hubInterfaces/HubInterface");
const __1 = require("..");
/**
* Class for creating a User Agent Session for sending and verifying
* Authentication Requests and Responses.
*/
class UserAgentSession {
constructor(sender, keyReference, resolver) {
this.sender = sender;
this.resolver = resolver;
this.keyReference = keyReference;
this.options = sender.options;
this.cryptoFactory = this.options.cryptoFactory;
}
/**
* Sign a User Agent Request.
* @param redirectUrl url that recipient should send response back to.
* @param nonce nonce that will come back in response.
* @param options Open ID Connect optional parameters
*/
async signRequest(redirectUrl, nonce, options) {
const request = {
iss: this.sender.id,
response_type: 'id_token',
response_mode: 'form_post',
client_id: redirectUrl,
scope: 'openid did_authn',
nonce,
registration: { id_token_signed_response_alg: ['EdDSA'] }
};
if (options) {
if (options.scopes) {
const additionalScopes = options.scopes.map((scope) => Buffer.from(JSON.stringify(scope)).toString('base64')).join(' ');
request.scope = `openid did_authn ${additionalScopes}`;
}
if (options.state) {
Object.assign(request, { state: options.state });
}
if (options.claimRequests) {
// throw new Error('UserAgent does not currently support open ID Connect claims');
Object.assign(request, { claims: { id_token: options.claimRequests } });
}
/**
* adds manifest to registration parameter (backwards compatible for Identiverse demo).
* else adds the id_token_signed_response_alg parameter.
*/
if (options.manifest) {
Object.assign(request, { registration: JSON.stringify(options.manifest) });
}
}
return this.sender.sign(request, this.keyReference);
}
/**
* Send an Open ID Connect User Agent Response.
* @param request Open ID Connect request to respond to.
* @param claims Future Feature: any claims the request asked for.
* @param grants any permissionGrants approved by the user.
* @throws Throws if claims are included
*/
async sendResponse(request, grants, claims) {
// 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(this.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
// create OIDC response object
let response = {
iss: 'https://self-issued.me',
sub: await PublicKey_1.default.getThumbprint(publicKey),
aud: request.client_id,
nonce: request.nonce,
did_comm: {
did: this.sender.id
},
sub_jwk: publicKey,
iat,
exp
};
// add requested claims to response
if (claims) {
response = Object.assign(response, claims);
}
// response = Object.assign(response, claims);
let receipt = {
credentials: [],
permissionGrants: []
};
if (grants) {
// Send hub permission objects
if (!this.permissions) {
this.permissions = new Permissions_1.default({
hubOwner: this.sender,
clientIdentifier: this.sender,
recipientsPublicKeys: undefined,
context: IPermissionGrant_1.PERMISSION_GRANT_CONTEXT,
type: IPermissionGrant_1.PERMISSION_GRANT_TYPE,
hubInterface: HubInterface_1.HubInterfaceType.Permissions,
commitStrategy: HubInterface_1.CommitStrategyType.Basic,
cryptoOptions: new __1.CryptoOptions(),
hubProtectionStrategy: undefined
});
}
for (let i = 0; i < grants.length; i++) {
const permissionBundle = grants[i];
const permissionReceipt = {
owner: this.sender.id,
grantee: request.iss,
definition: {
name: permissionBundle.name,
description: permissionBundle.description,
iconUrl: permissionBundle.iconUrl
},
objects: [],
};
for (let j = 0; j < permissionBundle.grants.length; j++) {
let grant = permissionBundle.grants[j];
grant.owner = this.sender.id;
grant.grantee = request.iss;
const grantWriteResponse = await this.permissions.addObject(grants[i]);
permissionReceipt.objects.push(grantWriteResponse.getRevisions()[0]);
}
receipt.permissionGrants.push(permissionReceipt);
}
}
// sign the response
const jws = await this.sender.sign(response, this.keyReference);
// create response body string
let responseBody = `id_token=${jws}`;
if (request.state) {
responseBody = `${responseBody}&state=${request.state}`;
}
const oidcResponse = await node_fetch_1.default(request.client_id, {
method: 'POST',
body: responseBody,
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Content-Length': responseBody.length.toString()
},
});
if (oidcResponse.status !== 204) {
throw new Error(`OpenID Connect response failed to send: ${oidcResponse.text()}`);
}
if (!claims && !grants) {
return oidcResponse;
}
return receipt;
}
/**
* Verify a request was signed and sent by Identifier.
* @param jws Signed Payload
*/
async verify(jws) {
// get identifier id from key id in header.
const token = await JwsToken_1.default.deserialize(jws, { cryptoFactory: this.cryptoFactory });
const payload = JSON.parse(token.payload.toString());
/**
* If iss parameter is 'https://selfissued.me', payload is an OIDC auth request
* and did is in the did parameter. Else the payload is a OIDC auth response,
* so the did is in the iss parameter. If either parameter is undefined,
* the payload is not formatted properly, so throw an error.
*/
const issuerIdentifier = payload.iss === 'https://self-issued.me' ? payload.did_comm.did : payload.iss;
if (!issuerIdentifier) {
throw new UserAgentError_1.default('Unable to identify issuer of the token.');
}
// verify jws and return payload.
const identifier = new Identifier_1.default(issuerIdentifier, this.options);
const document = await identifier.getDocument();
const publicKeysFromDocument = document.getPublicKeysFromDocument();
if (token.signatures.length < 0) {
throw new UserAgentError_1.default('No signature included');
}
let keyMatches = null;
const keyIdRegex = /([^#]*)#?(.+$)/;
if (token.signatures[0].protected && (token.signatures[0].protected).has('kid')) {
const keyIdentifier = (token.signatures[0].protected).get('kid');
keyMatches = keyIdentifier.match(keyIdRegex);
}
else if (token.signatures[0].header && (token.signatures[0].header).has('kid')) {
const keyIdentifier = (token.signatures[0].header).get('kid');
keyMatches = keyIdentifier.match(keyIdRegex);
}
if (keyMatches === null) {
throw new UserAgentError_1.default('Cannot locate keyID');
}
if (keyMatches[1].length > 0 && keyMatches[1] !== identifier.id) {
throw new UserAgentError_1.default('Issuer signer does not match issuer');
}
const keyId = keyMatches[2];
const matchingPublicKeys = publicKeysFromDocument.filter((publicKey) => {
return publicKey.kid && publicKey.kid.endsWith(keyId);
});
if (!await token.verify(matchingPublicKeys)) {
throw new UserAgentError_1.default('Invalid signature');
}
return payload;
}
/**
* Verify an Open ID Connect response was signed and sent by response.did.
* @param responseJws Signed response token
*/
async verifyResponse(responseJws) {
return this.verify(responseJws);
}
/**
* Verify an Open ID Connect request was signed and sent by Identifier, and return all contents
* @param request Signed request token
*/
async verifyAndHydrateRequest(requestJws) {
const request = await this.verify(requestJws);
const prompt = Object.assign({}, request);
// if there are credentials requested, include them.
if (request.claims && 'credential' in request.claims.id_token) {
prompt.credentialsRequested = Object.keys(request.claims.id_token.credential);
}
// if there are scopes requested include them.
let scopes = request.scope.split(' ');
if (scopes.length > 2) {
scopes = scopes.filter((scope) => scope !== 'openid' && scope !== 'did_authn');
prompt.identityHubPermissionsRequested = [];
// Not forEach to allow proper async await
for (let i = 0; i < scopes.length; i++) {
prompt.identityHubPermissionsRequested.push(...(await this.parseScopeValue(request.iss, scopes[i])));
}
}
// if there are credentials minted, include them.
// const MICROSOFT_BLUE = '#0078d4';
// if there is a registration, pull requester information
if (request.registration) {
let manifest;
if (typeof (request.registration) === 'string') {
manifest = JSON.parse(request.registration);
if (manifest.client_name) {
prompt.name = manifest.client_name;
prompt.logoUrl = manifest.logo_uri;
prompt.dataUsePolicy = manifest.policy_uri;
prompt.homepage = manifest.client_uri;
prompt.termsOfService = manifest.tos_uri;
}
}
else {
manifest = request.registration;
}
}
// any additional scraping we can do from the request metadata itself
if (request.client_id.startsWith('http')) {
// the redirect_uri is over http(s)
const redirectUrl = new url_1.URL(request.client_id);
prompt.host = redirectUrl.host;
}
return prompt;
}
/**
* Parses a encoded scope value and constructs IPermissionRequestPrompts
* @param requester the DID of the requester for this permission
* @param encodedScope The Encoded scope value
* @returns IPermissionRequestPrompt(s) corresponding to the scopes requested
*/
async parseScopeValue(requester, encodedScope) {
const prompts = [];
const scope = JSON.parse(Buffer.from(encodedScope, 'base64').toString());
const scopeDefinitionUris = Object.keys(scope);
for (let j = 0; j < scopeDefinitionUris.length; j++) {
const scopeDefinitionUri = scopeDefinitionUris[j];
const scopeModifiers = scope[scopeDefinitionUri];
// Performance Opprotunity: opprotunity to parallelize
const response = await node_fetch_1.default(scopeDefinitionUri);
if (response.status !== 200) {
throw Error(`Failed to retrieve Scope ${scopeDefinitionUri}. ${await response.text()}`);
}
const scopeDefinition = JSON.parse(await response.text());
prompts.push({
required: scopeModifiers && scopeModifiers.essential ? scopeModifiers.essential : false,
name: scopeDefinition.resourceBundle.name,
description: scopeDefinition.resourceBundle.description,
iconUrl: scopeDefinition.resourceBundle.icon_uri,
grants: scopeDefinition.access.map((grantRequest) => {
const contextTypeRegex = /(^[^\/]+\/\/.*)\/(.*)$/i;
const matches = grantRequest.resource_type.match(contextTypeRegex);
if (!matches) {
throw new Error(`Scope Definition ${scopeDefinitionUri} has invalid access resource_type ${grantRequest.resource_type}`);
}
return {
owner: this.sender.id,
grantee: requester,
allow: grantRequest.allow,
context: matches[1],
type: matches[2]
};
}),
});
}
return prompts;
}
}
exports.default = UserAgentSession;
//# sourceMappingURL=UserAgentSession.js.map