-
Notifications
You must be signed in to change notification settings - Fork 45
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
22886cb
commit 662b234
Showing
4 changed files
with
312 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
import { randomUuid } from '@web5/crypto/utils'; | ||
|
||
/** | ||
* The Web5 Connect Request object. | ||
*/ | ||
export type Web5ConnectRequest = any; // TODO: define type in common repo for reuse (https://github.com/TBD54566975/dwn-server/issues/138) | ||
|
||
/** | ||
* The Web5 Connect Response object, which is also an OIDC ID token | ||
*/ | ||
export type Web5ConnectResponse = any; // TODO: define type in common repo for reuse (https://github.com/TBD54566975/dwn-server/issues/138) | ||
|
||
/** | ||
* The result of the setWeb5ConnectRequest() method. | ||
*/ | ||
export type SetWeb5ConnectRequestResult = { | ||
/** | ||
* The Request URI that the wallet should use to retrieve the request object. | ||
*/ | ||
request_uri: string; | ||
|
||
/** | ||
* The time in seconds that the Request URI is valid for. | ||
*/ | ||
expires_in: number; | ||
} | ||
|
||
/** | ||
* The Web5 Connect Server is responsible for handling the Web5 Connect flow. | ||
*/ | ||
export class Web5ConnectServer { | ||
|
||
private baseUrl: string; | ||
private dataStore = new Map(); // TODO: turn this into a TTL cache (https://github.com/TBD54566975/dwn-server/issues/138) | ||
|
||
/** | ||
* Creates a new instance of the Web5 Connect Server. | ||
* @param params.baseUrl The the base URL of the connect server including the port, this is used to construct the full request URI. | ||
*/ | ||
public constructor({ baseUrl }: { | ||
baseUrl: string; | ||
}) { | ||
this.baseUrl = baseUrl; | ||
} | ||
|
||
/** | ||
* Stores the given Web5 Connect Request object, which is also an OAuth 2 Pushed Authorization Request (PAR) object. | ||
* This is the initial call to the connect server to start the Web5 Connect flow. | ||
*/ | ||
public async setWeb5ConnectRequest(request: Web5ConnectRequest): Promise<SetWeb5ConnectRequestResult> { | ||
console.log('Received Pushed Authorization Request (PAR) request.'); | ||
|
||
// Generate a request URI | ||
const requestId = randomUuid(); | ||
const request_uri = `${this.baseUrl}/connect/${requestId}.jwt`; | ||
|
||
// Store the Request Object. | ||
this.dataStore.set(`request:${requestId}`, request); | ||
|
||
return { | ||
request_uri, | ||
expires_in : 600, | ||
}; | ||
} | ||
|
||
/** | ||
* Returns the Web5 Connect Request object. The request ID can only be used once. | ||
*/ | ||
public async getWeb5ConnectRequest(requestId: string): Promise<Web5ConnectRequest | undefined> { | ||
const request = this.dataStore.get(`request:${requestId}`); | ||
|
||
// Delete the Request Object from the data store now that it has been retrieved. | ||
this.dataStore.delete(`request:${requestId}`); | ||
|
||
return request; | ||
} | ||
|
||
/** | ||
* Sets the Web5 Connect Response object, which is also an OIDC ID token. | ||
*/ | ||
public async setWeb5ConnectResponse(state: string, response: Web5ConnectResponse): Promise<any> { | ||
console.log('Identity Provider pushed response with ID token.'); | ||
|
||
this.dataStore.set(`response:${state}`, response); | ||
} | ||
|
||
/** | ||
* Gets the Web5 Connect Response object. The `state` string can only be used once. | ||
*/ | ||
public async getWeb5ConnectResponse(state: string): Promise<Web5ConnectResponse | undefined> { | ||
const response = this. dataStore.get(`response:${state}`); | ||
|
||
// Delete the Response object from the data store now that it has been retrieved. | ||
this.dataStore.delete(`response:${state}`); | ||
|
||
return response; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,116 @@ | ||
import fetch from 'node-fetch'; | ||
import { config } from '../../src/config.js'; | ||
import { DwnServer } from '../../src/dwn-server.js'; | ||
import { expect } from 'chai'; | ||
import { webcrypto } from 'node:crypto'; | ||
|
||
// node.js 18 and earlier needs globalThis.crypto polyfill | ||
if (!globalThis.crypto) { | ||
// @ts-ignore | ||
globalThis.crypto = webcrypto; | ||
} | ||
|
||
describe('Web5 Connect scenarios', function () { | ||
const web5ConnectBaseUrl = 'http://localhost:3000'; | ||
|
||
let dwnServer: DwnServer; | ||
const dwnServerConfig = { ...config } // not touching the original config | ||
|
||
before(async function () { | ||
|
||
// NOTE: using SQL to workaround an issue where multiple instances of DwnServer can be started using LevelDB in the same test run, | ||
// and dwn-server.spec.ts already uses LevelDB. | ||
dwnServerConfig.messageStore = 'sqlite://', | ||
dwnServerConfig.dataStore = 'sqlite://', | ||
dwnServerConfig.eventLog = 'sqlite://', | ||
|
||
dwnServer = new DwnServer({ config: dwnServerConfig }); | ||
await dwnServer.start(); | ||
}); | ||
|
||
after(function () { | ||
dwnServer.stop(() => { }); | ||
}); | ||
|
||
beforeEach(function () { | ||
dwnServer.start(); | ||
}); | ||
|
||
afterEach(function () { | ||
dwnServer.stop(() => {}); | ||
}); | ||
|
||
it('should be able to set and get Web5 Connect Request & Response objects', async () => { | ||
// Scenario: | ||
// 1. App sends the Web5 Connect Request object to the Web5 Connect server. | ||
// 2. Identity Provider (wallet) fetches the Web5 Connect Request object from the Web5 Connect server. | ||
// 3. Should receive 404 if fetching the same Web5 Connect Request again | ||
// 4. Identity Provider (wallet) should receive 400 if sending an incomplete response. | ||
// 5. Identity Provider (wallet) sends the Web5 Connect Response object to the Web5 Connect server. | ||
// 6. App fetches the Web5 Connect Response object from the Web5 Connect server. | ||
// 7. Should receive 404 if fetching the same Web5 Connect Response object again. | ||
|
||
// 1. App sends the Web5 Connect Request object to the Web5 Connect server. | ||
const requestBody = { request: { dummyProperty: 'dummyValue' } }; | ||
const postWeb5ConnectRequestResult = await fetch(`${web5ConnectBaseUrl}/connect/par`, { | ||
method: 'POST', | ||
headers: { 'Content-Type': 'application/json' }, | ||
body: JSON.stringify(requestBody), | ||
}); | ||
expect(postWeb5ConnectRequestResult.status).to.equal(201); | ||
|
||
// 2. Identity Provider (wallet) fetches the Web5 Connect Request object from the Web5 Connect server. | ||
const requestUrl = (await postWeb5ConnectRequestResult.json() as any).request_uri; | ||
const getWeb5ConnectRequestResult = await fetch(requestUrl, { | ||
method: 'GET', | ||
}); | ||
const fetchedRequest = await getWeb5ConnectRequestResult.json(); | ||
expect(getWeb5ConnectRequestResult.status).to.equal(200); | ||
expect(fetchedRequest).to.deep.equal(requestBody.request); | ||
|
||
// 3. Should receive 404 if fetching the same Web5 Connect Request again | ||
const getWeb5ConnectRequestResult2 = await fetch(requestUrl, { | ||
method: 'GET', | ||
}); | ||
expect(getWeb5ConnectRequestResult2.status).to.equal(404); | ||
|
||
// 4. Identity Provider (wallet) should receive 400 if sending an incomplete response. | ||
const incompleteResponseBody = { | ||
id_token : { dummyToken: 'dummyToken' }, | ||
// state : 'dummyState', // intentionally missing | ||
}; | ||
const postIncompleteWeb5ConnectResponseResult = await fetch(`${web5ConnectBaseUrl}/connect/sessions`, { | ||
method: 'POST', | ||
headers: { 'Content-Type': 'application/json' }, | ||
body: JSON.stringify(incompleteResponseBody), | ||
}); | ||
expect(postIncompleteWeb5ConnectResponseResult.status).to.equal(400); | ||
|
||
// 5. Identity Provider (wallet) sends the Web5 Connect Response object to the Web5 Connect server. | ||
const web5ConnectResponseBody = { | ||
id_token : { dummyToken: 'dummyToken' }, | ||
state : 'dummyState', | ||
}; | ||
const postWeb5ConnectResponseResult = await fetch(`${web5ConnectBaseUrl}/connect/sessions`, { | ||
method: 'POST', | ||
headers: { 'Content-Type': 'application/json' }, | ||
body: JSON.stringify(web5ConnectResponseBody), | ||
}); | ||
expect(postWeb5ConnectResponseResult.status).to.equal(201); | ||
|
||
// 6. App fetches the Web5 Connect Response object from the Web5 Connect server. | ||
const web5ConnectResponseUrl = `${web5ConnectBaseUrl}/connect/sessions/${web5ConnectResponseBody.state}.jwt`; | ||
const getWeb5ConnectResponseResult = await fetch(web5ConnectResponseUrl, { | ||
method: 'GET', | ||
}); | ||
const fetchedResponse = await getWeb5ConnectResponseResult.json(); | ||
expect(getWeb5ConnectResponseResult.status).to.equal(200); | ||
expect(fetchedResponse).to.deep.equal(web5ConnectResponseBody.id_token); | ||
|
||
// 7. Should receive 404 if fetching the same Web5 Connect Response object again. | ||
const getWeb5ConnectResponseResult2 = await fetch(web5ConnectResponseUrl, { | ||
method: 'GET', | ||
}); | ||
expect(getWeb5ConnectResponseResult2.status).to.equal(404); | ||
}); | ||
}); |