ajout app

This commit is contained in:
2024-04-17 20:22:30 +02:00
parent cc017cfc5e
commit f9d05a2fd3
8025 changed files with 729805 additions and 0 deletions

View File

@@ -0,0 +1,24 @@
import { Router, Request, Response, NextFunction } from 'express'
import { exchange, TokenExchangeOptions } from './service'
const router = Router()
router.post('/', async (req: Request, res: Response, next: NextFunction) => {
try {
const options = req.body as TokenExchangeOptions
res.json(await exchange(options))
} catch (err) {
res.status(400)
if (
err.error &&
(err.error === 'invalid_grant' ||
err.error === 'invalid_request' ||
err.error === 'unsupported_grant_type')
) {
res.json(err)
}
next(err)
}
})
export default router

View File

@@ -0,0 +1,136 @@
import { SignJWT } from 'jose'
import { getConfig } from '@/config/config'
import { ErrorCode, newError } from '@/infra/error'
import { newHyphenlessUuid } from '@/infra/id'
import { verifyPassword } from '@/infra/password'
import userRepo, { User } from '@/user/repo'
export type TokenGrantType = 'password' | 'refresh_token'
// https://datatracker.ietf.org/doc/html/rfc6749#section-5.1
export type Token = {
access_token: string
token_type: string
expires_in: number
refresh_token: string
}
// https://datatracker.ietf.org/doc/html/rfc6749#section-4.3.2
export type TokenExchangeOptions = {
grant_type: TokenGrantType
username?: string
password?: string
refresh_token?: string
}
export async function exchange(options: TokenExchangeOptions): Promise<Token> {
validateParemeters(options)
// https://datatracker.ietf.org/doc/html/rfc6749#section-4.3
if (options.grant_type === 'password') {
let user: User
try {
user = await userRepo.findByUsername(options.username)
} catch {
throw newError({ code: ErrorCode.InvalidUsernameOrPassword })
}
if (!user.isEmailConfirmed) {
throw newError({ code: ErrorCode.EmailNotConfimed })
}
if (verifyPassword(options.password, user.passwordHash)) {
return newToken(user.id)
} else {
throw newError({ code: ErrorCode.InvalidUsernameOrPassword })
}
}
// https://datatracker.ietf.org/doc/html/rfc6749#section-6
if (options.grant_type === 'refresh_token') {
let user: User
try {
user = await userRepo.findByRefreshTokenValue(options.refresh_token)
} catch {
throw newError({ code: ErrorCode.InvalidUsernameOrPassword })
}
if (!user.isEmailConfirmed) {
throw newError({ code: ErrorCode.EmailNotConfimed })
}
if (new Date() >= new Date(user.refreshTokenExpiry)) {
throw newError({ code: ErrorCode.RefreshTokenExpired })
}
return newToken(user.id)
}
}
function validateParemeters(options: TokenExchangeOptions) {
if (!options.grant_type) {
throw newError({
code: ErrorCode.InvalidRequest,
message: 'Missing parameter: grant_type',
})
}
if (
options.grant_type !== 'password' &&
options.grant_type !== 'refresh_token'
) {
throw newError({
code: ErrorCode.UnsupportedGrantType,
message: `Grant type unsupported: ${options.grant_type}`,
})
}
if (options.grant_type === 'password') {
if (!options.username) {
throw newError({
code: ErrorCode.InvalidRequest,
message: 'Missing parameter: username',
})
}
if (!options.password) {
throw newError({
code: ErrorCode.InvalidRequest,
message: 'Missing parameter: password',
})
}
}
if (options.grant_type === 'refresh_token' && !options.refresh_token) {
throw newError({
code: ErrorCode.InvalidRequest,
message: 'Missing parameter: refresh_token',
})
}
}
async function newToken(userId: string): Promise<Token> {
const config = getConfig().token
const expiry = newAccessTokenExpiry()
const jwt = await new SignJWT({ sub: userId })
.setProtectedHeader({ alg: 'HS256' })
.setIssuedAt()
.setIssuer(config.issuer)
.setAudience(config.audience)
.setExpirationTime(expiry)
.sign(new TextEncoder().encode(config.jwtSigningKey))
const token: Token = {
access_token: jwt,
expires_in: expiry,
token_type: 'Bearer',
refresh_token: newHyphenlessUuid(),
}
const user = await userRepo.findByID(userId)
await userRepo.update({
id: user.id,
refreshTokenValue: token.refresh_token,
refreshTokenExpiry: newRefreshTokenExpiry(),
})
return token
}
function newRefreshTokenExpiry(): string {
const now = new Date()
now.setSeconds(now.getSeconds() + getConfig().token.refreshTokenLifetime)
return now.toISOString()
}
function newAccessTokenExpiry(): number {
const now = new Date()
now.setSeconds(now.getSeconds() + getConfig().token.refreshTokenLifetime)
return Math.floor(now.getTime() / 1000)
}