@microsoft/useragent-sdk
Version:
SDK for building decentralized identity wallets and enterprise agents.
547 lines • 25 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
const src_1 = require("../../src");
const Identifier_1 = require("../../src/Identifier");
const TestResolver_1 = require("../resolvers/TestResolver");
const nodeFetch = require("node-fetch");
const KeyContainer_1 = require("../../src/crypto/keys/KeyContainer");
describe('UserAgentSession', () => {
let challengerSession;
let responderSession;
let challenger;
let responder;
let challengerResolver = new TestResolver_1.default();
let responderResolver = new TestResolver_1.default();
beforeAll(async () => {
challenger = new Identifier_1.default('did:test:challenger', new src_1.UserAgentOptions());
responder = new Identifier_1.default('did:test:responder', new src_1.UserAgentOptions());
});
beforeEach(async () => {
challengerSession = new src_1.UserAgentSession(challenger, 'did:ion-did:ion-ES256K-sig', challengerResolver);
responderSession = new src_1.UserAgentSession(responder, 'did:ion-did:ion-ES256K-sig', responderResolver);
});
describe('signRequest', () => {
const redirectUrl = `localhost/${Math.round(Math.random() * 255).toString(16)}`;
let defaultOIDC = {
client_id: redirectUrl,
iss: 'did:test:challenger',
nonce: 'Will Be Set In The Test',
response_mode: "form_post",
response_type: "id_token",
scope: 'openid did_authn',
registration: {
id_token_signed_response_alg: ['EdDSA']
}
};
let testParams = [
{
should: 'create a simple id_token request',
expect: defaultOIDC
},
{
should: 'include state',
options: {
state: 'alphabet soup'
},
expect: Object.assign({}, defaultOIDC, {
state: 'alphabet soup'
})
},
{
should: 'encode the manifest',
options: {
manifest: {
client_name: 'alice',
logo_uri: 'localhost/favicon.ico',
}
},
expect: Object.assign({}, defaultOIDC, {
registration: JSON.stringify({
client_name: 'alice',
logo_uri: 'localhost/favicon.ico',
})
})
},
{
should: 'include additonal scopes',
options: {
scopes: [
{
'localhost/test.all': null,
}
]
},
expect: Object.assign({}, defaultOIDC, {
scope: 'openid did_authn ' + Buffer.from(JSON.stringify({
'localhost/test.all': null,
})).toString('base64')
})
},
{
should: 'support multiple scope definitions',
options: {
scopes: [
{
'localhost/test.all': { essential: false },
},
{
'localhost/test.all': { value: 'sure' },
}
]
},
expect: Object.assign({}, defaultOIDC, {
scope: 'openid did_authn ' + Buffer.from(JSON.stringify({
'localhost/test.all': { essential: false },
})).toString('base64') + ' ' + Buffer.from(JSON.stringify({
'localhost/test.all': { value: 'sure' },
})).toString('base64')
})
},
];
testParams.forEach((testCase) => {
it(`should ${testCase.should}`, async () => {
const nonce = Math.round(Math.random() * Number.MAX_SAFE_INTEGER).toString(16);
const signSpy = spyOn(challenger, 'sign').and.callFake((request) => {
testCase.expect.nonce = nonce;
expect(request).toEqual(testCase.expect);
return Promise.resolve('');
});
await challengerSession.signRequest(redirectUrl, nonce, testCase.options);
expect(signSpy).toHaveBeenCalled();
});
});
});
describe('verifyAndHydrateRequest', () => {
const redirectUrl = `http://localhost/${Math.round(Math.random() * 255).toString(16)}`;
const defaultPrompt = {
client_id: redirectUrl,
host: 'localhost',
iss: 'did:test:challenger',
nonce: 'Will Be Set In The Test',
response_mode: 'form_post',
response_type: 'id_token',
scope: 'openid did_authn'
};
const noScopePrompt = {
client_id: redirectUrl,
host: 'localhost',
iss: 'did:test:challenger',
nonce: 'Will Be Set In The Test',
response_mode: 'form_post',
response_type: 'id_token',
};
let testParams = [{
should: 'create a basic sign in prompt',
expected: Object.assign({}, defaultPrompt)
},
{
should: 'populate fields in the manifest',
request: {
manifest: {
client_name: 'Test Client 123',
logo_uri: 'http://localhost/icon',
client_uri: 'http://localhost/index.html',
policy_uri: 'http://localhost/cookies',
tos_uri: 'http://localhost/tos'
}
},
expected: Object.assign({}, defaultPrompt, {
name: 'Test Client 123',
logoUrl: 'http://localhost/icon',
homepage: 'http://localhost/index.html',
dataUsePolicy: 'http://localhost/cookies',
termsOfService: 'http://localhost/tos'
})
},
{
should: 'ignore unfilled manifest fields',
request: {
manifest: {
client_name: 'Test Client 123'
}
},
expected: Object.assign({}, defaultPrompt, {
name: 'Test Client 123',
logoUrl: undefined,
homepage: undefined,
dataUsePolicy: undefined,
termsOfService: undefined
})
},
// DISABLED WHILE CLAIMS THROW
// {
// should: 'include credential claims',
// request: {
// claimRequests: {
// credential: {
// TestStatus: null
// }
// }
// },
// expected: Object.assign({}, defaultPrompt, {
// credentialsRequested: ['TestStatus']
// })
// },
{
should: 'retrieve permission scopes',
request: {
scopes: [{
'http://localhost/testStatus.read': { essential: true },
}]
},
resolvedScopes: {
"http://localhost/testStatus.read": {
value: "http://localhost/testStatus.read",
resourceBundle: {
name: "Test Status",
description: "Read access to all test statuses",
icon_uri: "http://localhost/icon.ico"
},
access: [
{
resource_type: 'https://schema.org/TestStatus',
allow: '-R--'
}
]
}
},
expected: Object.assign({}, noScopePrompt, {
identityHubPermissionsRequested: [
{
required: true,
name: "Test Status",
description: "Read access to all test statuses",
iconUrl: 'http://localhost/icon.ico',
grants: [
{
owner: 'did:test:responder',
grantee: 'did:test:challenger',
allow: '-R--',
context: 'https://schema.org',
type: 'TestStatus'
}
]
}
]
})
},
{
should: 'handle multiple different permission scopes',
request: {
scopes: [{
'http://localhost/testStatus.read': { essential: true },
'http://localhost/testStatus.write': null,
}]
},
resolvedScopes: {
"http://localhost/testStatus.read": {
value: "http://localhost/testStatus.read",
resourceBundle: {
name: "Test Status",
description: "Read access to all test statuses",
icon_uri: "http://localhost/icon.ico"
},
access: [
{
resource_type: 'https://schema.org/TestStatus',
allow: '-R--'
}
]
},
"http://localhost/testStatus.write": {
value: "http://localhost/testStatus.write",
resourceBundle: {
name: "Test Status",
description: "Write access to all test statuses",
icon_uri: "http://localhost/icon.ico"
},
access: [
{
resource_type: 'https://schema.org/TestStatus',
allow: 'C-UD'
}
]
}
},
expected: Object.assign({}, noScopePrompt, {
identityHubPermissionsRequested: [
{
required: true,
name: "Test Status",
description: "Read access to all test statuses",
iconUrl: 'http://localhost/icon.ico',
grants: [
{
owner: 'did:test:responder',
grantee: 'did:test:challenger',
allow: '-R--',
context: 'https://schema.org',
type: 'TestStatus'
}
]
},
{
required: false,
name: "Test Status",
description: "Write access to all test statuses",
iconUrl: 'http://localhost/icon.ico',
grants: [
{
owner: 'did:test:responder',
grantee: 'did:test:challenger',
allow: 'C-UD',
context: 'https://schema.org',
type: 'TestStatus'
}
]
}
]
})
}];
testParams.forEach((testCase) => {
it(`should ${testCase.should}`, async () => {
const nonce = Math.round(Math.random() * Number.MAX_SAFE_INTEGER).toString(16);
// utilize the sign request function, since we've got it...
let formedRequest;
spyOn(challengerSession['sender'], 'sign').and.callFake((request) => {
formedRequest = request;
return Promise.resolve('');
});
await challengerSession.signRequest(redirectUrl, nonce, testCase.request);
const encryptedStub = Math.round(Math.random() * Number.MAX_SAFE_INTEGER).toString(32);
const verifySpy = spyOn(responderSession, 'verify').and.callFake(async (request) => {
expect(request).toEqual(encryptedStub);
return Promise.resolve(formedRequest);
});
const fetchSpy = spyOn(nodeFetch, 'default').and.callFake((url) => {
if (testCase.resolvedScopes && url in testCase.resolvedScopes) {
return Promise.resolve({
status: 200,
text: () => {
return JSON.stringify(testCase.resolvedScopes[url]);
}
});
}
else {
fail(`Attempted superfluous resolve to url: ${url}`);
}
});
let expected = Object.assign({}, formedRequest, testCase.expected, { nonce });
let actual = await responderSession.verifyAndHydrateRequest(encryptedStub);
expect(verifySpy).toHaveBeenCalled();
if (testCase.resolvedScopes) {
expect(fetchSpy).toHaveBeenCalled();
}
;
expect(actual).toEqual(expected);
});
});
it('should not include host if the redirect is a deeplink', async () => {
const nonce = Math.round(Math.random() * Number.MAX_SAFE_INTEGER).toString(16);
// utilize the sign request function, since we've got it...
let formedRequest;
spyOn(challengerSession['sender'], 'sign').and.callFake((request) => {
formedRequest = request;
return Promise.resolve('');
});
await challengerSession.signRequest('myapp://response', nonce);
const verifySpy = spyOn(responderSession, 'verify').and.returnValue(Promise.resolve(formedRequest));
let actual = await responderSession.verifyAndHydrateRequest('doesnt Matter');
expect(verifySpy).toHaveBeenCalled();
expect(actual.host).toBeUndefined();
});
it('should throw if the scope definition failed to be retrieved', async () => {
const nonce = Math.round(Math.random() * Number.MAX_SAFE_INTEGER).toString(16);
// utilize the sign request function, since we've got it...
let formedRequest;
spyOn(challengerSession['sender'], 'sign').and.callFake((request) => {
formedRequest = request;
return Promise.resolve('');
});
await challengerSession.signRequest(redirectUrl, nonce, {
scopes: [
{
'https://169.254.0.0/404': null,
}
]
});
const verifySpy = spyOn(responderSession, 'verify').and.returnValue(Promise.resolve(formedRequest));
const fetchSpy = spyOn(nodeFetch, 'default').and.returnValue(Promise.resolve({
status: 404,
text: () => 'Not Found'
}));
try {
await responderSession.verifyAndHydrateRequest('doesnt Matter');
fail('expected to throw');
}
catch (error) {
expect(error.message).toContain('https://169.254.0.0/404');
}
finally {
expect(verifySpy).toHaveBeenCalled();
expect(fetchSpy).toHaveBeenCalled();
}
});
it('should throw if the scope definitions resource_type is incorrectly formatted', async () => {
const nonce = Math.round(Math.random() * Number.MAX_SAFE_INTEGER).toString(16);
// utilize the sign request function, since we've got it...
let formedRequest;
spyOn(challengerSession['sender'], 'sign').and.callFake((request) => {
formedRequest = request;
return Promise.resolve('');
});
await challengerSession.signRequest(redirectUrl, nonce, {
scopes: [
{
'https://localhost/ok': null,
}
]
});
const verifySpy = spyOn(responderSession, 'verify').and.returnValue(Promise.resolve(formedRequest));
const fetchSpy = spyOn(nodeFetch, 'default').and.returnValue(Promise.resolve({
status: 200,
text: () => JSON.stringify({
value: "http://localhost/ok",
resourceBundle: {
name: "Test",
description: "should fail"
},
access: [
{
resource_type: 'https://schema.org',
allow: '----'
}
]
})
}));
try {
await responderSession.verifyAndHydrateRequest('doesnt Matter');
fail('expected to throw');
}
catch (error) {
expect(error.message).toContain('resource_type');
}
finally {
expect(verifySpy).toHaveBeenCalled();
expect(fetchSpy).toHaveBeenCalled();
}
});
});
describe('sendResponse', () => {
const redirectUrl = `http://localhost/${Math.round(Math.random() * 255).toString(16)}`;
const defaultResponse = {
iss: 'https://self-issued.me',
aud: redirectUrl,
sub: 'thumbprint',
nonce: 'nonce',
did_comm: {
did: 'did:test:responder'
},
sub_jwk: {},
exp: 0,
iat: 0
};
const testCases = [
{
should: 'send a simple id_token back',
expected: Object.assign({}, defaultResponse)
},
{
should: 'send permission grants',
grants: [
{
name: 'Test',
description: 'Read test results',
required: true,
grants: [
{
allow: '-R--',
context: 'schema.org/',
owner: 'did:test:responder',
grantee: 'did:test:challenger',
type: 'test',
}
]
}
],
expected: Object.assign({}, defaultResponse)
}
];
testCases.forEach((testCase) => {
it(`should ${testCase.should}`, async () => {
const nonce = Math.round(Math.random() * Number.MAX_SAFE_INTEGER).toString(16);
// utilize the sign request function, since we've got it...
let formedRequest;
spyOn(challengerSession['sender'], 'sign').and.callFake((request) => {
formedRequest = request;
return Promise.resolve('');
});
await challengerSession.signRequest(redirectUrl, nonce, testCase.request);
// mock the entire HubInterface to avoid sending data
let grantsSubmitted = 0;
responderSession['permissions'] = {
addObject: (commit) => {
if (testCase.grants) {
let grant = commit;
grant.grants.forEach((permissionGrant) => {
expect(permissionGrant.owner).toEqual('did:test:responder');
expect(permissionGrant.grantee).toEqual('did:test:challenger');
if (!testCase.grants.some((grantBundle) => {
return grantBundle.grants.some((expectedGrant) => {
return permissionGrant.allow === expectedGrant.allow && permissionGrant.context === expectedGrant.context && permissionGrant.type === expectedGrant.type;
});
})) {
fail(`Unexpected Permission Grant commited: ${grant}`);
}
else {
grantsSubmitted++;
}
});
}
else {
fail('Attempted to Commit an object when no Permission Grants were passed');
}
return Promise.resolve({
getRevisions: () => { return 'permissionGrantId'; }
});
}
};
spyOn(responderSession['sender']['options']['keyStore'], 'get').and.returnValue(Promise.resolve(new KeyContainer_1.default(new src_1.EcPrivateKey({
d: Math.round(Math.random() * Number.MAX_SAFE_INTEGER),
x: Math.round(Math.random() * Number.MAX_SAFE_INTEGER),
y: Math.round(Math.random() * Number.MAX_SAFE_INTEGER)
}).getPublicKey())));
const signCode = Math.round(Math.random() * Number.MAX_SAFE_INTEGER).toString(16);
const signSpy = spyOn(responderSession['sender'], 'sign').and.callFake((response) => {
expect(response).toEqual(Object.assign(testCase.expected, {
nonce,
exp: response.exp,
iat: response.iat,
sub: response.sub,
sub_jwk: response.sub_jwk,
}));
return Promise.resolve(signCode);
});
const fetchSpy = spyOn(nodeFetch, 'default').and.callFake((url, options) => {
expect(url).toEqual(redirectUrl);
expect(options.method.toLowerCase()).toEqual('post');
expect(options.body).toEqual(`id_token=${signCode}`);
return Promise.resolve({
status: 204,
text: () => 'Test passed'
});
});
await responderSession.sendResponse(formedRequest, testCase.grants, testCase.claims);
expect(signSpy).toHaveBeenCalled();
expect(fetchSpy).toHaveBeenCalled();
if (testCase.grants) {
const totalGrantsExpected = testCase.grants.reduce((total, grantPrompt) => {
return total + grantPrompt.grants.length;
}, 0);
expect(grantsSubmitted).toEqual(totalGrantsExpected);
}
});
});
});
});
//# sourceMappingURL=UserAgentSession.spec.js.map