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:
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user