README
@selfage/service_handler
Install
npm install @selfage/service_handler
Overview
Written in TypeScript and compiled to ES6 with inline source map & source. See @selfage/tsconfig for full compiler options. Provides type-safe service handler interfaces to be implemented and hooked onto Express.js
. The service here only refers to one simple kind of client-server interaction: Sending a HTTP POST request in JSON as request body and receiving a response in JSON.
UnauthedServiceHandler
UnauthedServiceHandler
is an interface that can be implemented as the following example.
import { UnauthedServiceHandler } from '@selfage/service_handler';
import { GetCommentsRequest, GetCommentsResponse, GET_COMMENTS } from './get_comments';
export class GetCommentsHandler implements UnauthedServiceHandler<GetCommentsRequest, GetCommentsResponse> {
public serviceDescriptor = GET_COMMENTS;
public async handle(logContext: string, request: GetCommentsRequest): Promise<GetCommentsResponse> {
// await database operations.
return {
texts: ["comment1", "comment2"]
};
}
}
logContext
contains a randomly generated request id, though not guaranteed to be universally unique, that can be prepended to any logging happened within the life of a request processing, making it easier to group logs associated with a certain request.
get_comments.ts
(source) is typically generated by installing @selfage/cli
and running selfage gen get_comments
which requires an input file get_comments.json
(source), specifying the url endpoint/path as /get_comments
.
See @selfage/service_descriptor
and @selfage/message
for more explanation of the JSON file. Typically, get_comments.json
and get_comments.ts
will be shared between client-side and server-side code.
Partial implementation of AuthedServiceHandler
Authentication is done through validating a signed session string, passed from the request body, i.e., signedSession
field from the request. See @selfage/service_descriptor
for an example of generating an AuthedServiceDescriptor
. Also read further below for how to obtain a signed session string.
AuthedServiceHandler
is an interface requiring sessionDescriptor
to help parse the validated session string into a structured data. See @selfage/message
for how to generate a MessageDescriptor
.
Typically for one project, the session strucutre is always the same. By partially implementing AuthedServiceHandler
, i.e., by only providing sessionDescriptor
, the rest of the project can extends it for consistent session parsing.
In the example below, we import session defined in get_history.json
which also contains GetHistory
service, and name the file as authed_service_handler.ts
.
import { AuthedServiceHandler } from '@selfage/service_handler';
import { AuthedServiceDescriptor } from '@selfage/service_descriptor';
import { MySession, MY_SESSION } from './get_history';
export abstract class AuthedServiceHandlerWithSession<ServiceRequest, ServiceResponse> implements AuthedServiceHandler<ServiceRequest, ServiceResponse, MySession> {
public sessionDescriptor = MY_SESSION;
abstract serviceDescriptor: AuthedServiceDescriptor<ServiceRequest, ServiceResponse>;
abstract handle: handle: (logContext: string, request: ServiceRequest, session: MySession ) => Promise<ServiceResponse>;
}
Full implementation of AuthedServiceHandler
An example that extends the partially implemented AuthedServiceHandler
above looks like the following.
import { AuthedServiceHandlerWithSession } from './authed_service_handler';
import { MySession, GetHistoryRequest, GetHistoryResponse, GET_HISTORY } from './get_history';
export class GetHistoryHandler extends AuthedServiceHandlerWithSession<GetHistoryRequest, GetHistoryResponse> {
public serviceDescriptor = GET_HISTORY;
public async handle(logContext: string, request: GetHistoryRequest, session: MySession): Promise<GetHistoryResponse> {
// await database operations.
return {
// ...
};
}
}
Except extending AuthedServiceHandlerWithSession
and the additional session
argument in method handle()
, see UnauthedServiceHandler for a full example.
get_history.ts
(source) is typically generated by installing @selfage/cli
and running selfage gen get_history
which requires an input file get_history.json
(source), specifying the url endpoint/path as /get_history
.
Register unauthed/authed handlers
The following is an exmaple to register the handlers above to Express.js
. Under the hood, it takes path
field in a service descriptor as the routing path in Express.js
and wraps handlers to parse request and send response.
import express = require('express');
import { registerUnauthed, registerAuthed } from '@selfage/service_handler/register';
import { GetCommentsHandler } from './get_comments_handler';
import { GetHistoryHandler } from './get_history_handler';
let app = express();
registerUnauthed(app, new GetCommentsHandler());
registerAuthed(app, new GetHistoryHandler());
In particular, handlers are wrapped with the following common process.
- Allows CORS for all sites.
- Use
express.json()
middleware to read and parse JSON string. - Validate the JSON request object into a type-safe request object.
- Validate and parse
signedSession
field into a type-safe session object, when callregisterAuthed()
with anAuthedServiceHandler
. - Pass request and session into the handler.
- Catch error thrown from handlers.
- If an error is caught, check the presence of
statusCode
field of the error and respond with it, or respond 500 if not present. E.g., you could throw anHttpError
from@selfage/http_error
when encounter issues. - If no error is caught, respond with the response object as a JSON object.
CORS & preflight handler
Allowing CORS for all domains is an opinionated decision that restricting CORS doesn't help account/data security at all, but might annoy future development. We should guarantee security by other approaches.
Before making any cross-site request, browsers might send a preflight request to ask for valid domain/site. We provide a simple preflight handler to allow all sites.
import express = require('express');
import { registerCorsAllowedPreflightHandler } from '@selfage/service_handler/preflight_handler';
let app = express();
registerCorsAllowedPreflightHandler(app);
Sign a session string
You have to configure your secret key for signing at the startup of your server, i.e., a secret key for sha256 algorithm. Please refer to other instructions on the best practice of generating a secret key and storing it.
import { SessionSigner } from '@selfage/service_handler/session_signer';
SessionSigner.SECRET_KEY = 'Configure a secrect key';
// Configure routing and start server.
A typical example showing below is to return the signed session string when signing in, supposing SignInResponse
containing a signedSession
field.
import { SessionBuilder } from '@selfage/service_handler/session_signer';
import { UnauthedServiceHandler } from '@selfage/service_handler';
import { SignInRequest, SignInResponse, SIGN_IN } from './sign_in';
export class GetCommentsHandler implements UnauthedServiceHandler<SignInRequest, SignInResponse> {
public serviceDescriptor = SIGN_IN;
private sessionBuilder = SessionBuilder.create();
public async handle(logContext: string, request: SignInRequest): Promise<SignInResponse> {
// await database operations.
let signedSession = this.sessionBuilder.build(JSON.stringify({sessionId: '1234', userId: '5678'}));
return {
signedSession: signedSession
};
}
}
Session expiration
Regardless of the data structure of your session, the signed session string always contains the timestamp when signing. By default, a session is expired 30 days after the signing timestamp. You have to re-sign a session the same way as a new session and return it to the client to refresh the timestamp.
You can configure the session longevity as the following, usually before starting your server.
import { SessionExtractor } from '@selfage/service_handler/session_signer';
SessionExtractor.SESSION_LONGEVITY = 30 * 24 * 60 * 60; // seconds
// Configure routing and start server.
Session validation error
All AuthedServiceHandler
s registered with registerAuthed()
will validate incoming requests' signedSession
field, and either catch validation errors or proceed to the implemented AuthedServiceHandler
s. For any validatoin error caught, we will return the 401 error code to the client, regardless of missing session, invalid signature or expired timestamp.
Request body size
We choose 1MiB or 1024*1024
bytes as the limit of the request body size, making the same assumption as Google's Datastore which imposes the same size limit for an entity.