ajout app
This commit is contained in:
24
Voltaserve/idp/src/token/router.ts
Normal file
24
Voltaserve/idp/src/token/router.ts
Normal 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
|
136
Voltaserve/idp/src/token/service.ts
Normal file
136
Voltaserve/idp/src/token/service.ts
Normal 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)
|
||||
}
|
Reference in New Issue
Block a user