import { decode, sign, TokenExpiredError, verify } from "jsonwebtoken"; import Koa from "koa"; import Router from "koa-router"; import { sendError } from "./route/error_handler"; import Knex from "knex"; import { createKnexUserController } from "./db/mod"; import { request } from "http"; import { get_setting } from "./SettingConfig"; import { IUser, UserAccessor } from "./model/mod"; type PayloadInfo = { username: string; permission: string[]; }; export type UserState = { user: PayloadInfo; }; const isUserState = (obj: object | string): obj is PayloadInfo => { if (typeof obj === "string") return false; return "username" in obj && "permission" in obj && (obj as { permission: unknown }).permission instanceof Array; }; type RefreshPayloadInfo = { username: string }; const isRefreshToken = (obj: object | string): obj is RefreshPayloadInfo => { if (typeof obj === "string") return false; return "username" in obj && typeof (obj as { username: unknown }).username === "string"; }; export const accessTokenName = "access_token"; export const refreshTokenName = "refresh_token"; const accessExpiredTime = 60 * 60; //1 hour const refreshExpiredTime = 60 * 60 * 24 * 14; // 14 day; export const getAdminAccessTokenValue = () => { const { jwt_secretkey } = get_setting(); return publishAccessToken(jwt_secretkey, "admin", [], accessExpiredTime); }; export const getAdminRefreshTokenValue = () => { const { jwt_secretkey } = get_setting(); return publishRefreshToken(jwt_secretkey, "admin", refreshExpiredTime); }; const publishAccessToken = ( secretKey: string, username: string, permission: string[], expiredtime: number, ) => { const payload = sign( { username: username, permission: permission, }, secretKey, { expiresIn: expiredtime }, ); return payload; }; const publishRefreshToken = ( secretKey: string, username: string, expiredtime: number, ) => { const payload = sign( { username: username }, secretKey, { expiresIn: expiredtime }, ); return payload; }; function setToken( ctx: Koa.Context, token_name: string, token_payload: string | null, expiredtime: number, ) { const setting = get_setting(); if (token_payload === null && !!!ctx.cookies.get(token_name)) { return; } ctx.cookies.set(token_name, token_payload, { httpOnly: true, secure: setting.secure, sameSite: "strict", expires: new Date(Date.now() + expiredtime * 1000), }); }; export const createLoginMiddleware = (userController: UserAccessor) => async (ctx: Koa.Context, _next: Koa.Next) => { const setting = get_setting(); const secretKey = setting.jwt_secretkey; const body = ctx.request.body; //check format if (typeof body == "string" || !("username" in body) || !("password" in body)) { return sendError( 400, "invalid form : username or password is not found in query.", ); } const username = body["username"]; const password = body["password"]; //check type if (typeof username !== "string" || typeof password !== "string") { return sendError( 400, "invalid form : username or password is not string", ); } //if admin login is forbidden? if (username === "admin" && setting.forbid_remote_admin_login) { return sendError(403, "forbidden remote admin login"); } const user = await userController.findUser(username); //username not exist if (user === undefined) return sendError(401, "not authorized"); //password not matched if (!user.password.check_password(password)) { return sendError(401, "not authorized"); } //create token const userPermission = await user.get_permissions(); const payload = publishAccessToken( secretKey, user.username, userPermission, accessExpiredTime, ); const payload2 = publishRefreshToken( secretKey, user.username, refreshExpiredTime, ); setToken(ctx, accessTokenName, payload, accessExpiredTime); setToken(ctx, refreshTokenName, payload2, refreshExpiredTime); ctx.body = { username: user.username, permission: userPermission, accessExpired: (Math.floor(Date.now() / 1000) + accessExpiredTime), }; console.log(`${username} logined`); return; }; export const LogoutMiddleware = (ctx: Koa.Context, next: Koa.Next) => { const setting = get_setting() ctx.cookies.set(accessTokenName, null); ctx.cookies.set(refreshTokenName, null); ctx.body = { ok: true, username: "", permission: setting.guest }; return; }; export const createUserMiddleWare = (userController: UserAccessor) => async (ctx: Koa.ParameterizedContext, next: Koa.Next) => { const refreshToken = refreshTokenHandler(userController); const setting = get_setting(); const setGuest = async () => { setToken(ctx, accessTokenName, null, 0); setToken(ctx, refreshTokenName, null, 0); ctx.state["user"] = { username: "", permission: setting.guest }; return await next(); }; return await refreshToken(ctx, setGuest, next); }; const refreshTokenHandler = (cntr: UserAccessor) => async (ctx: Koa.Context, fail: Koa.Next, next: Koa.Next) => { const accessPayload = ctx.cookies.get(accessTokenName); const setting = get_setting(); const secretKey = setting.jwt_secretkey; if (accessPayload == undefined) { return await checkRefreshAndUpdate(); } try { const o = verify(accessPayload, secretKey); if (isUserState(o)) { ctx.state.user = o; return await next(); } else { console.error("invalid token detected"); throw new Error("token form invalid"); } } catch (e) { if (e instanceof TokenExpiredError) { return await checkRefreshAndUpdate(); } else throw e; } async function checkRefreshAndUpdate() { const refreshPayload = ctx.cookies.get(refreshTokenName); if (refreshPayload === undefined) { return await fail(); // refresh token doesn't exist } else { try { const o = verify(refreshPayload, secretKey); if (isRefreshToken(o)) { const user = await cntr.findUser(o.username); if (user === undefined) return await fail(); //already non-existence user const perm = await user.get_permissions(); const payload = publishAccessToken( secretKey, user.username, perm, accessExpiredTime, ); setToken(ctx, accessTokenName, payload, accessExpiredTime); ctx.state.user = { username: o.username, permission: perm }; } else { console.error("invalid token detected"); throw new Error("token form invalid"); } } catch (e) { if (e instanceof TokenExpiredError) { // refresh token is expired. return await fail(); } else throw e; } } return await next(); }; }; export const createRefreshTokenMiddleware = (cntr: UserAccessor) => async (ctx: Koa.Context, next: Koa.Next) => { const handler = refreshTokenHandler(cntr); await handler(ctx, fail, success); async function fail() { const user = ctx.state.user as PayloadInfo; ctx.body = { refresh: false, ...user, }; ctx.type = "json"; }; async function success() { const user = ctx.state.user as PayloadInfo; ctx.body = { ...user, refresh: true, refreshExpired: Math.floor(Date.now() / 1000 + accessExpiredTime), }; ctx.type = "json"; }; }; export const resetPasswordMiddleware = (cntr: UserAccessor) => async (ctx: Koa.Context, next: Koa.Next) => { const body = ctx.request.body; if (typeof body !== "object" || !('username' in body) || !('oldpassword' in body) || !('newpassword' in body)) { return sendError(400, "request body is invalid format"); } const username = body['username']; const oldpw = body['oldpassword']; const newpw = body['newpassword']; if (typeof username !== "string" || typeof oldpw !== "string" || typeof newpw !== "string") { return sendError(400, "request body is invalid format"); } const user = await cntr.findUser(username); if (user === undefined) { return sendError(403, "not authorized"); } if (!user.password.check_password(oldpw)) { return sendError(403, "not authorized"); } user.reset_password(newpw); ctx.body = { ok: true } ctx.type = 'json'; } export function createLoginRouter(userController: UserAccessor) { let router = new Router(); router.post('/login', createLoginMiddleware(userController)); router.post('/logout', LogoutMiddleware); router.post('/refresh', createRefreshTokenMiddleware(userController)); router.post('/reset', resetPasswordMiddleware(userController)); return router; } export const getAdmin = async (cntr: UserAccessor) => { const admin = await cntr.findUser("admin"); if (admin === undefined) { throw new Error("initial process failed!"); //??? } return admin; }; export const isAdminFirst = (admin: IUser) => { return admin.password.hash === "unchecked" && admin.password.salt === "unchecked"; };