Skip to content

Commit 917c80e

Browse files
authored
feat(auth): add mobile logout endpoint with access token validation (#1445)
1 parent eb5a654 commit 917c80e

File tree

1 file changed

+93
-0
lines changed

1 file changed

+93
-0
lines changed

src/app/api/mobile/logout/route.ts

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import { NextRequest, NextResponse } from 'next/server'
2+
import { createRemoteJWKSet, jwtVerify } from 'jose'
3+
import { isNullOrEmpty } from '@/js/auth/mobile'
4+
import { AUTH_CONFIG_SERVER } from '@/Config'
5+
6+
if (AUTH_CONFIG_SERVER == null) throw new Error('AUTH_CONFIG_SERVER not defined')
7+
8+
const { clientId, clientSecret, issuer } = AUTH_CONFIG_SERVER
9+
const JWKS = createRemoteJWKSet(new URL(`${issuer}/.well-known/jwks.json`))
10+
11+
/**
12+
* Verify the access token from Authorization header
13+
*/
14+
async function verifyAccessToken (request: NextRequest): Promise<boolean> {
15+
const authHeader = request.headers.get('authorization')
16+
if (authHeader == null || !authHeader.startsWith('Bearer ')) {
17+
return false
18+
}
19+
20+
const token = authHeader.substring(7).trim()
21+
try {
22+
await jwtVerify(token, JWKS, {
23+
issuer: issuer + '/',
24+
audience: 'https://api.openbeta.io/'
25+
})
26+
return true
27+
} catch {
28+
return false
29+
}
30+
}
31+
32+
/**
33+
* Mobile logout handler - revokes refresh token at Auth0
34+
*/
35+
export async function POST (request: NextRequest): Promise<NextResponse> {
36+
// Verify access token
37+
const isAuthenticated = await verifyAccessToken(request)
38+
if (!isAuthenticated) {
39+
return NextResponse.json(
40+
{ error: 'Unauthorized - Invalid access token' },
41+
{ status: 401 }
42+
)
43+
}
44+
45+
// Parse request body
46+
let refreshToken: string
47+
try {
48+
const data = await request.json()
49+
refreshToken = data.refreshToken
50+
51+
if (isNullOrEmpty(refreshToken)) {
52+
console.error('Empty refreshToken!')
53+
throw new Error('Invalid payload')
54+
}
55+
} catch (error) {
56+
return NextResponse.json(
57+
{ error: 'Invalid payload' },
58+
{ status: 400 }
59+
)
60+
}
61+
62+
// Revoke token at Auth0
63+
try {
64+
const response = await fetch(`${issuer}oauth/revoke`, {
65+
method: 'POST',
66+
headers: {
67+
'Content-Type': 'application/json'
68+
},
69+
body: JSON.stringify({
70+
client_id: clientId,
71+
client_secret: clientSecret,
72+
token: refreshToken
73+
})
74+
})
75+
76+
if (!response.ok) {
77+
const errorData = await response.json()
78+
console.error('#### Auth0 revoke error ####', errorData)
79+
return NextResponse.json(
80+
{ error: errorData },
81+
{ status: response.status }
82+
)
83+
}
84+
85+
return NextResponse.json({ success: true }, { status: 200 })
86+
} catch (error) {
87+
console.error('#### Auth0 revoke error ####', error)
88+
return NextResponse.json(
89+
{ error: 'Failed to revoke token' },
90+
{ status: 500 }
91+
)
92+
}
93+
}

0 commit comments

Comments
 (0)