update
This commit is contained in:
22
packages/auth/package.json
Normal file
22
packages/auth/package.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "@repo/auth",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"exports": {
|
||||
"./auth": "./src/auth.ts",
|
||||
"./session": "./src/session.ts",
|
||||
"./context": "./src/context.ts",
|
||||
"./middleware/*": "./src/middleware/*.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"jose": "^6.1.0",
|
||||
"remix-auth-oauth2": "^3.4.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"react-router": "^7.9.5",
|
||||
"@types/node": "^24.9.2",
|
||||
"@types/react": "^19.2.2",
|
||||
"@types/react-dom": "^19.2.2",
|
||||
"typescript": "^5.9.3"
|
||||
}
|
||||
}
|
||||
43
packages/auth/src/auth.ts
Normal file
43
packages/auth/src/auth.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import type { OAuth2Tokens } from 'arctic'
|
||||
import { decodeJwt } from 'jose'
|
||||
import { Authenticator } from 'remix-auth'
|
||||
import { CodeChallengeMethod, OAuth2Strategy } from 'remix-auth-oauth2'
|
||||
|
||||
export type User = {
|
||||
sub: string
|
||||
email: string
|
||||
name: string
|
||||
scope: string
|
||||
email_verified: boolean
|
||||
accessToken: string
|
||||
refreshToken: string
|
||||
}
|
||||
|
||||
export function createAuth(env) {
|
||||
const authenticator = new Authenticator()
|
||||
const strategy = new OAuth2Strategy(
|
||||
{
|
||||
clientId: env.CLIENT_ID,
|
||||
clientSecret: env.CLIENT_SECRET,
|
||||
redirectURI: env.REDIRECT_URI,
|
||||
authorizationEndpoint: `${env.ISSUER_URL}/authorize`,
|
||||
tokenEndpoint: `${env.ISSUER_URL}/token`,
|
||||
tokenRevocationEndpoint: `${env.ISSUER_URL}/revoke`,
|
||||
scopes: env.SCOPE.split(' '),
|
||||
codeChallengeMethod: CodeChallengeMethod.S256
|
||||
},
|
||||
async ({ tokens }: { tokens: OAuth2Tokens }) => {
|
||||
const user = decodeJwt(tokens.idToken())
|
||||
|
||||
return {
|
||||
...user,
|
||||
accessToken: tokens.accessToken(),
|
||||
refreshToken: tokens.hasRefreshToken() ? tokens.refreshToken() : null
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
authenticator.use(strategy, 'oidc')
|
||||
|
||||
return authenticator
|
||||
}
|
||||
5
packages/auth/src/context.ts
Normal file
5
packages/auth/src/context.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import type { User } from '@/auth'
|
||||
import { createContext } from 'react-router'
|
||||
|
||||
export const userContext = createContext<User | null>(null)
|
||||
export const requestIdContext = createContext<string | null>(null)
|
||||
70
packages/auth/src/middleware/auth.ts
Normal file
70
packages/auth/src/middleware/auth.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { requestIdContext, userContext } from '@/context'
|
||||
import { createSessionStorage } from '@/session'
|
||||
|
||||
import { createAuth, type User } from '@/auth'
|
||||
import { decodeJwt } from 'jose'
|
||||
import { redirect, type LoaderFunctionArgs } from 'react-router'
|
||||
import type { OAuth2Strategy } from 'remix-auth-oauth2'
|
||||
|
||||
export const authMiddleware = async (
|
||||
{ request, context }: LoaderFunctionArgs,
|
||||
next: () => Promise<Response>
|
||||
): Promise<Response> => {
|
||||
const sessionStorage = createSessionStorage(context.cloudflare.env)
|
||||
const authenticator = createAuth(context.cloudflare.env)
|
||||
const strategy = authenticator.get<OAuth2Strategy<User>>('oidc')
|
||||
const session = await sessionStorage.getSession(request.headers.get('cookie'))
|
||||
const requestId = context.get(requestIdContext)
|
||||
let user = session.get('user') as User | null
|
||||
|
||||
session.set('returnTo', new URL(request.url).toString())
|
||||
|
||||
if (!user) {
|
||||
console.log('There is no user logged in')
|
||||
|
||||
return redirect('/login', {
|
||||
headers: new Headers({
|
||||
'Set-Cookie': await sessionStorage.commitSession(session)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
try {
|
||||
const accessToken = decodeJwt(user.accessToken) as { exp: number }
|
||||
const accessTokenExp = accessToken.exp * 1000
|
||||
const leeway = 120 * 1000 // 2 minutes
|
||||
|
||||
if (Date.now() > accessTokenExp - leeway) {
|
||||
const tokens = await (strategy as any).refreshToken(user.refreshToken)
|
||||
|
||||
user = {
|
||||
...user,
|
||||
accessToken: tokens.accessToken(),
|
||||
refreshToken: tokens.refreshToken()
|
||||
}
|
||||
|
||||
console.debug(`[${requestId}] Refresh token retrieved`, user)
|
||||
// Should replace the user in the session
|
||||
session.set('user', user)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`[${requestId}]`, error?.stack)
|
||||
|
||||
// If refreshing the token fails, remove the user from the current session
|
||||
// so the user is forced to sign in again
|
||||
session.unset('user')
|
||||
|
||||
return redirect('/login', {
|
||||
headers: new Headers({
|
||||
'Set-Cookie': await sessionStorage.commitSession(session)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
context.set(userContext, user)
|
||||
|
||||
const response = await next()
|
||||
const sessionCookie = await sessionStorage.commitSession(session)
|
||||
response.headers.set('Set-Cookie', sessionCookie)
|
||||
return response
|
||||
}
|
||||
20
packages/auth/src/middleware/logging.ts
Normal file
20
packages/auth/src/middleware/logging.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { requestIdContext } from '@/context'
|
||||
import { type LoaderFunctionArgs } from 'react-router'
|
||||
|
||||
export const loggingMiddleware = async (
|
||||
{ request, context }: LoaderFunctionArgs,
|
||||
next
|
||||
) => {
|
||||
const requestId = crypto.randomUUID()
|
||||
context.set(requestIdContext, requestId)
|
||||
|
||||
console.log(`[${requestId}] ${request.method} ${request.url}`)
|
||||
|
||||
const start = performance.now()
|
||||
const response = await next()
|
||||
const duration = performance.now() - start
|
||||
|
||||
console.log(`[${requestId}] Response ${response.status} (${duration}ms)`)
|
||||
|
||||
return response
|
||||
}
|
||||
16
packages/auth/src/session.ts
Normal file
16
packages/auth/src/session.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { createCookieSessionStorage } from 'react-router'
|
||||
|
||||
export function createSessionStorage(env) {
|
||||
const sessionStorage = createCookieSessionStorage({
|
||||
cookie: {
|
||||
name: '__session',
|
||||
httpOnly: true,
|
||||
secure: false,
|
||||
secrets: [env.SESSION_SECRET],
|
||||
sameSite: 'lax',
|
||||
path: '/',
|
||||
maxAge: 86400 * 7 // 7 days
|
||||
}
|
||||
})
|
||||
return sessionStorage
|
||||
}
|
||||
11
packages/auth/tsconfig.json
Normal file
11
packages/auth/tsconfig.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"moduleResolution": "bundler",
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
Reference in New Issue
Block a user