diff --git a/src/pages/AuthCallback.vue b/src/pages/AuthCallback.vue
index 4daae8d..dc7cca3 100644
--- a/src/pages/AuthCallback.vue
+++ b/src/pages/AuthCallback.vue
@@ -1,7 +1,10 @@
-
-
-
+
+
+
diff --git a/src/pages/LoginPage.vue b/src/pages/LoginPage.vue
index 4be0504..7616cfa 100644
--- a/src/pages/LoginPage.vue
+++ b/src/pages/LoginPage.vue
@@ -14,7 +14,7 @@
-
⚠ {{ errorMessage }}
+
⚠ {{ errorMessage || oauthError }}
@@ -110,9 +110,11 @@ const router = useRouter()
const nav = useWizardNav()
const auth = useAuthStore()
const gitlabHost = ref('gitlab.com')
-const dropdownOpen = ref(false)
-const dropdownRef = ref(null)
+const dropdownOpen = ref(false)
+const dropdownRef = ref(null)
const pendingGitlab = ref(false)
+const oauthError = ref(null)
+const popupCleanup = ref(null)
const providers = [
{ id: 'github', name: 'GitHub', icon: 'fab fa-github' },
@@ -132,10 +134,24 @@ const signedInProviders = computed(() => {
const showHostBox = computed(() => pendingGitlab.value || auth.provider === 'gitlab')
onMounted(() => {
+ // If the worker redirected an OAuth popup back here with an error,
+ // forward it to the parent window and close the popup.
+ if (window.opener && route.query.error) {
+ window.opener.postMessage(
+ { type: 'github-oauth-callback', error: route.query.error },
+ '*',
+ )
+ window.close()
+ return
+ }
+
updateNav()
document.addEventListener('click', onDocClick)
})
-onBeforeUnmount(() => document.removeEventListener('click', onDocClick))
+onBeforeUnmount(() => {
+ document.removeEventListener('click', onDocClick)
+ popupCleanup.value?.()
+})
watch(() => auth.isLoggedIn, updateNav)
function updateNav() {
@@ -155,8 +171,9 @@ function updateNav() {
const errorMessage = computed(() => {
if (!route.query.error) return null
const map = {
- no_token: 'Authentication failed: no token received.',
- no_code: 'Authentication failed: no code received.',
+ no_token: 'Authentication failed: no token received.',
+ no_code: 'Authentication failed: no code received.',
+ invalid_state: 'Authentication failed: state mismatch — possible CSRF attack.',
}
return map[route.query.error] ?? `Authentication error: ${route.query.error}`
})
@@ -182,11 +199,55 @@ async function signIn(providerId) {
}
function loginGithub() {
+ oauthError.value = null
+ const state = crypto.randomUUID()
+ sessionStorage.setItem('github_oauth_state', state)
+
const workerUrl = process.env.WORKER_URL
const clientId = process.env.GITHUB_CLIENT_ID
- const redirectUri = `${workerUrl}/auth/github/callback`
- const params = new URLSearchParams({ client_id: clientId, scope: 'repo user', redirect_uri: redirectUri })
- window.location.href = `https://github.com/login/oauth/authorize?${params}`
+ const redirectUri = `${workerUrl}/callback/github`
+ const params = new URLSearchParams({ client_id: clientId, scope: 'repo user', redirect_uri: redirectUri, state })
+ const authUrl = `https://github.com/login/oauth/authorize?${params}`
+
+ const popup = window.open(authUrl, 'github-oauth', 'width=600,height=700,popup=1')
+
+ if (!popup) {
+ // popup was blocked — fall back to full-page redirect
+ window.location.href = authUrl
+ return
+ }
+
+ function onMessage(event) {
+ if (event.data?.type !== 'github-oauth-callback') return
+ cleanup()
+
+ const { token, state: returnedState, error } = event.data
+ const storedState = sessionStorage.getItem('github_oauth_state')
+ sessionStorage.removeItem('github_oauth_state')
+
+ if (error) {
+ oauthError.value = `Authentication failed: ${error.replaceAll('_', ' ')}.`
+ return
+ }
+
+ if (!token || returnedState !== storedState) {
+ oauthError.value = 'Authentication failed: invalid response from GitHub.'
+ return
+ }
+
+ auth.loginWithToken('github', token)
+ }
+
+ const timer = setInterval(() => { if (popup.closed) cleanup() }, 500)
+
+ function cleanup() {
+ clearInterval(timer)
+ window.removeEventListener('message', onMessage)
+ popupCleanup.value = null
+ }
+
+ popupCleanup.value = cleanup
+ window.addEventListener('message', onMessage)
}
async function loginGitlab() {