Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added Web5 Connect Server #139

Merged
merged 4 commits into from
Jun 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,7 @@ Configuration can be set using environment variables
| `DS_PORT` | Port that the server listens on | `3000` |
| `DS_MAX_RECORD_DATA_SIZE` | Maximum size for `RecordsWrite` data. use `b`, `kb`, `mb`, `gb` for value | `1gb` |
| `DS_WEBSOCKET_SERVER` | Whether to enable listening over `ws:`. values: `on`,`off` | `on` |
| `DWN_BASE_URL` | Base external URL of this DWN. Used to construct URL paths such as the `Request URI` for the Web5 Connect flow. | `http://localhost` |
| `DWN_REGISTRATION_STORE_URL` | URL to use for storage of registered DIDs. Leave unset to if DWN does not require registration (ie. open for all) | unset |
| `DWN_REGISTRATION_PROOF_OF_WORK_SEED` | Seed to generate the challenge nonce from, this allows all DWN instances in a cluster to generate the same challenge. | unset |
| `DWN_REGISTRATION_PROOF_OF_WORK_ENABLED` | Require new users to complete a proof-of-work challenge | `false` |
Expand Down
7 changes: 7 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,13 @@ export const config = {
* otherwise we fall back on the use defined `DWN_SERVER_PACKAGE_NAME` or `@web5/dwn-server`.
*/
serverName: process.env.npm_package_name || process.env.DWN_SERVER_PACKAGE_NAME || '@web5/dwn-server',

/**
* The base external URL of this DWN.
* This is used to construct URL paths such as the `Request URI` in the Web5 Connect flow.
*/
baseUrl: process.env.DWN_BASE_URL || 'http://localhost',

/**
* Used to populate the `version` and `sdkVersion` properties returned by the `/info` endpoint.
*
Expand Down
102 changes: 96 additions & 6 deletions src/http-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,26 +12,28 @@ import { v4 as uuidv4 } from 'uuid';

import type { RequestContext } from './lib/json-rpc-router.js';
import type { JsonRpcRequest } from './lib/json-rpc.js';
import { createJsonRpcErrorResponse, JsonRpcErrorCodes } from './lib/json-rpc.js';

import type { DwnServerConfig } from './config.js';
import type { DwnServerError } from './dwn-error.js';
import type { RegistrationManager } from './registration/registration-manager.js';
import { config } from './config.js';
import { type DwnServerError } from './dwn-error.js';
import { jsonRpcRouter } from './json-rpc-api.js';
import { Web5ConnectServer } from './web5-connect/web5-connect-server.js';
import { createJsonRpcErrorResponse, JsonRpcErrorCodes } from './lib/json-rpc.js';
import { requestCounter, responseHistogram } from './metrics.js';
import type { RegistrationManager } from './registration/registration-manager.js';


export class HttpApi {
#config: DwnServerConfig;
#packageInfo: { version?: string, sdkVersion?: string, server: string };
#api: Express;
#server: http.Server;
web5ConnectServer: Web5ConnectServer;
registrationManager: RegistrationManager;
dwn: Dwn;

constructor(config: DwnServerConfig, dwn: Dwn, registrationManager?: RegistrationManager) {
console.log(config);
log.info(config);

this.#packageInfo = {
server: config.serverName,
Expand All @@ -55,6 +57,11 @@ export class HttpApi {
this.registrationManager = registrationManager;
}

// create the Web5 Connect Server
this.web5ConnectServer = new Web5ConnectServer({
baseUrl: `${config.baseUrl}:${config.port}`,
});

this.#setupMiddleware();
this.#setupRoutes();
}
Expand Down Expand Up @@ -313,6 +320,7 @@ export class HttpApi {
}

res.json({
url : config.baseUrl,
server : this.#packageInfo.server,
maxFileSize : config.maxRecordDataSize,
registrationRequirements : registrationRequirements,
Expand All @@ -321,6 +329,8 @@ export class HttpApi {
webSocketSupport : config.webSocketSupport,
});
});

this.#setupWeb5ConnectServerRoutes();
}

#listen(port: number, callback?: () => void): void {
Expand All @@ -342,7 +352,7 @@ export class HttpApi {
if (this.#config.registrationStoreUrl !== undefined) {
this.#api.post('/registration', async (req: Request, res: Response) => {
const requestBody = req.body;
console.log('Registration request:', requestBody);
log.info('Registration request:', requestBody);

try {
await this.registrationManager.handleRegistrationRequest(requestBody);
Expand All @@ -353,14 +363,94 @@ export class HttpApi {
if (dwnServerError.code !== undefined) {
res.status(400).json(dwnServerError);
} else {
console.log('Error handling registration request:', error);
log.info('Error handling registration request:', error);
res.status(500).json({ success: false });
}
}
});
}
}

#setupWeb5ConnectServerRoutes(): void {
/**
* Endpoint that the connecting App pushes the Pushed Authorization Request Object to start the Web5 Connect flow.
*/
this.#api.post('/connect/par', async (req, res) => {
log.info('Storing Pushed Authorization Request (PAR) request...');

const result = await this.web5ConnectServer.setWeb5ConnectRequest(req.body.request);
res.status(201).json(result);
});

