Secure with jwt

This commit is contained in:
pikiou
2021-11-30 07:45:31 +01:00
parent 976fbded67
commit a84c329040
12 changed files with 224 additions and 17 deletions

View File

@@ -15,4 +15,5 @@ export default {
},
],
},
JWT_SECRET: "RblQqA6uF#msq2312bebf2FLFn4XzWQ6dttXSJwBX#?gL2JWf!",
}

View File

@@ -1,5 +1,5 @@
import path from "path"
import express from "express"
import express, { RequestHandler } from "express"
import logger from "morgan"
import compression from "compression"
import helmet from "helmet"
@@ -15,6 +15,7 @@ import devServer from "./devServer"
import ssr from "./ssr"
import certbotRouter from "../routes/certbot"
import { secure } from "./secure"
import { jeuJavListGet } from "./gsheets/jeuJav"
import { envieListGet, envieAdd } from "./gsheets/envies"
import { membreGet, membreSet } from "./gsheets/membres"
@@ -43,21 +44,23 @@ app.use(express.static(path.resolve(process.cwd(), "public")))
// Enable dev-server in development
if (__DEV__) devServer(app)
/**
* APIs
*/
// Google Sheets API
app.use(express.json())
app.get("/JeuJavListGet", jeuJavListGet)
app.get("/EnvieListGet", envieListGet)
app.get("/MembreGet", membreGet)
app.post("/MembreSet", membreSet)
app.post("/EnvieAdd", envieAdd)
// Sign in & up API
app.post("/api/user/login", loginHandler)
/**
* APIs
*/
// Google Sheets API
app.get("/JeuJavListGet", jeuJavListGet)
app.get("/EnvieListGet", envieListGet)
app.post("/EnvieAdd", envieAdd)
// Secured APIs
app.get("/MembreGet", secure as RequestHandler, membreGet)
app.post("/MembreSet", secure as RequestHandler, membreSet)
// Use React server-side rendering middleware
app.get("*", ssr)

58
src/server/secure.ts Normal file
View File

@@ -0,0 +1,58 @@
import { NextFunction, Request, Response } from "express"
import path from "path"
import { promises as fs } from "fs"
import { verify, sign } from "jsonwebtoken"
import { canonicalEmail } from "../utils/standardization"
import config from "../config"
type AuthorizedRequest = Request & { headers: { authorization: string } }
let cachedSecret: string
getSecret() // Necessary until we can make async express middleware
export function secure(request: AuthorizedRequest, response: Response, next: NextFunction): void {
if (!cachedSecret) {
response.status(408).json({
error: "Server still loading",
})
return
}
const rawToken = request.headers.authorization
const token = rawToken && rawToken.split(/\s/)[1]
verify(token, cachedSecret, (tokenError: any, decoded: any) => {
if (tokenError) {
response.status(403).json({
error: "Invalid token, please Log in first, criterion auth",
})
return
}
decoded.user.replace(/@gmailcom$/, "@gmail.com")
response.locals.jwt = decoded
next()
})
}
async function getSecret() {
if (!cachedSecret) {
const SECRET_PATH = path.resolve(process.cwd(), "access/jwt_secret.json")
try {
const secretContent = await fs.readFile(SECRET_PATH)
cachedSecret = secretContent && JSON.parse(secretContent.toString()).secret
} catch (e: any) {
cachedSecret = config.JWT_SECRET
}
}
return cachedSecret
}
export async function getJwt(email: string): Promise<string> {
const jwt = sign({ user: canonicalEmail(email), permissions: [] }, await getSecret(), {
expiresIn: "7d",
})
return jwt
}

View File

@@ -2,6 +2,7 @@ import { Request, Response, NextFunction } from "express"
import bcrypt from "bcrypt"
import { Membre, MemberLogin, emailRegexp, passwordMinLength } from "../../services/membres"
import getAccessors from "../gsheets/accessors"
import { getJwt } from "../secure"
const { listGet } = getAccessors("Membres", new Membre())
@@ -50,9 +51,12 @@ export async function login(rawEmail: string, rawPassword: string): Promise<Memb
throw Error("Mauvais mot de passe pour cet email")
}
const jwt = await getJwt(email)
return {
membre: {
prenom: membre.prenom,
},
jwt,
}
}

View File

@@ -1,6 +1,7 @@
import axios from "axios"
import config from "../config"
import { axiosConfig } from "./auth"
export type ElementWithId = unknown & { id: number }
@@ -15,6 +16,7 @@ export function get<Element>(elementName: string): (id: number) => Promise<{
return async (id: number): Promise<ElementGetResponse> => {
try {
const { data } = await axios.get(`${config.API_URL}/${elementName}Get`, {
...axiosConfig,
params: { id },
})
return { data }
@@ -34,7 +36,7 @@ export function listGet<Element>(elementName: string): () => Promise<{
}
return async (): Promise<ElementListGetResponse> => {
try {
const { data } = await axios.get(`${config.API_URL}/${elementName}ListGet`)
const { data } = await axios.get(`${config.API_URL}/${elementName}ListGet`, axiosConfig)
return { data }
} catch (error) {
return { error: error as Error }
@@ -57,7 +59,8 @@ export function add<ElementNoId extends object, Element extends ElementNoId & El
try {
const { data } = await axios.post(
`${config.API_URL}/${elementName}Add`,
membreWithoutId
membreWithoutId,
axiosConfig
)
return { data }
} catch (error) {
@@ -76,7 +79,11 @@ export function set<Element>(elementName: string): (membre: Element) => Promise<
}
return async (membre: Element): Promise<ElementGetResponse> => {
try {
const { data } = await axios.post(`${config.API_URL}/${elementName}Set`, membre)
const { data } = await axios.post(
`${config.API_URL}/${elementName}Set`,
membre,
axiosConfig
)
return { data }
} catch (error) {
return { error: error as Error }

22
src/services/auth.ts Normal file
View File

@@ -0,0 +1,22 @@
import { AxiosRequestConfig } from "axios"
const storage: any = localStorage
const jwt: string | null = storage?.getItem("id_token")
if (jwt) {
setJWT(jwt)
}
export const axiosConfig: AxiosRequestConfig = {
headers: {},
}
export function setJWT(token: string): void {
axiosConfig.headers.Authorization = `Bearer ${token}`
storage?.setItem("id_token", token)
}
export function unsetJWT(): void {
delete axiosConfig.headers.Authorization
storage?.removeItem("id_token")
}

View File

@@ -36,6 +36,7 @@ export interface MemberLogin {
membre?: {
prenom: string
}
jwt?: string
error?: string
}

View File

@@ -0,0 +1,18 @@
export function canonicalEmail(email: string): string {
email = email.replace(/^\s+|\s+$/g, "")
if (/@gmail.com$/.test(email)) {
let domain = email.replace(/^.*@/, "")
domain = domain.replace(/^googlemail%.com$/, "gmail.com")
email = email
.replace(/\./g, "")
.replace(/^[^@]+/, (match) => match.toLowerCase())
.replace(/@.*$/, `@${domain}`)
}
return email.toLowerCase()
}
export function validEmail(email: string): boolean {
return /^(?=.{1,254}$)(?=.{1,64}@)[-!#$%&'*+/0-9=?A-Z^_`a-z{|}~]+(\.[-!#$%&'*+/0-9=?A-Z^_`a-z{|}~]+)*@[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?(\.[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?)*$/.test(
email
)
}