From c89ff7e61ed339b3c115355ed3dd5eef6377d8c1 Mon Sep 17 00:00:00 2001 From: kittentm Date: Sat, 1 Aug 2026 09:29:46 +0200 Subject: [PATCH] feat: add check for portal-access --- app/composables/useAuth.js | 2 +- app/pages/oauth/index.vue | 29 +++++++++++++++++++++---- server/api/auth/check-session.post.ts | 27 ++++++++++++++++++----- server/api/auth/token.post.ts | 31 +++++++++++++++++++++++---- 4 files changed, 75 insertions(+), 14 deletions(-) diff --git a/app/composables/useAuth.js b/app/composables/useAuth.js index 4852bc1..7f86707 100644 --- a/app/composables/useAuth.js +++ b/app/composables/useAuth.js @@ -37,7 +37,7 @@ export const useAuth = () => { method: 'POST', body: { code, - redirectUri: `${window.location.origin}/` + redirectUri: `${window.location.origin}/oauth` } }) diff --git a/app/pages/oauth/index.vue b/app/pages/oauth/index.vue index 8492940..cffd8a8 100644 --- a/app/pages/oauth/index.vue +++ b/app/pages/oauth/index.vue @@ -2,19 +2,35 @@ import { ref, onMounted } from 'vue' const config = useRuntimeConfig() +const route = useRoute() +const { exchangeCodeForToken } = useAuth() const isInitialized = ref(false) const isLoggingIn = ref(false) +const errorMessage = ref('') -onMounted(() => { +onMounted(async () => { isInitialized.value = true + + if (route.query.code) { + isLoggingIn.value = true + const success = await exchangeCodeForToken(route.query.code) + + if (success) { + await navigateTo('/') + } else { + errorMessage.value = 'Access Denied' + isLoggingIn.value = false + } + } }) const handleLogin = () => { if (isLoggingIn.value) return isLoggingIn.value = true + errorMessage.value = '' - const redirectUri = `${window.location.origin}/` + const redirectUri = `${window.location.origin}/oauth` const authUrl = new URL( `${config.public.keycloakUrl}/realms/${config.public.keycloakRealm}/protocol/openid-connect/auth` @@ -23,7 +39,7 @@ const handleLogin = () => { authUrl.searchParams.append('client_id', config.public.keycloakClientId) authUrl.searchParams.append('redirect_uri', redirectUri) authUrl.searchParams.append('response_type', 'code') - authUrl.searchParams.append('scope', 'openid profile email') + authUrl.searchParams.append('scope', 'openid profile email roles') window.location.href = authUrl.toString() } @@ -39,12 +55,17 @@ const handleLogin = () => {

Hai there :3

+ +

+ {{ errorMessage }} +

+ diff --git a/server/api/auth/check-session.post.ts b/server/api/auth/check-session.post.ts index 969c808..209b153 100644 --- a/server/api/auth/check-session.post.ts +++ b/server/api/auth/check-session.post.ts @@ -1,23 +1,40 @@ +function parseJwt(token: string) { + try { + const base64Payload = token.split('.')[1] + const payload = Buffer.from(base64Payload, 'base64').toString('utf8') + return JSON.parse(payload) + } catch { + return null + } +} + export default defineEventHandler(async (event) => { const config = useRuntimeConfig(event) - const body = await readBody(event) + const body = await readBody(event).catch(() => ({})) if (!body?.token) { - return { active: false } + return { active: false, reason: 'No token provided' } } const userinfoUrl = `${config.public.keycloakUrl}/realms/${config.public.keycloakRealm}/protocol/openid-connect/userinfo` try { - const user = await $fetch(userinfoUrl, { + const user = await $fetch(userinfoUrl, { method: 'GET', headers: { Authorization: `Bearer ${body.token}` } }) + const decodedToken = parseJwt(body.token) + const realmRoles: string[] = decodedToken?.realm_access?.roles || [] + + if (!realmRoles.includes('portal-access')) { + return { active: false, reason: 'Missing portal-access role' } + } + return { active: true, user } - } catch (error) { - return { active: false } + } catch { + return { active: false, reason: 'Invalid or expired token' } } }) \ No newline at end of file diff --git a/server/api/auth/token.post.ts b/server/api/auth/token.post.ts index 983a8c0..6a6d2fb 100644 --- a/server/api/auth/token.post.ts +++ b/server/api/auth/token.post.ts @@ -1,3 +1,13 @@ +function parseJwt(token: string) { + try { + const base64Payload = token.split('.')[1] + const payload = Buffer.from(base64Payload, 'base64').toString('utf8') + return JSON.parse(payload) + } catch { + return null + } +} + export default defineEventHandler(async (event) => { const config = useRuntimeConfig(event) const body = await readBody(event) @@ -14,13 +24,16 @@ export default defineEventHandler(async (event) => { const params = new URLSearchParams({ grant_type: 'authorization_code', client_id: config.public.keycloakClientId, - client_secret: config.keycloakClientSecret, code: body.code, redirect_uri: body.redirectUri }) + if (config.keycloakClientSecret) { + params.append('client_secret', config.keycloakClientSecret) + } + try { - const response = await $fetch(tokenUrl, { + const response = await $fetch(tokenUrl, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' @@ -28,11 +41,21 @@ export default defineEventHandler(async (event) => { body: params.toString() }) + const decodedToken = parseJwt(response.access_token) + const realmRoles: string[] = decodedToken?.realm_access?.roles || [] + + if (!realmRoles.includes('portal-access')) { + throw createError({ + statusCode: 403, + statusMessage: 'Access Denied' + }) + } + return response } catch (error: any) { throw createError({ - statusCode: error.response?.status || 500, - statusMessage: error.response?._data?.error_description || 'Failed to exchange authorization code' + statusCode: error.statusCode || error.response?.status || 401, + statusMessage: error.data?.message || error.response?._data?.error_description || 'Failed to exchange code' }) } }) \ No newline at end of file