session login

This commit is contained in:
monoid 2021-01-11 03:14:07 +09:00
parent d67b50edf4
commit 8b47c4b178
5 changed files with 293 additions and 98 deletions

View File

@ -1,8 +1,8 @@
import React, { createContext, useRef, useState } from 'react'; import React, { createContext, useEffect, useRef, useState } from 'react';
import ReactDom from 'react-dom'; import ReactDom from 'react-dom';
import {BrowserRouter, Redirect, Route, Switch as RouterSwitch} from 'react-router-dom'; import {BrowserRouter, Redirect, Route, Switch as RouterSwitch} from 'react-router-dom';
import { Gallery, ContentAbout, LoginPage, NotFoundPage} from './page/mod'; import { Gallery, ContentAbout, LoginPage, NotFoundPage} from './page/mod';
import {UserContext} from './state'; import {getInitialValue, UserContext} from './state';
import './css/style.css'; import './css/style.css';
@ -10,6 +10,14 @@ const FooProfile = ()=><div>test profile</div>;
const App = () => { const App = () => {
const [user,setUser] = useState(""); const [user,setUser] = useState("");
const [userPermission,setUserPermission] = useState<string[]>([]); const [userPermission,setUserPermission] = useState<string[]>([]);
(async ()=>{
const {username,permission} = await getInitialValue();
if(username !== user){
setUser(username);
setUserPermission(permission);
}
})();
//useEffect(()=>{});
return ( return (
<UserContext.Provider value={{ <UserContext.Provider value={{
username:user, username:user,

View File

@ -47,7 +47,7 @@ export const LoginPage = ()=>{
<div style={{minHeight:theme.spacing(2)}}></div> <div style={{minHeight:theme.spacing(2)}}></div>
<form style={{display:'flex', flexFlow:'column', alignItems:'stretch'}}> <form style={{display:'flex', flexFlow:'column', alignItems:'stretch'}}>
<TextField label="username" onChange={(e)=>setUserLoginInfo({...userLoginInfo,username:e.target.value ?? "",})}></TextField> <TextField label="username" onChange={(e)=>setUserLoginInfo({...userLoginInfo,username:e.target.value ?? "",})}></TextField>
<TextField label="password" type="password" <TextField label="password" type="password" onKeyDown={(e)=>{if(e.key === 'Enter') doLogin();}}
onChange={(e)=>setUserLoginInfo({...userLoginInfo,password:e.target.value ?? "",})}/> onChange={(e)=>setUserLoginInfo({...userLoginInfo,password:e.target.value ?? "",})}/>
<div style={{minHeight:theme.spacing(2)}}></div> <div style={{minHeight:theme.spacing(2)}}></div>
<div style={{display:'flex'}}> <div style={{display:'flex'}}>

View File

@ -1,8 +1,47 @@
import React, { createContext, useRef, useState } from 'react'; import React, { createContext, useRef, useState } from 'react';
export const UserContext = createContext({ export const UserContext = createContext({
username:"", username: "",
permission:["openContent"], permission: [] as string[],
setUsername:(s:string)=>{}, setUsername: (s: string) => { },
setPermission:(permission:string[])=>{} setPermission: (permission: string[]) => { }
}); });
type LoginLocalStorage = {
username: string,
permission: string[],
refreshExpired: number
};
let localObj: LoginLocalStorage|null = null;
export const getInitialValue = async () => {
if(localObj === null){
const storagestr = window.localStorage.getItem("UserLoginContext") as string | null;
const storage = storagestr !== null ? JSON.parse(storagestr) as LoginLocalStorage | null : null;
localObj = storage;
}
if (localObj !== null && localObj.refreshExpired > Math.floor(Date.now() / 1000)) {
return {
username: localObj.username,
permission: localObj.permission,
}
}
const res = await fetch('/user/refresh', {
method: 'POST',
});
if (res.status !== 200) throw new Error("Maybe Network Error")
const r = await res.json() as LoginLocalStorage & { refresh: boolean };
if (r.refresh) {
localObj = {
username: r.username,
permission: r.permission,
refreshExpired: r.refreshExpired
}
window.localStorage.setItem("UserLoginContext", JSON.stringify(localObj));
}
return {
username: r.username,
permission: r.permission
}
}

View File

@ -1,106 +1,253 @@
import {sign, decode, verify} from 'jsonwebtoken'; import { decode, sign, TokenExpiredError, verify } from "jsonwebtoken";
import Koa from 'koa'; import Koa from "koa";
import Router from 'koa-router'; import Router from "koa-router";
import { sendError } from './route/error_handler'; import { sendError } from "./route/error_handler";
import Knex from 'knex' import Knex from "knex";
import { createKnexUserController } from './db/mod'; import { createKnexUserController } from "./db/mod";
import { request } from 'http'; import { request } from "http";
import { get_setting } from './setting'; import { get_setting } from "./setting";
import { IUser } from './model/mod'; import { IUser, UserAccessor } from "./model/mod";
type PayloadInfo = { type PayloadInfo = {
username:string, username: string;
permission:string[] permission: string[];
}
export type UserState = {
user:PayloadInfo
}; };
const isUserState = (obj:object|string):obj is PayloadInfo =>{ export type UserState = {
if(typeof obj ==="string") return false; user: PayloadInfo;
return 'username' in obj && 'permission' in obj && (obj as {permission:unknown}).permission instanceof Array; };
}
export const loginTokenName = 'access_token' 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 getAdminCookieValue = ()=>{ 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;
};
const setToken = (
ctx: Koa.Context,
token_name: string,
token_payload: string|null,
expiredtime: number,
) => {
const setting = get_setting(); const setting = get_setting();
const secretKey = setting.jwt_secretkey; if(token_payload === null && !!!ctx.cookies.get(token_name)){
return sign({ return;
username: "admin", }
permission: [], ctx.cookies.set(token_name, token_payload, {
},secretKey,{expiresIn:'3d'}); httpOnly: true,
} secure: !setting.localmode,
sameSite: "strict",
export const createLoginMiddleware = (knex: Knex)=>{ expires: new Date(Date.now() + expiredtime),
});
};
export const createLoginMiddleware = (knex: Knex) => {
const userController = createKnexUserController(knex); const userController = createKnexUserController(knex);
return async (ctx: Koa.Context,next: Koa.Next)=>{ return async (ctx: Koa.Context, next: Koa.Next) => {
const setting = get_setting(); const setting = get_setting();
const secretKey = setting.jwt_secretkey; const secretKey = setting.jwt_secretkey;
const body = ctx.request.body; const body = ctx.request.body;
if(!('username' in body)||!('password' in body)){ //check format
return sendError(400,"invalid form : username or password is not found in query."); if (!("username" in body) || !("password" in body)) {
return sendError(
400,
"invalid form : username or password is not found in query.",
);
} }
const username = body['username']; const username = body["username"];
const password = body['password']; const password = body["password"];
if(typeof username !== "string" || typeof password !== "string"){ //check type
return sendError(400,"invalid form : username or password is not string") if (typeof username !== "string" || typeof password !== "string") {
return sendError(
400,
"invalid form : username or password is not string",
);
} }
if(setting.forbid_remote_admin_login && username === "admin"){ //if admin login is forbidden?
return sendError(403,"forbid remote admin login"); if (username === "admin" && setting.forbid_remote_admin_login) {
return sendError(403, "forbiden remote admin login");
} }
const user = await userController.findUser(username); const user = await userController.findUser(username);
if(user === undefined){ //username not exist
return sendError(401,"not authorized"); if (user === undefined) return sendError(401, "not authorized");
} //password not matched
if(!user.password.check_password(password)){ if (!user.password.check_password(password)) {
return sendError(401,"not authorized"); return sendError(401, "not authorized");
} }
//create token
const userPermission = await user.get_permissions(); const userPermission = await user.get_permissions();
const payload = sign({ 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, username: user.username,
permission: userPermission permission: userPermission,
},secretKey,{expiresIn:'3h'}); refreshExpired: (Math.floor(Date.now() / 1000) + refreshExpiredTime),
ctx.cookies.set(loginTokenName,payload,{httpOnly:true, secure: !setting.localmode,sameSite:'strict'}); };
ctx.body = {ok:true, username: user.username, permission: userPermission}
console.log(`${username} logined`); console.log(`${username} logined`);
return; return;
}; };
}; };
export const LogoutMiddleware = (ctx:Koa.Context,next:Koa.Next)=>{ export const LogoutMiddleware = (ctx: Koa.Context, next: Koa.Next) => {
ctx.cookies.set(loginTokenName,undefined); ctx.cookies.set(accessTokenName, null);
ctx.body = {ok:true}; ctx.cookies.set(refreshTokenName,null);
ctx.body = { ok: true };
return; return;
} };
export const UserMiddleWare = async (ctx:Koa.ParameterizedContext<UserState>,next:Koa.Next)=>{ export const createUserMiddleWare = (knex: Knex) =>
const secretKey = get_setting().jwt_secretkey; async (ctx: Koa.ParameterizedContext<UserState>, next: Koa.Next) => {
const payload = ctx.cookies.get(loginTokenName); const userController = createKnexUserController(knex);
const refreshToken = refreshTokenHandler(userController);
const setting = get_setting(); const setting = get_setting();
if(payload == undefined){ const setGuest = async () => {
ctx.state['user'] = {username:"", setToken(ctx, accessTokenName, null, 0);
permission:setting.guest}; setToken(ctx, refreshTokenName, null, 0);
ctx.state["user"] = { username: "", permission: setting.guest };
return await next(); return await next();
};
return await refreshToken(ctx,setGuest,next);
};
const refreshTokenHandler = (cntr:UserAccessor) => async (ctx:Koa.Context,fail: Koa.Next,next: Koa.Next)=>{
const payload = ctx.cookies.get(accessTokenName);
const setting = get_setting();
const secretKey = setting.jwt_secretkey;
const checkRefreshAndUpdate = async () => {
const payload2 = ctx.cookies.get(refreshTokenName);
if (payload2 === undefined) return await fail(); // refresh token doesn't exist
else {
try {
const o = verify(payload2, 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");
return;
} }
const o = verify(payload,secretKey); } catch (e) {
if(isUserState(o)){ if (e instanceof TokenExpiredError) { // refresh token is expired.
return await fail();
} else throw e;
}
}
return await next();
};
if (payload == undefined) {
return await checkRefreshAndUpdate();
}
try {
const o = verify(payload, secretKey);
if (isUserState(o)) {
ctx.state.user = o; ctx.state.user = o;
return await next(); return await next();
} } else {
else{
console.error("invalid token detected"); console.error("invalid token detected");
throw new Error("token form invalid");
}
} catch (e) {
if (e instanceof TokenExpiredError) {
return await checkRefreshAndUpdate();
} else throw e;
} }
} }
export const createRefreshTokenMiddleware = (knex:Knex)=> async (ctx:Koa.Context,next:Koa.Next)=>{
export const getAdmin = async(knex : Knex)=>{ const cntr= createKnexUserController(knex);
const handler = refreshTokenHandler(cntr);
const fail = async ()=>{
const user = ctx.state.user as PayloadInfo;
ctx.body = {
refresh:false,
...user
}
ctx.type = 'json';
};
const success = async ()=>{
const user = ctx.state.user as PayloadInfo;
ctx.body = {
...user,
refresh:true,
refreshExpired: Math.floor(Date.now()/1000 + accessExpiredTime),
}
ctx.type = 'json';
}
await handler(ctx,fail,success);
}
export const getAdmin = async (knex: Knex) => {
const cntr = createKnexUserController(knex); const cntr = createKnexUserController(knex);
const admin = await cntr.findUser("admin"); const admin = await cntr.findUser("admin");
if(admin === undefined){ if (admin === undefined) {
throw new Error("initial process failed!");//??? throw new Error("initial process failed!"); //???
} }
return admin; return admin;
} };
export const isAdminFirst = (admin: IUser)=>{ export const isAdminFirst = (admin: IUser) => {
return admin.password.hash === "unchecked" && admin.password.salt === "unchecked"; return admin.password.hash === "unchecked" &&
} admin.password.salt === "unchecked";
};

View File

@ -11,7 +11,7 @@ import { createKnexContentsAccessor } from './db/contents';
import bodyparser from 'koa-bodyparser'; import bodyparser from 'koa-bodyparser';
import {error_handler} from './route/error_handler'; import {error_handler} from './route/error_handler';
import {UserMiddleWare,createLoginMiddleware, isAdminFirst, getAdmin, LogoutMiddleware} from './login'; import {createUserMiddleWare,createLoginMiddleware, isAdminFirst, getAdmin, LogoutMiddleware, createRefreshTokenMiddleware} from './login';
import {createInterface as createReadlineInterface} from 'readline'; import {createInterface as createReadlineInterface} from 'readline';
@ -35,7 +35,7 @@ export async function create_server(){
let app = new Koa(); let app = new Koa();
app.use(bodyparser()); app.use(bodyparser());
app.use(error_handler); app.use(error_handler);
app.use(UserMiddleWare); app.use(createUserMiddleWare(db));
//app.use(ctx=>{ctx.state['setting'] = settings}); //app.use(ctx=>{ctx.state['setting'] = settings});
const index_html = readFileSync("index.html"); const index_html = readFileSync("index.html");
@ -67,12 +67,13 @@ export async function create_server(){
router.post('/user/login',createLoginMiddleware(db)); router.post('/user/login',createLoginMiddleware(db));
router.post('/user/logout',LogoutMiddleware); router.post('/user/logout',LogoutMiddleware);
router.post('/user/refresh',createRefreshTokenMiddleware(db));
if(setting.mode == "development"){ if(setting.mode == "development"){
let mm_count = 0; let mm_count = 0;
app.use(async (ctx,next)=>{ app.use(async (ctx,next)=>{
console.log(`==========================${mm_count++}`); console.log(`==========================${mm_count++}`);
const fromClient = ctx.state['user'] === undefined ? ctx.ip : ctx.state['user'].username; const fromClient = ctx.state['user'].username === "" ? ctx.ip : ctx.state['user'].username;
console.log(`${fromClient} : ${ctx.method} ${ctx.url}`); console.log(`${fromClient} : ${ctx.method} ${ctx.url}`);
await next(); await next();
//console.log(`404`); //console.log(`404`);