This commit is contained in:
2025-11-05 16:26:01 -03:00
parent 488b96dc51
commit 0698cff8cf
76 changed files with 374 additions and 2580 deletions

43
packages/auth/src/auth.ts Normal file
View 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
}

View 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)

View 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
}

View 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
}

View 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
}