From 87878292187f191ca6092381639387b866fac7eb Mon Sep 17 00:00:00 2001 From: kittentm Date: Mon, 27 Jul 2026 23:23:02 +0200 Subject: [PATCH] feat: add client secret support --- app/composables/useAuth.js | 160 ++++++++++++++++++++++++++ app/composables/useKeycloak.js | 85 -------------- app/pages/index.vue | 44 +++++-- app/pages/oauth/index.vue | 33 +++--- nuxt.config.ts | 1 + server/api/auth/check-session.post.ts | 23 ++++ server/api/auth/logout.post.ts | 26 +++++ server/api/auth/refresh.post.ts | 32 ++++++ server/api/auth/token.post.ts | 38 ++++++ 9 files changed, 334 insertions(+), 108 deletions(-) create mode 100644 app/composables/useAuth.js delete mode 100644 app/composables/useKeycloak.js create mode 100644 server/api/auth/check-session.post.ts create mode 100644 server/api/auth/logout.post.ts create mode 100644 server/api/auth/refresh.post.ts create mode 100644 server/api/auth/token.post.ts diff --git a/app/composables/useAuth.js b/app/composables/useAuth.js new file mode 100644 index 0000000..4852bc1 --- /dev/null +++ b/app/composables/useAuth.js @@ -0,0 +1,160 @@ +import { ref } from 'vue' + +const userToken = ref(null) +const refreshToken = ref(null) +const userProfile = ref(null) +const isAuthenticated = ref(false) +const isInitialized = ref(false) + +let sessionPollTimer = null + +export const useAuth = () => { + const setSession = (data) => { + userToken.value = data.access_token + if (data.refresh_token) { + refreshToken.value = data.refresh_token + if (import.meta.client) { + localStorage.setItem('keycloak_refresh_token', data.refresh_token) + } + } + isAuthenticated.value = true + } + + const clearSession = () => { + stopLiveSessionPolling() + userToken.value = null + refreshToken.value = null + userProfile.value = null + isAuthenticated.value = false + if (import.meta.client) { + localStorage.removeItem('keycloak_refresh_token') + } + } + + const exchangeCodeForToken = async (code) => { + try { + const data = await $fetch('/api/auth/token', { + method: 'POST', + body: { + code, + redirectUri: `${window.location.origin}/` + } + }) + + setSession(data) + window.history.replaceState({}, document.title, window.location.pathname) + await verifySession() + startLiveSessionPolling() + return true + } catch (error) { + console.error('Code exchange failed:', error) + clearSession() + return false + } finally { + isInitialized.value = true + } + } + + const silentRefresh = async () => { + const storedRefreshToken = refreshToken.value || (import.meta.client && localStorage.getItem('keycloak_refresh_token')) + + if (!storedRefreshToken) { + clearSession() + isInitialized.value = true + return false + } + + try { + const data = await $fetch('/api/auth/refresh', { + method: 'POST', + body: { refreshToken: storedRefreshToken } + }) + + setSession(data) + const isValid = await verifySession() + if (isValid) { + startLiveSessionPolling() + return true + } + return false + } catch (error) { + console.warn('Silent refresh failed:', error) + clearSession() + return false + } finally { + isInitialized.value = true + } + } + + const verifySession = async () => { + if (!userToken.value) return false + + try { + const res = await $fetch('/api/auth/check-session', { + method: 'POST', + body: { token: userToken.value } + }) + + if (res.active) { + userProfile.value = res.user + isAuthenticated.value = true + return true + } else { + return await silentRefresh() + } + } catch (e) { + return await silentRefresh() + } + } + + const startLiveSessionPolling = () => { + if (import.meta.server) return + stopLiveSessionPolling() + + sessionPollTimer = setInterval(async () => { + if (isAuthenticated.value) { + const valid = await verifySession() + if (!valid) { + handleLocalLogout() + } + } + }, 5000) + } + + const stopLiveSessionPolling = () => { + if (sessionPollTimer) { + clearInterval(sessionPollTimer) + sessionPollTimer = null + } + } + + const handleLocalLogout = () => { + clearSession() + isInitialized.value = true + navigateTo('/oauth') + } + + const logout = async () => { + try { + await $fetch('/api/auth/logout', { + method: 'POST', + body: { refreshToken: refreshToken.value } + }) + } catch (e) { + console.warn('Server logout failed:', e) + } finally { + handleLocalLogout() + } + } + + return { + userToken, + userProfile, + isAuthenticated, + isInitialized, + exchangeCodeForToken, + silentRefresh, + verifySession, + logout + } +} \ No newline at end of file diff --git a/app/composables/useKeycloak.js b/app/composables/useKeycloak.js deleted file mode 100644 index 11a8968..0000000 --- a/app/composables/useKeycloak.js +++ /dev/null @@ -1,85 +0,0 @@ -import { ref } from 'vue' - -const keycloakInstance = ref(null) -const isAuthenticated = ref(false) -const userProfile = ref(null) -const isInitialized = ref(false) - -export const useKeycloak = () => { - const config = useRuntimeConfig() - - const initKeycloak = async () => { - if (isInitialized.value && keycloakInstance.value?.authenticated) { - return true - } - - if (!import.meta.client) return false - - try { - const Keycloak = (await import('keycloak-js')).default - const keycloak = new Keycloak({ - url: config.public.keycloakUrl, - realm: config.public.keycloakRealm, - clientId: config.public.keycloakClientId - }) - - keycloakInstance.value = keycloak - - const auth = await keycloak.init({ - onLoad: 'check-sso', - pkceMethod: 'S256', - responseMode: 'query', - checkLoginIframe: false - }) - - const loggedIn = Boolean(auth || keycloak.authenticated) - isAuthenticated.value = loggedIn - isInitialized.value = true - - if (loggedIn) { - try { - userProfile.value = await keycloak.loadUserProfile() - } catch (e) { - console.warn('Could not load user profile:', e) - } - - if (window.location.search.includes('code=')) { - window.history.replaceState({}, document.title, window.location.pathname) - } - } - - return loggedIn - } catch (error) { - console.error('Keycloak init failed:', error) - isInitialized.value = true - isAuthenticated.value = false - return false - } - } - - const login = () => { - if (keycloakInstance.value) { - keycloakInstance.value.login({ - redirectUri: `${window.location.origin}/` - }) - } - } - - const logout = () => { - if (keycloakInstance.value) { - keycloakInstance.value.logout({ - redirectUri: `${window.location.origin}/oauth` - }) - } - } - - return { - keycloakInstance, - isAuthenticated, - userProfile, - isInitialized, - initKeycloak, - login, - logout - } -} \ No newline at end of file diff --git a/app/pages/index.vue b/app/pages/index.vue index 3108717..2711c83 100644 --- a/app/pages/index.vue +++ b/app/pages/index.vue @@ -1,20 +1,23 @@