feat: refactor auth flow and enable exchange token flow (#95)

* feat: refactor auth flow and enable exchange token flow

* fix: do not cache to local storage for exchange token

* chore: remove logging

* chore: lint

* feat: pkce alignment with oauth endpoint

* feat: default log in via accountBinding.authenticateAccount if available

* feat: do not show legacy sign in if connectors has accountBinding.authenticateAccount flow

* fix: base64url safe
This commit is contained in:
Oğuzhan Koral
2026-03-25 17:21:07 +03:00
committed by GitHub
parent d2b0d35119
commit a69de13f16
11 changed files with 619 additions and 187 deletions
+8 -67
View File
@@ -3,17 +3,16 @@
</template>
<script setup lang="ts">
import { md5 } from '@speckle/shared'
import { ToastNotificationType } from '@speckle/ui-components'
import { useRoute, useRouter } from 'vue-router'
import { useAuthManager } from '~/lib/authn/useAuthManager'
import type { Account } from '~/lib/bindings/definitions/IAccountBinding'
import { useTokenExchange } from '~/lib/authn/useTokenExchange'
import { useHostAppStore } from '~/store/hostApp'
const route = useRoute()
const router = useRouter()
const { getChallenge, getChallengeUrl } = useAuthManager()
const { $accountBinding } = useNuxtApp()
const { getChallenge, getCodeVerifier, getChallengeUrl } = useAuthManager()
const { exchangeAccessCode } = useTokenExchange()
const hostApp = useHostAppStore()
onMounted(async () => {
@@ -22,69 +21,11 @@ onMounted(async () => {
const accessCode = route.query.access_code as string | undefined
if (accessCode && origin) {
const challenge = getChallenge()
const body = {
appId: 'sdui',
appSecret: 'sdui',
accessCode,
challenge
if (!challenge) {
throw new Error('No challenge found in storage.')
}
// Exchange the access code for a real token (optional)
const response = await fetch(new URL('/auth/token', origin), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
})
if (!response.ok) {
const errorText = await response.text()
hostApp.setNotification({
title: 'Log In',
type: ToastNotificationType.Danger,
description: `Token exchange failed with status ${response.status}: ${errorText}`
})
// Stop processing and redirect immediately on failure
return router.replace('/')
}
const { token, refreshToken } = (await response.json()) as {
token: string
refreshToken: string
}
const graphqlQuery = {
query:
'query { activeUser { id name email company avatar } serverInfo { name company adminContact description version } }'
}
const userAndServerInfoResponse = await fetch(new URL('/graphql', origin), {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}` // Add the token as a Bearer token
},
body: JSON.stringify(graphqlQuery)
})
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const userAndServerInfo = await userAndServerInfoResponse.json()
const accountId = md5(
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
userAndServerInfo.data.activeUser.email + origin
).toUpperCase()
const account: Account = {
id: accountId,
token,
refreshToken,
isDefault: true,
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
serverInfo: { url: origin, ...userAndServerInfo.data.serverInfo },
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
userInfo: userAndServerInfo.data.activeUser
}
await $accountBinding.addAccount(accountId, account)
const codeVerifier = getCodeVerifier() ?? undefined
await exchangeAccessCode(origin, accessCode, challenge, codeVerifier)
} else {
throw new Error('No access code is found.')
}
@@ -92,7 +33,7 @@ onMounted(async () => {
hostApp.setNotification({
type: ToastNotificationType.Danger,
title: 'Failed to add your Speckle account.',
description: error as string
description: error instanceof Error ? error.message : (error as string)
})
} finally {
router.replace('/')