should fix auth token passing

This commit is contained in:
aecsocket
2026-04-03 20:24:18 +01:00
parent 26b2febcc8
commit 954b0c901f
5 changed files with 109 additions and 127 deletions
+1
View File
@@ -63,6 +63,7 @@
"highlight.js": "^11.7.0",
"intl-messageformat": "^10.7.7",
"iso-3166-2": "1.0.0",
"jose": "^6.2.2",
"js-yaml": "^4.1.0",
"jszip": "^3.10.1",
"lru-cache": "^11.2.4",
@@ -1335,18 +1335,11 @@ async function initializeIntercom() {
if (!auth.value?.user) return
try {
const intercomData = await $fetch<{
token: string
user: AuthUser
}>('/api/intercom/messenger-jwt')
const intercomData = await $fetch<{ token: string }>('/api/intercom/messenger-jwt')
Intercom({
app_id: config.public.intercomAppId,
intercom_user_jwt: intercomData.token,
user_id: intercomData.user.id,
name: intercomData.user.username,
email: intercomData.user.email,
created_at: Math.floor(new Date(intercomData.user.created).getTime() / 1000),
session_duration: 1000 * 60 * 60 * 24,
})
} catch (error) {
@@ -0,0 +1,99 @@
import { type Labrinth, ModrinthApiError } from '@modrinth/api-client'
import { SignJWT } from 'jose'
import { useServerModrinthClient } from '~/server/utils/api-client'
type IntercomTokenResponse = {
token: string
}
async function signIntercomUserJwt(
user: { id: string; username: string; email?: string; created: string },
secret: string,
): Promise<string> {
const createdAt = Math.floor(new Date(user.created).getTime() / 1000)
const payload: Record<string, string | number> = {
user_id: user.id,
name: user.username,
}
if (user.email) {
payload.email = user.email
}
if (Number.isFinite(createdAt)) {
payload.created_at = createdAt
}
return await new SignJWT(payload)
.setProtectedHeader({ alg: 'HS256', typ: 'JWT' })
.setIssuedAt()
.setExpirationTime('1h')
.sign(new TextEncoder().encode(secret))
}
export default defineEventHandler(async (event): Promise<IntercomTokenResponse> => {
if (event.method !== 'GET') {
throw createError({
statusCode: 405,
message: 'Method not allowed',
})
}
const authToken = getCookie(event, 'auth-token')
if (!authToken) {
throw createError({
statusCode: 401,
message: 'Authentication required',
})
}
setHeader(event, 'cache-control', 'private, no-store, max-age=0')
const config = useRuntimeConfig(event)
if (!config.intercomIdentitySecret) {
throw createError({
statusCode: 500,
message: 'Intercom identity secret is not configured',
})
}
const client = useServerModrinthClient({
event,
authToken,
})
let user: { id: string; username: string; email?: string; created: string }
try {
const currentUser = await client.request<Labrinth.Users.v2.User>('/user', {
api: 'labrinth',
version: 2,
method: 'GET',
})
user = {
id: currentUser.id,
username: currentUser.username,
email: currentUser.email,
created: currentUser.created,
}
} catch (error) {
if (error instanceof ModrinthApiError && error.statusCode === 401) {
throw createError({
statusCode: 401,
message: 'Authentication required',
})
}
throw createError({
statusCode: 502,
message: 'Failed to resolve current user',
})
}
const token = await signIntercomUserJwt(user, config.intercomIdentitySecret)
return {
token,
}
})
@@ -1,119 +0,0 @@
import { createHmac } from 'node:crypto'
import { ModrinthApiError } from '@modrinth/api-client'
import { useServerModrinthClient } from '~/server/utils/api-client'
type AuthenticatedUser = {
id: string
username: string
email?: string
created: string
}
type IntercomTokenResponse = {
token: string
user: AuthenticatedUser
}
function base64UrlEncode(input: string | Buffer): string {
return Buffer.from(input)
.toString('base64')
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/g, '')
}
function signIntercomUserJwt(payload: Record<string, unknown>, secret: string): string {
const header = {
alg: 'HS256',
typ: 'JWT',
}
const encodedHeader = base64UrlEncode(JSON.stringify(header))
const encodedPayload = base64UrlEncode(JSON.stringify(payload))
const unsignedToken = `${encodedHeader}.${encodedPayload}`
const signature = createHmac('sha256', secret).update(unsignedToken).digest()
return `${unsignedToken}.${base64UrlEncode(signature)}`
}
export default defineEventHandler(async (event): Promise<IntercomTokenResponse> => {
if (getMethod(event) !== 'GET') {
throw createError({
statusCode: 405,
message: 'Method not allowed',
})
}
const headerToken = getHeader(event, 'authorization')
const parsedHeaderToken = headerToken?.replace(/^Bearer\s+/i, '').trim()
const authToken = parsedHeaderToken || getCookie(event, 'auth-token')
if (!authToken) {
throw createError({
statusCode: 401,
message: 'Authentication required',
})
}
setHeader(event, 'cache-control', 'private, no-store, max-age=0')
const config = useRuntimeConfig(event)
if (!config.intercomIdentitySecret) {
throw createError({
statusCode: 500,
message: 'Intercom identity secret is not configured',
})
}
const client = useServerModrinthClient({
event,
authToken,
})
let user: AuthenticatedUser
try {
const currentUser = await client.request<AuthenticatedUser>('/user', {
api: 'labrinth',
version: 2,
method: 'GET',
})
user = {
id: currentUser.id,
username: currentUser.username,
email: currentUser.email,
created: currentUser.created,
}
} catch (error) {
if (error instanceof ModrinthApiError && error.statusCode === 401) {
throw createError({
statusCode: 401,
message: 'Authentication required',
})
}
throw createError({
statusCode: 502,
message: 'Failed to resolve current user',
})
}
const now = Math.floor(Date.now() / 1000)
const token = signIntercomUserJwt(
{
user_id: user.id,
email: user.email,
name: user.username,
created_at: Math.floor(new Date(user.created).getTime() / 1000),
iat: now,
exp: now + 60 * 60,
},
config.intercomIdentitySecret,
)
return {
token,
user,
}
})
+8
View File
@@ -326,6 +326,9 @@ importers:
iso-3166-2:
specifier: 1.0.0
version: 1.0.0
jose:
specifier: ^6.2.2
version: 6.2.2
js-yaml:
specifier: ^4.1.0
version: 4.1.1
@@ -6727,6 +6730,9 @@ packages:
resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==}
hasBin: true
jose@6.2.2:
resolution: {integrity: sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ==}
js-beautify@1.15.4:
resolution: {integrity: sha512-9/KXeZUKKJwqCXUdBxFJ3vPh467OCckSBmYDwSK/EtV090K+iMJ7zx2S3HLVDIWFQdqMIsZWbnaGiba18aWhaA==}
engines: {node: '>=14'}
@@ -16355,6 +16361,8 @@ snapshots:
jiti@2.6.1: {}
jose@6.2.2: {}
js-beautify@1.15.4:
dependencies:
config-chain: 1.1.13