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 { MongoClient } from "https://deno.land/x/mongo@v0.31.2/mod.ts";
|
||||||
import UserRouter from "./routers/UserRouter.ts";
|
import { RootRouter } from "./routers/RootRouter.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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default async function clonegur() {
|
export default async function clonegur() {
|
||||||
const salt = new TextDecoder().decode(await Deno.readFile('keys/salt.txt'));
|
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 RestRequest from "../server/RestRequest.ts";
|
||||||
import RestResponse from "../server/RestResponse.ts";
|
import RestResponse from "../server/RestResponse.ts";
|
||||||
import Router from "../server/Router.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 { 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 HttpError from "../server/HttpError.ts";
|
||||||
import User from "../models/User.ts";
|
import User from "../models/User.ts";
|
||||||
import { body, rest, schema } from "../server/decorators.ts";
|
|
||||||
import AppRouter from "./AppRouter.ts";
|
import AppRouter from "./AppRouter.ts";
|
||||||
import * as bcrypt from "https://deno.land/x/bcrypt@v0.4.1/mod.ts";
|
|
||||||
|
|
||||||
interface SignupRequest {
|
interface SignupRequest {
|
||||||
username: string;
|
username: string;
|
||||||
@ -11,7 +12,7 @@ interface SignupRequest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default class UserRouter extends AppRouter {
|
export default class UserRouter extends AppRouter {
|
||||||
@rest('GET', '/')
|
@rest('GET', '/:username')
|
||||||
async get(@schema('string') username: string) {
|
async get(@schema('string') username: string) {
|
||||||
const res = await this.users.findOne({ username });
|
const res = await this.users.findOne({ username });
|
||||||
|
|
||||||
|
@ -13,7 +13,7 @@ export interface RestOptions {
|
|||||||
route?: string;
|
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 {
|
export interface RouterHandler {
|
||||||
path: string;
|
path: string;
|
||||||
@ -26,8 +26,8 @@ export function addMetaQuery<T extends Router>(target: T, ...handlers: ((r: T) =
|
|||||||
props.push(...handlers);
|
props.push(...handlers);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function makeParameterModifier(func: ProcessFunc) {
|
export function makeParameterModifier<T extends Router>(func: ProcessFunc<T>) {
|
||||||
return (target: Router, key: string, index: number) => {
|
return (target: T, key: string & keyof T, index: number) => {
|
||||||
let res = Reflect.getOwnMetadata('router:params', target, key);
|
let res = Reflect.getOwnMetadata('router:params', target, key);
|
||||||
|
|
||||||
if (res === undefined) Reflect.defineMetadata('router:params', res = [], 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 type AuthType = 'raw' | 'jwt';
|
||||||
|
|
||||||
export function auth(type: AuthType = 'jwt') {
|
export function auth<T extends Router>(salt: (self: T) => string, type: AuthType = 'jwt') {
|
||||||
return makeParameterModifier(req => {
|
return makeParameterModifier<T>(function (req) {
|
||||||
let res = req.headers.authorization;
|
let res = req.headers.authorization;
|
||||||
if (typeof res !== 'string') return undefined;
|
if (typeof res !== 'string') return undefined;
|
||||||
if (res.startsWith('Bearer')) res = res.substring(6).trimStart();
|
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;
|
return res;
|
||||||
});
|
});
|
||||||
|
@ -8,7 +8,7 @@ import serialize from "../serialize.ts";
|
|||||||
export type HttpMethod = '*' | 'GET' | 'POST' | 'CHANGE' | 'DELETE' | 'PUT' | 'UPDATE';
|
export type HttpMethod = '*' | 'GET' | 'POST' | 'CHANGE' | 'DELETE' | 'PUT' | 'UPDATE';
|
||||||
|
|
||||||
type Base<KeyT extends string> = Router & { [X in KeyT]: Function; };
|
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) {
|
export default function rest<KeyT extends keyof T & string, T extends Base<KeyT>>(method: HttpMethod, route?: string) {
|
||||||
return (target: T, key: KeyT) => {
|
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 params: string[] = [];
|
||||||
const args: unknown[] = [];
|
const args: unknown[] = [];
|
||||||
const allMods: ModArray = [];
|
const allMods: ModArray<T> = [];
|
||||||
|
|
||||||
let signature = r[key].toString();
|
let signature = r[key].toString();
|
||||||
signature = signature.substring(signature.indexOf('(') + 1, signature.indexOf(')'));
|
signature = signature.substring(signature.indexOf('(') + 1, signature.indexOf(')'));
|
||||||
params.push(...signature.split(',').map(v => v.trim()).filter(v => v !== ''));
|
params.push(...signature.split(',').map(v => v.trim()).filter(v => v !== ''));
|
||||||
|
|
||||||
for (let proto = r; proto instanceof Router; proto = Object.getPrototypeOf(proto)) {
|
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;
|
if (!data) continue;
|
||||||
for (let i = 0; i < data.length; i++) {
|
for (let i = 0; i < data.length; i++) {
|
||||||
if (data[i] === undefined) continue;
|
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];
|
let arg: unknown = req.params[param];
|
||||||
|
|
||||||
for (const mod of allMods[i] ?? []) {
|
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);
|
args.push(arg);
|
||||||
|
@ -23,5 +23,5 @@ export default async function serialize(val: unknown, depth = 16): Promise<Blob>
|
|||||||
val = val.toString();
|
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