Skip to content

Commit

Permalink
Implement bearer token validation and auth.
Browse files Browse the repository at this point in the history
  • Loading branch information
johnspurlock-skymethod committed Feb 12, 2022
1 parent 3b2ad62 commit 3af2084
Show file tree
Hide file tree
Showing 18 changed files with 141 additions and 52 deletions.
33 changes: 20 additions & 13 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,15 +34,23 @@ export async function parseRpcOptions(options: Record<string, unknown>) {
return { origin, privateKey };
}

export async function sendRpc(request: RpcRequest, origin: string, privateKey: CryptoKey) {
export async function sendRpc(request: RpcRequest, origin: string, credential: { privateKey: CryptoKey } | { bearerToken: string }) {
const body = JSON.stringify(request);
const method = 'POST';
const url = `${origin}/rpc`;
const keyId = 'admin';
const { signature, date, digest, stringToSign } = await computeHttpSignatureHeaders({ method, url, body, privateKey, keyId })
const headers = { date, signature, digest, 'content-type': APPLICATION_JSON_UTF8 };
console.log(Object.entries(headers).map(v => v.join(': ')).join('\n'));
console.log(stringToSign);
let headers: Record<string, string> = { 'content-type': APPLICATION_JSON_UTF8 };
if ('privateKey' in credential) {
// http-signature-based authorization
const { privateKey } = credential;
const { signature, date, digest, stringToSign } = await computeHttpSignatureHeaders({ method, url, body, privateKey, keyId });
headers = { ...headers, date, signature, digest };
if (_verbose) console.log(stringToSign);
} else {
// bearer-token-based authorization
headers = { ...headers, authorization: `Bearer ${credential.bearerToken}` };
}
if (_verbose) console.log(Object.entries(headers).map(v => v.join(': ')).join('\n'));

const fetcher = makeMinipubFetcher();
const res = await fetcher(url, { method, body, headers });
Expand All @@ -52,7 +60,10 @@ export async function sendRpc(request: RpcRequest, origin: string, privateKey: C

//

let _verbose = false;

async function minipub(args: (string | number)[], options: Record<string, unknown>) {
_verbose = !!options.verbose;
const command = args[0];
const fn = {
'activity-pub': activityPub, ap: activityPub,
Expand Down Expand Up @@ -83,15 +94,11 @@ async function minipub(args: (string | number)[], options: Record<string, unknow
await fn(args.slice(1), options);
}

async function tmp() {
const txt = await Deno.readTextFile('asdf');
const obj = JSON.parse(txt);
if (obj.signature && obj.signature.type === 'RsaSignature2017') {
// https://docs.joinmastodon.org/spec/security/#ld-sign
delete obj.signature;
async function tmp(_args: (string | number)[], options: Record<string, unknown>) {
const { origin, token } = options;
if (typeof origin === 'string' && typeof token === 'string') {
await sendRpc({ kind: 'delete-note', objectUuid: newUuid() }, origin, { bearerToken: token });
}
const apo = ApObject.parseObj(obj);
console.log(apo.toObj());
}

function uuid() {
Expand Down
2 changes: 1 addition & 1 deletion src/cli_create_note.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export async function createNote(args: (string | number)[], options: Record<stri
to: [ to ],
cc: cc ? [ cc ] : undefined,
};
await sendRpc(req, origin, privateKey);
await sendRpc(req, origin, { privateKey });
}

//
Expand Down
2 changes: 1 addition & 1 deletion src/cli_create_user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export async function createUser(_args: (string | number)[], options: Record<str
url,
icon,
};
await sendRpc(req, origin, privateKey);
await sendRpc(req, origin, { privateKey });
}

export async function parseUserOptions(options: Record<string, unknown>): Promise<{ origin: string; privateKey: CryptoKey, username?: string; name?: string; url?: string, icon?: Icon; iconSize?: number; }> {
Expand Down
2 changes: 1 addition & 1 deletion src/cli_delete_from_storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export async function deleteFromStorage(args: (string | number)[], options: Reco
domain,
key,
};
await sendRpc(req, origin, privateKey);
await sendRpc(req, origin, { privateKey });
}

//
Expand Down
2 changes: 1 addition & 1 deletion src/cli_delete_note.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export async function deleteNote(args: (string | number)[], options: Record<stri
kind: 'delete-note',
objectUuid,
};
await sendRpc(req, origin, privateKey);
await sendRpc(req, origin, { privateKey });
}

//
Expand Down
2 changes: 1 addition & 1 deletion src/cli_federate_activity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export async function federateActivity(args: (string | number)[], options: Recor
activityUuid,
dryRun,
};
await sendRpc(req, origin, privateKey);
await sendRpc(req, origin, { privateKey });
}

//
Expand Down
2 changes: 1 addition & 1 deletion src/cli_generate_admin_token.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export async function generateAdminToken(_args: (string | number)[], options: Re
const req: GenerateAdminTokenRequest = {
kind: 'generate-admin-token',
};
await sendRpc(req, origin, privateKey);
await sendRpc(req, origin, { privateKey });
}

//
Expand Down
2 changes: 1 addition & 1 deletion src/cli_like_object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export async function likeObject(args: (string | number)[], options: Record<stri
actorUuid,
objectId,
};
await sendRpc(req, origin, privateKey);
await sendRpc(req, origin, { privateKey });
}

//
Expand Down
2 changes: 1 addition & 1 deletion src/cli_revoke_admin_token.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export async function revokeAdminToken(_args: (string | number)[], options: Reco
const req: RevokeAdminTokenRequest = {
kind: 'revoke-admin-token',
};
await sendRpc(req, origin, privateKey);
await sendRpc(req, origin, { privateKey });
}

//
Expand Down
23 changes: 20 additions & 3 deletions src/cli_server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@ import { computeObject, matchObject } from './endpoints/object_endpoint.ts';
import { computeRpc, matchRpc } from './endpoints/rpc_endpoint.ts';
import { computeWebfinger, matchWebfinger } from './endpoints/webfinger_endpoint.ts';
import { makeSqliteStorage } from './sqlite_storage.ts';
import { computeServerResponse, ServerRequestOptionsProvider, ServerRequestRouter } from './server.ts';
import { computeServerResponse, ServerAdminBearerTokenChecker, ServerRequestOptionsProvider, ServerRequestRouter } from './server.ts';
import { ensureDir, dirname } from './deps_cli.ts';
import { MINIPUB_VERSION } from './version.ts';
import { computeValidateAdminToken } from './rpc/manage_admin_token.ts';

export const serverDescription = 'Starts a local Minipub server';

Expand All @@ -34,8 +35,18 @@ export async function server(_args: (string | number)[], options: Record<string,

const handler = async (request: Request, connInfo: ConnInfo): Promise<Response> => {

const computeRequestIp = () => {
const rt = connInfo.remoteAddr.transport === 'tcp' ? connInfo.remoteAddr.hostname : '<unknown>';
const cfConnectingIp = request.headers.get('cf-connecting-ip');
if (cfConnectingIp && rt === '127.0.0.1') {
// cloudflared tunnel
return cfConnectingIp;
}
return rt;
};

const optionsProvider: ServerRequestOptionsProvider = () => {
const requestIp = connInfo.remoteAddr.transport === 'tcp' ? connInfo.remoteAddr.hostname : '<unknown>';
const requestIp = computeRequestIp();
return Promise.resolve({ origin, adminIp, adminPublicKey, requestIp });
};

Expand All @@ -52,7 +63,13 @@ export async function server(_args: (string | number)[], options: Record<string,
const webfinger = matchWebfinger(method, pathname, searchParams); if (webfinger) return await computeWebfinger(webfinger.username, webfinger.domain, origin, storage);

};
return await computeServerResponse(request, optionsProvider, router);

const adminTokenChecker: ServerAdminBearerTokenChecker = async token => {
const { valid } = await computeValidateAdminToken({ kind: 'validate-admin-token', token }, storage);
return valid;
};

return await computeServerResponse(request, optionsProvider, router, adminTokenChecker);
};

console.log(`Local server: http://localhost:${port}, assuming public access at ${origin}`);
Expand Down
2 changes: 1 addition & 1 deletion src/cli_undo_like.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export async function undoLike(args: (string | number)[], options: Record<string
kind: 'undo-like',
activityUuid,
};
await sendRpc(req, origin, privateKey);
await sendRpc(req, origin, { privateKey });
}

//
Expand Down
2 changes: 1 addition & 1 deletion src/cli_update_note.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export async function updateNote(args: (string | number)[], options: Record<stri
objectUuid,
content: { lang: contentLang || 'und', value: content },
};
await sendRpc(req, origin, privateKey);
await sendRpc(req, origin, { privateKey });
}

//
Expand Down
2 changes: 1 addition & 1 deletion src/cli_update_user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export async function updateUser(args: (string | number)[], options: Record<stri
url,
icon,
};
await sendRpc(req, origin, privateKey);
await sendRpc(req, origin, { privateKey });
}

//
Expand Down
5 changes: 3 additions & 2 deletions src/endpoints/rpc_endpoint.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { checkCreateNoteRequest, checkCreateUserRequest, checkDeleteFromStorageRequest, checkDeleteNoteRequest, checkFederateActivityRequest, checkGenerateAdminTokenRequest, checkLikeObjectRequest, checkRevokeAdminTokenRequest, checkUndoLikeRequest, checkUpdateNoteRequest, checkUpdateUserRequest } from '../rpc_model.ts';
import { checkCreateNoteRequest, checkCreateUserRequest, checkDeleteFromStorageRequest, checkDeleteNoteRequest, checkFederateActivityRequest, checkGenerateAdminTokenRequest, checkLikeObjectRequest, checkRevokeAdminTokenRequest, checkUndoLikeRequest, checkUpdateNoteRequest, checkUpdateUserRequest, checkValidateAdminTokenRequest } from '../rpc_model.ts';
import { BackendStorage } from '../storage.ts';
import { computeCreateUser } from '../rpc/create_user.ts';
import { computeUpdateUser } from '../rpc/update_user.ts';
Expand All @@ -11,7 +11,7 @@ import { computeLikeObject } from '../rpc/like_object.ts';
import { computeUndoLike } from '../rpc/undo_like.ts';
import { computeUpdateNote } from '../rpc/update_note.ts';
import { computeDeleteNote } from '../rpc/delete_note.ts';
import { computeGenerateAdminToken, computeRevokeAdminToken } from '../rpc/manage_admin_token.ts';
import { computeGenerateAdminToken, computeRevokeAdminToken, computeValidateAdminToken } from '../rpc/manage_admin_token.ts';

export const matchRpc = (method: string, pathname: string) => method === 'POST' && pathname === '/rpc';

Expand All @@ -31,6 +31,7 @@ export async function computeRpc(request: { json(): Promise<unknown>; }, origin:
if (kind === 'undo-like' && checkUndoLikeRequest(body)) return await computeUndoLike(body, origin, storage);
if (kind === 'generate-admin-token' && checkGenerateAdminTokenRequest(body)) return await computeGenerateAdminToken(body, storage);
if (kind === 'revoke-admin-token' && checkRevokeAdminTokenRequest(body)) return await computeRevokeAdminToken(body, storage);
if (kind === 'validate-admin-token' && checkValidateAdminTokenRequest(body)) return await computeValidateAdminToken(body, storage);
throw new Error(`computeRpc: Unable to parse ${JSON.stringify(body)}`);
}
return Responses.rpc(await computeRpcResponse());
Expand Down
10 changes: 8 additions & 2 deletions src/rpc/manage_admin_token.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { encodeAscii85 } from '../deps.ts';
import { GenerateAdminTokenRequest, GenerateAdminTokenResponse, RevokeAdminTokenRequest, RevokeAdminTokenResponse } from '../rpc_model.ts';
import { BackendStorage } from '../storage.ts';
import { GenerateAdminTokenRequest, GenerateAdminTokenResponse, RevokeAdminTokenRequest, RevokeAdminTokenResponse, ValidateAdminTokenRequest, ValidateAdminTokenResponse } from '../rpc_model.ts';
import { BackendStorage, getRecord } from '../storage.ts';

export async function computeGenerateAdminToken(_req: GenerateAdminTokenRequest, storage: BackendStorage): Promise<GenerateAdminTokenResponse> {
const created = new Date().toISOString();
Expand All @@ -14,6 +14,12 @@ export async function computeRevokeAdminToken(_req: RevokeAdminTokenRequest, sto
return { kind: 'revoke-admin-token', existed };
}

export async function computeValidateAdminToken(req: ValidateAdminTokenRequest, storage: BackendStorage): Promise<ValidateAdminTokenResponse> {
const { token } = await storage.transaction(async txn => await getRecord(txn, 'token', 'admin')) || {};
const valid = req.token === token;
return { kind: 'validate-admin-token', valid };
}

//

function generateToken(): string {
Expand Down
21 changes: 21 additions & 0 deletions src/rpc_model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export type RpcRequest = CreateUserRequest
| UndoLikeRequest
| GenerateAdminTokenRequest
| RevokeAdminTokenRequest
| ValidateAdminTokenRequest
;

export type RpcResponse = CreateUserResponse
Expand All @@ -30,6 +31,7 @@ export type RpcResponse = CreateUserResponse
| UndoLikeResponse
| GenerateAdminTokenResponse
| RevokeAdminTokenResponse
| ValidateAdminTokenResponse
;

// validation
Expand Down Expand Up @@ -380,3 +382,22 @@ export interface RevokeAdminTokenResponse {
readonly kind: 'revoke-admin-token';
readonly existed: boolean;
}

// validate admin token

export interface ValidateAdminTokenRequest {
readonly kind: 'validate-admin-token';
readonly token: string;
}

export function checkValidateAdminTokenRequest(obj: any): obj is ValidateAdminTokenRequest {
return isStringRecord(obj)
&& check('kind', obj.kind, v => v === 'validate-admin-token')
&& check('token', obj.token, v => typeof v === 'string')
;
}

export interface ValidateAdminTokenResponse {
readonly kind: 'validate-admin-token';
readonly valid: boolean;
}
37 changes: 26 additions & 11 deletions src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,17 @@ export type ServerRequestOptionsProvider = () => Promise<ServerRequestOptions>;

export type ServerRequestRouterOptions = { isRpc: boolean, method: string, pathname: string, searchParams: URLSearchParams, headers: Headers, bodyText: string | undefined, canonicalUrl: string, fetcher: Fetcher };
export type ServerRequestRouter = (opts: ServerRequestRouterOptions) => Promise<Response | undefined>;
export type ServerAdminBearerTokenChecker = (bearerToken: string, origin: string) => Promise<boolean>;

export async function computeServerResponse(request: Request, optionsProvider: ServerRequestOptionsProvider, router: ServerRequestRouter): Promise<Response> {
const response = await computeResponse(request, optionsProvider, router);
export async function computeServerResponse(request: Request, optionsProvider: ServerRequestOptionsProvider, router: ServerRequestRouter, adminBearerTokenChecker: ServerAdminBearerTokenChecker): Promise<Response> {
const response = await computeResponse(request, optionsProvider, router, adminBearerTokenChecker);
console.log(`${response.status} response, content-type=${response.headers.get('content-type')}`);
return response;
}

//

async function computeResponse(request: Request, optionsProvider: ServerRequestOptionsProvider, router: ServerRequestRouter): Promise<Response> {
async function computeResponse(request: Request, optionsProvider: ServerRequestOptionsProvider, router: ServerRequestRouter, adminBearerTokenChecker: ServerAdminBearerTokenChecker): Promise<Response> {
const { url, method, headers } = request;
const urlObj = new URL(url);
const { pathname, searchParams } = urlObj;
Expand All @@ -42,16 +43,30 @@ async function computeResponse(request: Request, optionsProvider: ServerRequestO
console.log('canonicalUrl', canonicalUrl);
}

const isRpc = whitelisted && !!bodyText && matchRpc(method, pathname);
const isRpc = matchRpc(method, pathname);
if (isRpc) {
if (!whitelisted || !bodyText) return Responses.notFound();

// auth is required (admin)
// check http signature
const publicKeyProvider = (keyId: string) => {
if (keyId !== 'admin') throw new Error(`Unsupported keyId: ${keyId}`);
return Promise.resolve(adminPublicKey);
};
const { diffMillis } = await validateHttpSignature({ method, url: request.url, body: bodyText, headers: request.headers, publicKeyProvider });
console.log(`admin request sent ${diffMillis} millis ago`);
const authorization = request.headers.get('authorization');
if (authorization) {
// check for admin bearer token
const [ _, bearerToken ] = /^Bearer\s+(.*?)$/.exec(authorization) || [];
if (!bearerToken) throw new Error(`No authorization bearer token`);
const authorized = await adminBearerTokenChecker(bearerToken, origin);
if (!authorized) {
throw new Error(`Bad authorization bearer token`);
}
console.log(`bearer-token admin request`);
} else {
// check http signature
const publicKeyProvider = (keyId: string) => {
if (keyId !== 'admin') throw new Error(`Unsupported keyId: ${keyId}`);
return Promise.resolve(adminPublicKey);
};
const { diffMillis } = await validateHttpSignature({ method, url: request.url, body: bodyText, headers: request.headers, publicKeyProvider });
console.log(`signed admin request sent ${diffMillis} millis ago`);
}
}

const response = await router({ isRpc, method, pathname, searchParams, headers, bodyText, canonicalUrl, fetcher });
Expand Down

0 comments on commit 3af2084

Please sign in to comment.