Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
174 changes: 169 additions & 5 deletions __tests__/Auth0Client/requestTokenForPasskey.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import { verify } from '../../src/jwt';
import { MessageChannel } from 'worker_threads';
import * as utils from '../../src/utils';
import * as scope from '../../src/scope';
import { MfaRequiredError } from '../../src/errors';
import { GenericError, MfaRequiredError } from '../../src/errors';
import { PasskeyGetTokenError } from '../../src/passkey';

import { fetchResponse, setupFn } from './helpers';

Expand Down Expand Up @@ -179,6 +180,38 @@ describe('Auth0Client', () => {
expect(capturedOptions.realm).toBe('Username-Password-Authentication');
});

it('includes organization and verification when provided', async () => {
const auth0 = setup();

let capturedOptions: any;
auth0['_requestToken'] = async function (options: any) {
capturedOptions = options;
return {
decodedToken: {
encoded: { header: 'h', payload: 'p', signature: 's' },
header: {},
claims: { sub: 'me', __raw: 'raw' },
user: { sub: 'me' }
},
id_token: TEST_ID_TOKEN,
access_token: TEST_ACCESS_TOKEN,
token_type: 'Bearer',
expires_in: 86400,
scope: options.scope
};
};

await auth0._requestTokenForPasskey({
authSession: TEST_AUTH_SESSION,
credential: TEST_CREDENTIAL as any,
organization: 'org_abc123',
verification: { email: '123456' }
});

expect(capturedOptions.organization).toBe('org_abc123');
expect(capturedOptions.verification).toEqual({ email: '123456' });
});

it('does not include realm when not provided', async () => {
const auth0 = setup();

Expand Down Expand Up @@ -208,6 +241,36 @@ describe('Auth0Client', () => {
expect(capturedOptions.realm).toBeUndefined();
});

it('does not include verification when it is empty', async () => {
const auth0 = setup();

let capturedOptions: any;
auth0['_requestToken'] = async function (options: any) {
capturedOptions = options;
return {
decodedToken: {
encoded: { header: 'h', payload: 'p', signature: 's' },
header: {},
claims: { sub: 'me', __raw: 'raw' },
user: { sub: 'me' }
},
id_token: TEST_ID_TOKEN,
access_token: TEST_ACCESS_TOKEN,
token_type: 'Bearer',
expires_in: 86400,
scope: options.scope
};
};

await auth0._requestTokenForPasskey({
authSession: TEST_AUTH_SESSION,
credential: TEST_CREDENTIAL as any,
verification: {}
});

expect(capturedOptions).not.toHaveProperty('verification');
});

it('caches tokens and establishes session via _requestToken', async () => {
const auth0 = setup();

Expand Down Expand Up @@ -262,7 +325,7 @@ describe('Auth0Client', () => {
);
});

it('propagates token endpoint errors', async () => {
it('propagates terminal token endpoint errors as GenericError', async () => {
const auth0 = setup();

mockFetch.mockResolvedValueOnce(
Expand All @@ -272,17 +335,118 @@ describe('Auth0Client', () => {
})
);

await expect(
auth0._requestTokenForPasskey({
const err = await auth0
._requestTokenForPasskey({
authSession: TEST_AUTH_SESSION,
credential: TEST_CREDENTIAL as any
})
).rejects.toMatchObject({
.catch(e => e);

expect(err).toBeInstanceOf(GenericError);
expect(err).not.toBeInstanceOf(PasskeyGetTokenError);
expect(err).toMatchObject({
error: 'invalid_grant',
error_description: 'Auth session has expired'
});
});

it('throws a retryable PasskeyGetTokenError on a wrong OTP code', async () => {
const auth0 = setup();

mockFetch.mockResolvedValueOnce(
fetchResponse(false, {
error: 'invalid_grant',
error_description: 'Invalid or expired session',
auth_session: 'auth-session-retry',
verification_required: ['email']
})
);

const err = await auth0
._requestTokenForPasskey({
authSession: TEST_AUTH_SESSION,
credential: TEST_CREDENTIAL as any,
verification: { email: '000000' }
})
.catch(e => e);

expect(err).toBeInstanceOf(PasskeyGetTokenError);
expect(err.isRetryable).toBe(true);
expect(err.authSession).toBe('auth-session-retry');
expect(err.verificationRequired).toEqual(['email']);
});

it('lists every identifier when several OTP codes are wrong', async () => {
const auth0 = setup();

mockFetch.mockResolvedValueOnce(
fetchResponse(false, {
error: 'invalid_grant',
error_description: 'Invalid or expired session',
auth_session: 'auth-session-retry',
verification_required: ['email', 'phone']
})
);

const err = await auth0
._requestTokenForPasskey({
authSession: TEST_AUTH_SESSION,
credential: TEST_CREDENTIAL as any,
verification: { email: '000000', phone: '000000' }
})
.catch(e => e);

expect(err).toBeInstanceOf(PasskeyGetTokenError);
expect(err.isRetryable).toBe(true);
expect(err.authSession).toBe('auth-session-retry');
expect(err.verificationRequired).toEqual(['email', 'phone']);
});

it('propagates a missing code error as GenericError', async () => {
const auth0 = setup();

mockFetch.mockResolvedValueOnce(
fetchResponse(false, {
error: 'invalid_request',
error_description: 'Missing required parameter: verification.email'
})
);

const err = await auth0
._requestTokenForPasskey({
authSession: TEST_AUTH_SESSION,
credential: TEST_CREDENTIAL as any
})
.catch(e => e);

expect(err).toBeInstanceOf(GenericError);
expect(err).not.toBeInstanceOf(PasskeyGetTokenError);
expect(err.error).toBe('invalid_request');
});

it('propagates an error with only auth_session as GenericError', async () => {
const auth0 = setup();

mockFetch.mockResolvedValueOnce(
fetchResponse(false, {
error: 'invalid_grant',
error_description: 'Invalid or expired session',
auth_session: 'auth-session-retry'
})
);

const err = await auth0
._requestTokenForPasskey({
authSession: TEST_AUTH_SESSION,
credential: TEST_CREDENTIAL as any
})
.catch(e => e);

expect(err).toBeInstanceOf(GenericError);
expect(err).not.toBeInstanceOf(PasskeyGetTokenError);
expect(err.data).toEqual({ auth_session: 'auth-session-retry' });
});

it('stores MFA context and re-throws MfaRequiredError', async () => {
const auth0 = setup();
const setMFAAuthDetailsSpy = jest.spyOn(auth0.mfa, 'setMFAAuthDetails');
Expand Down
23 changes: 23 additions & 0 deletions __tests__/http.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,29 @@ describe('getJson', () => {
).rejects.toHaveProperty('mfa_token', '1234');
});

it('keeps the other error response fields on GenericError.data', async () => {
mockUnfetch.mockImplementation(() =>
Promise.resolve({
ok: false,
json: () =>
Promise.resolve({
error: 'invalid_grant',
error_description: 'Invalid or expired session',
auth_session: 'abc'
}),
headers: new Headers()
})
);

await expect(
getJSON('https://test.com/', null, null, null, {}, undefined)
).rejects.toMatchObject({
error: 'invalid_grant',
error_description: 'Invalid or expired session',
data: { auth_session: 'abc' }
});
});

it('throws MissingRefreshTokenError when missing_refresh_token is returned', async () => {
mockUnfetch.mockImplementation(() =>
Promise.resolve({
Expand Down
Loading
Loading