implement jwt
This commit is contained in:
parent
d6bfd50173
commit
221af7d0c1
75
src/JWT.ts
Normal file
75
src/JWT.ts
Normal file
@ -0,0 +1,75 @@
|
||||
import { hmac } from "https://deno.land/x/hmac@v2.0.1/mod.ts";
|
||||
|
||||
function trimBase64(val: string | Uint8Array) {
|
||||
if (val instanceof Uint8Array) {
|
||||
val = new TextDecoder().decode(val);
|
||||
}
|
||||
|
||||
while (val.endsWith('=')) {
|
||||
val = val.substring(0, val.length - 1);
|
||||
}
|
||||
|
||||
return val.replaceAll('+', '-').replaceAll('/', '_');
|
||||
}
|
||||
function toBase64(val: string) {
|
||||
val = btoa(val);
|
||||
|
||||
while (val.endsWith('=')) {
|
||||
val = val.substring(0, val.length - 1);
|
||||
}
|
||||
|
||||
return val.replaceAll('+', '-').replaceAll('/', '_');
|
||||
}
|
||||
function fromBase64(val: string) {
|
||||
return atob(val.replaceAll('-', '+').replaceAll('_', '/'));
|
||||
}
|
||||
|
||||
export interface JWTPayload {
|
||||
iss?: string;
|
||||
sub?: string;
|
||||
aud?: string;
|
||||
exp?: number;
|
||||
nbf?: number;
|
||||
iat?: number;
|
||||
jti?: string;
|
||||
// deno-lint-ignore no-explicit-any
|
||||
[prop: string]: any;
|
||||
}
|
||||
|
||||
export default {
|
||||
encode(payload: JWTPayload | string, key: string) {
|
||||
if (typeof payload === 'string') return payload;
|
||||
const rawHeader = JSON.stringify({ alg: "HS256", typ: "JWT" });
|
||||
const rawPayload = JSON.stringify(payload);
|
||||
const data = toBase64(rawHeader) + '.' + toBase64(rawPayload);
|
||||
const rawSignature = trimBase64(hmac('sha256', key, data, 'utf8', 'base64'));
|
||||
return data + '.' + rawSignature;
|
||||
},
|
||||
decode(jwt: string | JWTPayload, key?: string) {
|
||||
if (typeof jwt === 'object') return jwt;
|
||||
const segments = jwt.split('.');
|
||||
if (segments.length != 3) throw new Error("Expected jwt to have exactly 2 dots.");
|
||||
const [ rawHeader, rawPayload, givenSig ] = segments;
|
||||
const data = rawHeader + '.' + rawPayload;
|
||||
|
||||
if (key != undefined) {
|
||||
if (JSON.parse(fromBase64(rawHeader))?.alg != "HS256") return undefined;
|
||||
const actualSig = trimBase64(hmac('sha256', key, data, 'utf8', 'base64')) as string;
|
||||
if (givenSig != actualSig) return undefined;
|
||||
}
|
||||
|
||||
return JSON.parse(fromBase64(rawPayload)) as JWTPayload;
|
||||
},
|
||||
validate(j: string | JWTPayload, key: string) {
|
||||
if (typeof j === 'object') {
|
||||
j = this.encode(j, key);
|
||||
}
|
||||
const segments = j.split('.');
|
||||
if (segments.length != 3) throw new Error("Expected jwt to have exactly 2 dots.");
|
||||
const [ header, payload, givenSig ] = segments;
|
||||
const data = header + '.' + payload;
|
||||
const actualSig = trimBase64(hmac('sha256', key, data, 'utf8', 'base64')) as string;
|
||||
|
||||
return givenSig != actualSig;
|
||||
}
|
||||
}
|
21
src/index.ts
21
src/index.ts
@ -1,22 +1,5 @@
|
||||
import { Collection, MongoClient } from "https://deno.land/x/mongo@v0.31.2/mod.ts";
|
||||
import UserRouter from "./routers/UserRouter.ts";
|
||||
import User from "./models/User.ts";
|
||||
import Response from "./server/RestResponse.ts";
|
||||
import Router, { rest, route } from "./server/Router.ts";
|
||||
|
||||
class RootRouter extends Router {
|
||||
@route('users/*') users: UserRouter;
|
||||
|
||||
@rest('*', '*')
|
||||
default() {
|
||||
return new Response().body(new Blob(['Page not found :/'])).status(404);
|
||||
}
|
||||
|
||||
constructor(salt: string, users: Collection<User>) {
|
||||
super();
|
||||
this.users = new UserRouter(salt, users);
|
||||
}
|
||||
}
|
||||
import { MongoClient } from "https://deno.land/x/mongo@v0.31.2/mod.ts";
|
||||
import { RootRouter } from "./routers/RootRouter.ts";
|
||||
|
||||
export default async function clonegur() {
|
||||
const salt = new TextDecoder().decode(await Deno.readFile('keys/salt.txt'));
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { HttpError } from "../server/HttpError.ts";
|
||||
import HttpError from "../server/HttpError.ts";
|
||||
import RestRequest from "../server/RestRequest.ts";
|
||||
import RestResponse from "../server/RestResponse.ts";
|
||||
import Router from "../server/Router.ts";
|
||||
|
20
src/routers/RootRouter.ts
Normal file
20
src/routers/RootRouter.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { Collection } from "https://deno.land/x/mongo@v0.31.2/mod.ts";
|
||||
import UserRouter from "../routers/UserRouter.ts";
|
||||
import User from "../models/User.ts";
|
||||
import Response from "../server/RestResponse.ts";
|
||||
import Router from "../server/Router.ts";
|
||||
import { rest, route } from "../server/decorators.ts";
|
||||
|
||||
export class RootRouter extends Router {
|
||||
@route('users/*') users;
|
||||
|
||||
@rest('*', '*')
|
||||
default() {
|
||||
return new Response().body(new Blob(['Page not found :/'])).status(404);
|
||||
}
|
||||
|
||||
constructor(salt: string, users: Collection<User>) {
|
||||
super();
|
||||
this.users = new UserRouter(salt, users);
|
||||
}
|
||||
}
|
@ -1,9 +1,10 @@
|
||||
import { Collection } from "https://deno.land/x/mongo@v0.31.2/mod.ts";
|
||||
import * as bcrypt from "https://deno.land/x/bcrypt@v0.4.1/mod.ts";
|
||||
|
||||
import { body, rest, schema } from "../server/decorators.ts";
|
||||
import HttpError from "../server/HttpError.ts";
|
||||
import User from "../models/User.ts";
|
||||
import { body, rest, schema } from "../server/decorators.ts";
|
||||
import AppRouter from "./AppRouter.ts";
|
||||
import * as bcrypt from "https://deno.land/x/bcrypt@v0.4.1/mod.ts";
|
||||
|
||||
interface SignupRequest {
|
||||
username: string;
|
||||
@ -11,7 +12,7 @@ interface SignupRequest {
|
||||
}
|
||||
|
||||
export default class UserRouter extends AppRouter {
|
||||
@rest('GET', '/')
|
||||
@rest('GET', '/:username')
|
||||
async get(@schema('string') username: string) {
|
||||
const res = await this.users.findOne({ username });
|
||||
|
||||
|
@ -13,7 +13,7 @@ export interface RestOptions {
|
||||
route?: string;
|
||||
}
|
||||
|
||||
export type ProcessFunc = (req: RestRequest, arg: unknown, name: string) => Promise<unknown> | unknown;
|
||||
export type ProcessFunc<T extends Router> = (this: T, req: RestRequest, arg: unknown, name: string) => Promise<unknown> | unknown;
|
||||
|
||||
export interface RouterHandler {
|
||||
path: string;
|
||||
@ -26,8 +26,8 @@ export function addMetaQuery<T extends Router>(target: T, ...handlers: ((r: T) =
|
||||
props.push(...handlers);
|
||||
}
|
||||
|
||||
export function makeParameterModifier(func: ProcessFunc) {
|
||||
return (target: Router, key: string, index: number) => {
|
||||
export function makeParameterModifier<T extends Router>(func: ProcessFunc<T>) {
|
||||
return (target: T, key: string & keyof T, index: number) => {
|
||||
let res = Reflect.getOwnMetadata('router:params', target, key);
|
||||
|
||||
if (res === undefined) Reflect.defineMetadata('router:params', res = [], target, key);
|
||||
|
@ -1,14 +1,15 @@
|
||||
import { makeParameterModifier } from "../Router.ts";
|
||||
import JWT from "../../JWT.ts";
|
||||
import Router, { makeParameterModifier } from "../Router.ts";
|
||||
|
||||
export type AuthType = 'raw' | 'jwt';
|
||||
|
||||
export function auth(type: AuthType = 'jwt') {
|
||||
return makeParameterModifier(req => {
|
||||
export function auth<T extends Router>(salt: (self: T) => string, type: AuthType = 'jwt') {
|
||||
return makeParameterModifier<T>(function (req) {
|
||||
let res = req.headers.authorization;
|
||||
if (typeof res !== 'string') return undefined;
|
||||
if (res.startsWith('Bearer')) res = res.substring(6).trimStart();
|
||||
|
||||
if (type === 'jwt') throw new Error('JWT is not supported.');
|
||||
if (type === 'jwt') return JWT.decode(res, salt(this));
|
||||
|
||||
return res;
|
||||
});
|
||||
|
@ -8,7 +8,7 @@ import serialize from "../serialize.ts";
|
||||
export type HttpMethod = '*' | 'GET' | 'POST' | 'CHANGE' | 'DELETE' | 'PUT' | 'UPDATE';
|
||||
|
||||
type Base<KeyT extends string> = Router & { [X in KeyT]: Function; };
|
||||
type ModArray = ([ProcessFunc, ...ProcessFunc[]] | undefined)[];
|
||||
type ModArray<T extends Router> = ([ProcessFunc<T>, ...ProcessFunc<T>[]] | undefined)[];
|
||||
|
||||
export default function rest<KeyT extends keyof T & string, T extends Base<KeyT>>(method: HttpMethod, route?: string) {
|
||||
return (target: T, key: KeyT) => {
|
||||
@ -21,14 +21,14 @@ export default function rest<KeyT extends keyof T & string, T extends Base<KeyT>
|
||||
|
||||
const params: string[] = [];
|
||||
const args: unknown[] = [];
|
||||
const allMods: ModArray = [];
|
||||
const allMods: ModArray<T> = [];
|
||||
|
||||
let signature = r[key].toString();
|
||||
signature = signature.substring(signature.indexOf('(') + 1, signature.indexOf(')'));
|
||||
params.push(...signature.split(',').map(v => v.trim()).filter(v => v !== ''));
|
||||
|
||||
for (let proto = r; proto instanceof Router; proto = Object.getPrototypeOf(proto)) {
|
||||
const data = Reflect.getOwnMetadata('router:params', proto, key) as ModArray;
|
||||
const data = Reflect.getOwnMetadata('router:params', proto, key) as ModArray<T>;
|
||||
if (!data) continue;
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
if (data[i] === undefined) continue;
|
||||
@ -44,7 +44,7 @@ export default function rest<KeyT extends keyof T & string, T extends Base<KeyT>
|
||||
let arg: unknown = req.params[param];
|
||||
|
||||
for (const mod of allMods[i] ?? []) {
|
||||
arg = await mod(req, arg, params[i]);
|
||||
arg = await mod.call(r, req, arg, params[i]);
|
||||
}
|
||||
|
||||
args.push(arg);
|
||||
|
@ -23,5 +23,5 @@ export default async function serialize(val: unknown, depth = 16): Promise<Blob>
|
||||
val = val.toString();
|
||||
}
|
||||
|
||||
return new Blob([new TextEncoder().encode(JSON.stringify(val))]);
|
||||
return new Blob([JSON.stringify(val)]);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user