/**
* Endpoint that the Identity Provider (wallet) calls to retrieve the Pushed Authorization Request.
*/
this.#api.get('/connect/:requestId.jwt', async (req, res) => {
LiranCohen marked this conversation as resolved.
Show resolved Hide resolved
log.info(`Retrieving Web5 Connect Request object of ID: ${req.params.requestId}...`);

// Look up the request object based on the requestId.
const requestObjectJwt = await this.web5ConnectServer.getWeb5ConnectRequest(req.params.requestId);

if (!requestObjectJwt) {
res.status(404).json({
ok : false,
status : { code: 404, message: 'Not Found' }
});
} else {
res.set('Content-Type', 'application/jwt');
res.send(requestObjectJwt);
}
});

/**
* Endpoint that the Identity Provider (wallet) pushes the Authorization Response ID token to.
*/
this.#api.post('/connect/sessions', async (req, res) => {
log.info('Storing Identity Provider (wallet) pushed response with ID token...');

// Store the ID token.
const idToken = req.body.id_token;
const state = req.body.state;

if (idToken !== undefined && state != undefined) {

await this.web5ConnectServer.setWeb5ConnectResponse(state, idToken);

res.status(201).json({
ok : true,
status : { code: 201, message: 'Created' }
});

} else {
res.status(400).json({
ok : false,
status : { code: 400, message: 'Bad Request' }
});
}
});

/**
* Endpoint that the connecting App polls to check if the Identity Provider (Wallet) has posted the Web5 Connect Response object.
* The Web5 Connect Response is also an ID token.
*/
this.#api.get('/connect/sessions/:state.jwt', async (req, res) => {
log.info(`Retrieving ID token for state: ${req.params.state}...`);

// Look up the ID token.
const idToken = await this.web5ConnectServer.getWeb5ConnectResponse(req.params.state);

if (!idToken) {
res.status(404).json({
ok : false,
status : { code: 404, message: 'Not Found' }
});
} else {
res.set('Content-Type', 'application/jwt');
res.send(idToken);
}
});
}

async start(port: number, callback?: () => void): Promise<http.Server> {
this.#listen(port, callback);
return this.#server;
Expand Down
95 changes: 95 additions & 0 deletions src/web5-connect/web5-connect-server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
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 given to the Identity Provider (wallet) to fetch the Web5 Connect Request object.
*/
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> {
// 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> {
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;
}
}
1 change: 1 addition & 0 deletions tests/http-api.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1042,6 +1042,7 @@ describe('http api', function () {
expect(resp.status).to.equal(200);

const info = await resp.json();
expect(info['url']).to.equal('http://localhost');
expect(info['server']).to.equal('@web5/dwn-server');
expect(info['registrationRequirements']).to.include('terms-of-service');
expect(info['registrationRequirements']).to.include(
Expand Down
Loading
Loading