Enhance Passkey Authentication Logic and Error Handling

- Added `customerNumber` parameter to `getAuthenticationChallenge` and `authenticateWithPasskey` functions.
- Introduced challenge token usage in `verifyAuthentication` to align with OpenAPI spec.
- Improved error handling in `LoginForm.vue` with detailed error checks and logging.
- Ensured support for session tokens in `SubuserLoginForm.vue`.
This commit is contained in:
Jeppe Bundgaard
2026-02-24 00:34:07 +01:00
parent 4e7def8559
commit 75cde6a355
3 changed files with 82 additions and 32 deletions
+25 -3
View File
@@ -5,7 +5,7 @@ import { useStore } from 'vuex'
import { useI18n } from 'vue-i18n'
import axios from 'axios'
import {API_URL} from "@/config.js";
import { parseError, errors, clearErrors, addError, getError } from "@/components/request/HandleGlobalError.vue";
import { parseError, clearErrors, addError, getError } from "@/components/request/HandleGlobalError.vue";
import LoadButtonWhileAwait from "@/components/request/LoadButtonWhileAwait.vue";
import { SessionUser } from "@/components/session/token/SessionUser.vue";
import { isPasskeySupported, authenticateWithPasskey } from "@/services/PasskeyAuthService.js";
@@ -78,17 +78,30 @@ const doNothing = () => {
// Passkey login
const loginWithPasskey = async () => {
isPasskeyLoading.value = true;
clearErrors();
try {
const result = await authenticateWithPasskey('user');
const result = await authenticateWithPasskey('user', customer_number.value);
console.log('Passkey authentication result:', result);
if (result.token) {
localStorage.setItem('token', result.token);
successMessage.value = "Du er nu logget ind!";
setTimeout(() => {
router.go(0);
}, 500);
} else {
// No token returned - this shouldn't happen for successful auth
addError('auth', t('passkeys.login_failed'));
}
} catch (e) {
addError('auth', e.message || t('passkeys.login_failed'));
console.error('Passkey login error:', e);
// Check if it's an axios error with response data
if (e.response?.data?.message) {
addError('auth', e.response.data.message);
} else if (e.response?.data?.error) {
addError('auth', e.response.data.error);
} else {
addError('auth', e.message || t('passkeys.login_failed'));
}
} finally {
isPasskeyLoading.value = false;
}
@@ -233,5 +246,14 @@ form input:not([type="checkbox"]) {
justify-content: center;
font-size: 16px;
min-height: 43px;
cursor: pointer;
}
.submit:hover {
background-color: #1a4263;
}
.passkey-btn {
gap: 8px;
}
</style>
@@ -87,8 +87,11 @@ const loginWithPasskey = async () => {
isPasskeyLoading.value = true;
try {
const result = await authenticateWithPasskey('subuser');
if (result.token) {
localStorage.setItem('token', result.token);
// Subuser login returns 'session' token, user login returns 'token'
const sessionToken = result.session || result.token;
if (sessionToken) {
localStorage.setItem('token', sessionToken);
localStorage.setItem('is_subuser', 'true');
// Reload the page to update the UI
window.location.reload();
}
+52 -27
View File
@@ -49,60 +49,85 @@ const base64URLToArrayBuffer = (base64url) => {
/**
* Request authentication challenge from server
* @param {string} userType - 'user' or 'subuser'
* @returns {Promise<Object>} Challenge options from server
* @param {number|null} customerNumber - Optional customer number for user login
* @returns {Promise<Object>} Challenge options from server including challenge_token and publicKey
*/
const getAuthenticationChallenge = async (userType = 'user') => {
const getAuthenticationChallenge = async (userType = 'user', customerNumber = null) => {
const endpoint = userType === 'subuser'
? '/auth/passkey/challenge/subuser'
: '/auth/passkey/challenge';
const requestBody = {};
if (customerNumber) {
requestBody.customer_number = customerNumber;
}
const response = await axios.post(API_URL + endpoint);
return response.data.data;
const response = await axios.post(API_URL + endpoint, requestBody);
return response.data.data || response.data;
};
/**
* Send authentication assertion to server for verification
* @param {Object} assertion - The WebAuthn assertion
* @param {Object} assertion - The WebAuthn assertion matching openapi spec
* @param {string} challengeToken - The challenge token from challenge endpoint
* @param {string} userType - 'user' or 'subuser'
* @returns {Promise<Object>} Authentication response with token
* @returns {Promise<Object>} Authentication response with token or session
*/
const verifyAuthentication = async (assertion, userType = 'user') => {
const verifyAuthentication = async (assertion, challengeToken, userType = 'user') => {
const endpoint = userType === 'subuser'
? '/auth/passkey/verify/subuser'
: '/auth/passkey/verify';
const requestBody = {
challenge_token: challengeToken,
id: assertion.id,
response: {
clientDataJSON: assertion.clientDataJSON,
authenticatorData: assertion.authenticatorData,
signature: assertion.signature,
},
};
// Include userHandle if present
if (assertion.userHandle) {
requestBody.response.userHandle = assertion.userHandle;
}
const response = await axios.post(API_URL + endpoint, assertion);
return response.data.data;
const response = await axios.post(API_URL + endpoint, requestBody);
return response.data.data || response.data;
};
/**
* Authenticate using a passkey
* @param {string} userType - 'user' or 'subuser'
* @returns {Promise<{token: string}>} Authentication result with token
* @param {number|null} customerNumber - Optional customer number for user login
* @returns {Promise<{token: string}|{session: string}>} Authentication result with token or session
* @throws {Error} If authentication fails
*/
export const authenticateWithPasskey = async (userType = 'user') => {
export const authenticateWithPasskey = async (userType = 'user', customerNumber = null) => {
if (!isPasskeySupported()) {
throw new Error('Passkeys are not supported on this device');
}
try {
// Get challenge from server
const challengeOptions = await getAuthenticationChallenge(userType);
const challengeResponse = await getAuthenticationChallenge(userType, customerNumber);
const challengeToken = challengeResponse.challenge_token;
const publicKey = challengeResponse.publicKey;
// Prepare credential request options
// Prepare credential request options from server response
const publicKeyCredentialRequestOptions = {
challenge: base64URLToArrayBuffer(challengeOptions.challenge),
timeout: 60000,
userVerification: 'preferred',
rpId: window.location.hostname,
challenge: base64URLToArrayBuffer(publicKey.challenge),
timeout: publicKey.timeout || 60000,
userVerification: publicKey.userVerification || 'preferred',
rpId: publicKey.rpId || window.location.hostname,
};
// Add allowCredentials if provided by server
if (challengeOptions.allowCredentials && challengeOptions.allowCredentials.length > 0) {
publicKeyCredentialRequestOptions.allowCredentials = challengeOptions.allowCredentials.map(cred => ({
if (publicKey.allowCredentials && publicKey.allowCredentials.length > 0) {
publicKeyCredentialRequestOptions.allowCredentials = publicKey.allowCredentials.map(cred => ({
id: base64URLToArrayBuffer(cred.id),
type: 'public-key',
type: cred.type || 'public-key',
transports: cred.transports || ['internal', 'hybrid'],
}));
}
@@ -116,19 +141,19 @@ export const authenticateWithPasskey = async (userType = 'user') => {
throw new Error('No credential returned from authenticator');
}
// Prepare assertion for server verification
// Prepare assertion for server verification (matching openapi spec)
const assertion = {
credential_id: arrayBufferToBase64URL(credential.rawId),
authenticator_data: arrayBufferToBase64URL(credential.response.authenticatorData),
client_data_json: arrayBufferToBase64URL(credential.response.clientDataJSON),
id: arrayBufferToBase64URL(credential.rawId),
clientDataJSON: arrayBufferToBase64URL(credential.response.clientDataJSON),
authenticatorData: arrayBufferToBase64URL(credential.response.authenticatorData),
signature: arrayBufferToBase64URL(credential.response.signature),
user_handle: credential.response.userHandle
userHandle: credential.response.userHandle
? arrayBufferToBase64URL(credential.response.userHandle)
: null,
};
// Send assertion to server for verification
const result = await verifyAuthentication(assertion, userType);
// Send assertion to server for verification with challenge token
const result = await verifyAuthentication(assertion, challengeToken, userType);
return result;
} catch (error) {