-
Notifications
You must be signed in to change notification settings - Fork 6
feat(core, react): add step up mfa error handler #88
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
3af510f
f146f65
30e8e20
6503083
0120bb4
6c638ac
5c84f0f
e259e3d
6c5e8f0
2d7c839
bad7c98
4d01500
262c27e
a9b8016
d8afde1
a6ee389
f6a16be
1614dd3
a992c20
55b8cc5
3349a82
32b6647
7aa45cd
90f8ea4
50ad919
1775f3f
2140d88
88dbcf4
2fbe351
3175b2d
f190b79
9e9aa55
45e4661
38bac94
5003619
f96aa37
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -893,7 +893,7 @@ interface ComponentAction<T, U = undefined> { | |
| <li> | ||
| <code>tabs.provisioning.content.notifications.*</code> – Notification messages | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| (delete_success, remove_success, update_success, general_error, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| provisioning_disabled_success, scim_token_delete_sucess) | ||
| provisioning_disabled_success, scim_token_delete_success) | ||
| </li> | ||
| </ul> | ||
| </div> | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,3 +6,4 @@ | |
|
|
||
| export * from './api-error'; | ||
| export * from './business-error'; | ||
| export * from './proxy-http-client'; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,58 @@ | ||
| /** | ||
| * Shared HTTP client for proxy-mode API calls. | ||
| * @module proxy-http-client | ||
| * @internal | ||
| */ | ||
|
|
||
| /** | ||
| * A lightweight HTTP client scoped to a base URL. | ||
| * Provides `get` and `post` helpers with standardized error handling. | ||
| */ | ||
| export interface ProxyHttpClient { | ||
| /** Sends a GET request with optional query parameters. */ | ||
| get: <T>(path: string, query?: Record<string, string>) => Promise<T>; | ||
| /** Sends a POST request with a JSON body. */ | ||
| post: <T>(path: string, body: unknown) => Promise<T>; | ||
| } | ||
|
|
||
| /** | ||
| * Creates a proxy HTTP client scoped to the given base URL. | ||
| * | ||
| * @param baseUrl - The base URL for all requests (trailing slash is stripped). | ||
| * @returns A {@link ProxyHttpClient} with `get` and `post` methods. | ||
| */ | ||
| export function createProxyHttpClient(baseUrl: string): ProxyHttpClient { | ||
| const normalizedBase = baseUrl.replace(/\/$/, ''); | ||
|
|
||
| const handleResponse = async <T>(response: Response): Promise<T> => { | ||
| if (!response.ok) { | ||
| const errorBody = await response.json().catch(() => ({})); | ||
| throw Object.assign(new Error(errorBody.error_description || `HTTP ${response.status}`), { | ||
| status: response.status, | ||
| body: errorBody, | ||
| ...errorBody, | ||
| }); | ||
| } | ||
| return response.json(); | ||
| }; | ||
|
|
||
| const get = async <T>(path: string, query?: Record<string, string>): Promise<T> => { | ||
| const url = new URL(`${normalizedBase}${path}`); | ||
| if (query) { | ||
| Object.entries(query).forEach(([k, v]) => url.searchParams.set(k, v)); | ||
| } | ||
| const response = await fetch(url.toString()); | ||
| return handleResponse<T>(response); | ||
| }; | ||
|
|
||
| const post = async <T>(path: string, body: unknown): Promise<T> => { | ||
| const response = await fetch(`${normalizedBase}${path}`, { | ||
| method: 'POST', | ||
| headers: { 'Content-Type': 'application/json' }, | ||
| body: JSON.stringify(body), | ||
| }); | ||
| return handleResponse<T>(response); | ||
| }; | ||
|
|
||
| return { get, post }; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,7 +7,6 @@ import type { | |
| BasicAuth0ContextInterface, | ||
| CoreClientInterface, | ||
| User, | ||
| Auth0ContextInterface, | ||
| GetTokenSilentlyVerboseResponse, | ||
| GetTokenSilentlyOptions, | ||
| } from '../auth-types'; | ||
|
|
@@ -39,6 +38,29 @@ export const createMockVerboseTokenResponse = ( | |
| ...overrides, | ||
| }); | ||
|
|
||
| /** | ||
| * Creates a mock MFA API client | ||
| */ | ||
| const createMockMfaClient = () => ({ | ||
| getAuthenticators: vi.fn().mockResolvedValue([]), | ||
| enroll: vi.fn().mockResolvedValue({ | ||
| authenticatorType: 'otp', | ||
| secret: 'mock-secret', | ||
| barcodeUri: 'otpauth://totp/mock', | ||
| id: 'authenticator_123', | ||
| }), | ||
| challenge: vi.fn().mockResolvedValue({ | ||
| challengeType: 'oob', | ||
| oobCode: 'mock-oob-code', | ||
| }), | ||
| getEnrollmentFactors: vi.fn().mockResolvedValue([]), | ||
| verify: vi.fn().mockResolvedValue({ | ||
| id_token: 'mock-id-token', | ||
| access_token: 'mock-access-token', | ||
| expires_in: 3600, | ||
| }), | ||
| }); | ||
|
|
||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we should separate everything related to MFA into different files (types, mocks, etc). |
||
| /** | ||
| * Creates a mock BasicAuth0ContextInterface | ||
| */ | ||
|
|
@@ -59,36 +81,7 @@ export const createMockBasicAuth0Context = ( | |
| domain: TEST_DOMAIN, | ||
| clientId: TEST_CLIENT_ID, | ||
| }), | ||
| ...overrides, | ||
| }); | ||
|
|
||
| /** | ||
| * Creates a mock Auth0ContextInterface with full properties | ||
| */ | ||
| export const createMockAuth0Context = ( | ||
| overrides?: Partial<Auth0ContextInterface>, | ||
| ): Auth0ContextInterface => ({ | ||
| isAuthenticated: true, | ||
| isLoading: false, | ||
| user: createMockUser(), | ||
| getAccessTokenSilently: vi.fn().mockImplementation(async (options?: GetTokenSilentlyOptions) => { | ||
| if (options?.detailedResponse) { | ||
| return createMockVerboseTokenResponse(); | ||
| } | ||
| return 'mock-access-token'; | ||
| }), | ||
| getAccessTokenWithPopup: vi.fn().mockResolvedValue('mock-access-token'), | ||
| loginWithRedirect: vi.fn().mockResolvedValue(undefined), | ||
| loginWithPopup: vi.fn().mockResolvedValue(undefined), | ||
| logout: vi.fn().mockResolvedValue(undefined), | ||
| getIdTokenClaims: vi.fn().mockResolvedValue({ | ||
| sub: 'auth0|test-user-123', | ||
| aud: 'test-client-id', | ||
| iss: 'https://test-domain.auth0.com/', | ||
| }), | ||
| handleRedirectCallback: vi.fn().mockResolvedValue({ | ||
| appState: {}, | ||
| }), | ||
| mfa: createMockMfaClient(), | ||
| ...overrides, | ||
| }); | ||
|
|
||
|
|
@@ -115,9 +108,35 @@ export const createMockMyAccountApiClient = (): CoreClientInterface['myAccountAp | |
| delete: vi.fn().mockResolvedValue(undefined), | ||
| verify: vi.fn().mockResolvedValue({ confirmed: true }), | ||
| }, | ||
| withScopes: vi.fn().mockReturnThis(), | ||
| } as unknown as CoreClientInterface['myAccountApiClient']; | ||
| }; | ||
|
|
||
| /** | ||
| * Creates a mock StepUpApiService | ||
| */ | ||
| export const createMockStepUpApiService = (): CoreClientInterface['stepUpApiService'] => { | ||
| return { | ||
| getAuthenticators: vi.fn().mockResolvedValue([]), | ||
| enroll: vi.fn().mockResolvedValue({ | ||
| authenticatorType: 'otp', | ||
| secret: 'mock-secret', | ||
| barcodeUri: 'otpauth://totp/mock', | ||
| id: 'authenticator_123', | ||
| }), | ||
| challenge: vi.fn().mockResolvedValue({ | ||
| challengeType: 'oob', | ||
| oobCode: 'mock-oob-code', | ||
| }), | ||
| getEnrollmentFactors: vi.fn().mockResolvedValue([]), | ||
| verify: vi.fn().mockResolvedValue({ | ||
| id_token: 'mock-id-token', | ||
| access_token: 'mock-access-token', | ||
| expires_in: 3600, | ||
| }), | ||
| } as unknown as CoreClientInterface['stepUpApiService']; | ||
| }; | ||
|
|
||
| /** | ||
| * Creates a mock MyOrganizationClient service | ||
| */ | ||
|
|
@@ -203,6 +222,7 @@ export const createMockMyOrganizationApiClient = | |
| }, | ||
| }, | ||
| }, | ||
| withScopes: vi.fn().mockReturnThis(), | ||
| } as unknown as CoreClientInterface['myOrganizationApiClient']; | ||
| }; | ||
|
|
||
|
|
@@ -214,21 +234,25 @@ export const createMockCoreClient = (authDetails?: Partial<AuthDetails>): CoreCl | |
| const mockI18nService = createMockI18nService(); | ||
| const mockMyAccountApiClient = createMockMyAccountApiClient(); | ||
| const mockMyOrganizationApiClient = createMockMyOrganizationApiClient(); | ||
| const mockStepUpApiService = createMockStepUpApiService(); | ||
|
|
||
| return { | ||
| auth: mockAuth, | ||
| i18nService: mockI18nService, | ||
| myAccountApiClient: mockMyAccountApiClient, | ||
| myOrganizationApiClient: mockMyOrganizationApiClient, | ||
| stepUpApiService: mockStepUpApiService, | ||
| getMyAccountApiClient: vi.fn( | ||
| () => mockMyAccountApiClient, | ||
| ) as CoreClientInterface['getMyAccountApiClient'], | ||
| getMyOrganizationApiClient: vi.fn( | ||
| () => mockMyOrganizationApiClient, | ||
| ) as CoreClientInterface['getMyOrganizationApiClient'], | ||
| getStepUpApiService: vi.fn( | ||
| () => mockStepUpApiService, | ||
| ) as CoreClientInterface['getStepUpApiService'], | ||
| getToken: vi.fn().mockResolvedValue('mock-access-token'), | ||
| isProxyMode: vi.fn().mockReturnValue(false), | ||
| ensureScopes: vi.fn().mockResolvedValue(undefined), | ||
| getDomain: vi.fn( | ||
| () => mockAuth.domain ?? mockAuth.contextInterface?.getConfiguration()?.domain, | ||
| ), | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,2 +1,2 @@ | ||
| export * from './core-client.mocks'; | ||
| export * from './token-manager.mocks'; | ||
| export * from './spa-token-retriever.mocks'; |



There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we have designs for this? I think the UI experience is not aligned with the rest of the components.
Some examples in other components: