implement jwt

This commit is contained in:
TopchetoEU 2023-06-26 18:07:25 +03:00
parent d6bfd50173
commit 221af7d0c1
No known key found for this signature in database
GPG Key ID: 24E57B2E9C61AD19
9 changed files with 115 additions and 35 deletions

75
src/JWT.ts Normal file
View 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;
}
}

View File

@ -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'));

View File

@ -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
View 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);
}
}

View File

@ -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 });

View File

@ -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);

View File

@ -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;
});

View File

@ -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);

View File

@ -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)]);
}