From 22b8853a467925b49dbd1a4c5eab3368946ada88 Mon Sep 17 00:00:00 2001 From: kailash-b Date: Fri, 24 Jan 2025 15:08:02 +0530 Subject: [PATCH 1/2] Add hook support for authenticating using Password Realm grant --- src/hooks/__tests__/use-auth0.spec.jsx | 59 ++++++++++++++++++++++++++ src/hooks/auth0-context.ts | 8 ++++ src/hooks/auth0-provider.tsx | 19 +++++++++ src/hooks/use-auth0.ts | 5 ++- 4 files changed, 89 insertions(+), 2 deletions(-) diff --git a/src/hooks/__tests__/use-auth0.spec.jsx b/src/hooks/__tests__/use-auth0.spec.jsx index e172a934..d5a30fe2 100644 --- a/src/hooks/__tests__/use-auth0.spec.jsx +++ b/src/hooks/__tests__/use-auth0.spec.jsx @@ -65,6 +65,7 @@ const mockAuth0 = { loginWithOTP: jest.fn().mockResolvedValue(mockCredentials), loginWithRecoveryCode: jest.fn().mockResolvedValue(mockCredentials), hasValidCredentials: jest.fn().mockResolvedValue(), + passwordRealm: jest.fn().mockResolvedValue(mockCredentials), }, credentialsManager: { getCredentials: jest.fn().mockResolvedValue(mockCredentials), @@ -781,6 +782,64 @@ describe('The useAuth0 hook', () => { expect(result.current.error).toBe(mockAuthError); }); + it('can authorize with password realm, passing through all parameters', async () => { + const { result } = renderHook(() => useAuth0(), { + wrapper, + }); + + let promise = result.current.authorizeWithPasswordRealm({ + username: 'foo@gmail.com', + password: 'random-password', + realm: 'react-native-sample-app', + }); + + await waitFor(() => expect(result.current.isLoading).toBe(false)); + + expect(mockAuth0.auth.passwordRealm).toHaveBeenCalledWith({ + username: 'foo@gmail.com', + password: 'random-password', + realm: 'react-native-sample-app', + }); + + let credentials; + await act(async () => { + credentials = await promise; + }); + expect(credentials).toEqual({ + idToken: + 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxIiwiaXNzIjoiaHR0cHM6Ly9hdXRoMC5jb20iLCJhdWQiOiJjbGllbnQxMjMiLCJuYW1lIjoiVGVzdCBVc2VyIiwiZmFtaWx5X25hbWUiOiJVc2VyIiwicGljdHVyZSI6Imh0dHBzOi8vaW1hZ2VzL3BpYy5wbmcifQ==.c2lnbmF0dXJl', + accessToken: 'ACCESS TOKEN', + }); + }); + + it('sets the user prop after successful authentication', async () => { + const { result } = renderHook(() => useAuth0(), { + wrapper, + }); + + result.current.authorizeWithPasswordRealm(); + await waitFor(() => expect(result.current.isLoading).toBe(false)); + + expect(result.current.user).toMatchObject({ + name: 'Test User', + familyName: 'User', + picture: 'https://images/pic.png', + }); + }); + + it('does not set user prop when authentication fails', async () => { + mockAuth0.auth.passwordRealm.mockRejectedValue(mockAuthError); + const { result } = renderHook(() => useAuth0(), { + wrapper, + }); + + result.current.authorizeWithPasswordRealm(); + await waitFor(() => expect(result.current.isLoading).toBe(false)); + + expect(result.current.user).toBeNull(); + expect(result.current.error).toBe(mockAuthError); + }); + it('can clear the session', async () => { const { result } = renderHook(() => useAuth0(), { wrapper, diff --git a/src/hooks/auth0-context.ts b/src/hooks/auth0-context.ts index fa24ba04..84a8ecb9 100644 --- a/src/hooks/auth0-context.ts +++ b/src/hooks/auth0-context.ts @@ -14,6 +14,7 @@ import { WebAuthorizeParameters, PasswordlessWithSMSOptions, ClearSessionOptions, + PasswordRealmOptions, } from '../types'; export interface Auth0ContextInterface @@ -105,6 +106,12 @@ export interface Auth0ContextInterface * Clears the user's credentials without clearing their web session and logs them out. */ clearCredentials: () => Promise; + /** + * Authorize user with credentials using the Password Realm Grant. See {@link Auth#passwordRealm} + */ + authorizeWithPasswordRealm: ( + parameters: PasswordRealmOptions + ) => Promise; } export interface AuthState { @@ -143,6 +150,7 @@ const initialContext = { clearSession: stub, getCredentials: stub, clearCredentials: stub, + authorizeWithPasswordRealm: stub, }; const Auth0Context = createContext(initialContext); diff --git a/src/hooks/auth0-provider.tsx b/src/hooks/auth0-provider.tsx index d07df5a3..ff259439 100644 --- a/src/hooks/auth0-provider.tsx +++ b/src/hooks/auth0-provider.tsx @@ -25,6 +25,7 @@ import { User, WebAuthorizeOptions, WebAuthorizeParameters, + PasswordRealmOptions, } from '../types'; import { CustomJwtPayload } from '../internal-types'; import { convertUser } from '../utils/userConversion'; @@ -301,6 +302,22 @@ const Auth0Provider = ({ [client] ); + const authorizeWithPasswordRealm = useCallback( + async (parameters: PasswordRealmOptions) => { + try { + const credentials = await client.auth.passwordRealm(parameters); + const user = getIdTokenProfileClaims(credentials.idToken); + await client.credentialsManager.saveCredentials(credentials); + dispatch({ type: 'LOGIN_COMPLETE', user }); + return credentials; + } catch (error) { + dispatch({ type: 'ERROR', error }); + return; + } + }, + [client] + ); + const hasValidCredentials = useCallback( async (minTtl: number = 0) => { return await client.credentialsManager.hasValidCredentials(minTtl); @@ -334,6 +351,7 @@ const Auth0Provider = ({ clearSession, getCredentials, clearCredentials, + authorizeWithPasswordRealm, }), [ state, @@ -350,6 +368,7 @@ const Auth0Provider = ({ clearSession, getCredentials, clearCredentials, + authorizeWithPasswordRealm, ] ); diff --git a/src/hooks/use-auth0.ts b/src/hooks/use-auth0.ts index 0e9db951..c4158c61 100644 --- a/src/hooks/use-auth0.ts +++ b/src/hooks/use-auth0.ts @@ -25,10 +25,11 @@ import Auth0Context, { Auth0ContextInterface } from './auth0-context'; * clearSession, * getCredentials, * clearCredentials, - * requireLocalAuthentication + * requireLocalAuthentication, + * authorizeWithPasswordRealm, * } = useAuth0(); * ``` - * + * * Refer to {@link Auth0ContextInterface} on how to use the above methods. */ const useAuth0 = () => useContext(Auth0Context); From beb127d87df7f0a0f95389430a6e9a1c9e143dd7 Mon Sep 17 00:00:00 2001 From: kailash-b Date: Fri, 24 Jan 2025 15:15:43 +0530 Subject: [PATCH 2/2] Update example/readme with clearer steps for setup --- example/README.md | 6 +----- example/android/app/src/main/AndroidManifest.xml | 4 ++-- example/src/App.tsx | 4 ++-- example/src/auth0-configuration.js | 6 ++++-- 4 files changed, 9 insertions(+), 11 deletions(-) diff --git a/example/README.md b/example/README.md index cc829fec..a4faa193 100644 --- a/example/README.md +++ b/example/README.md @@ -16,8 +16,4 @@ The application will be built and launched on the specified platform, allowing y ### To run on different Auth0 Application 1. Change the `clientId` and `domain` value in `example/src/auth0-configuration.js` -2. For Android, Change the `auth0Domain` value in `example/android/app/build.gradle` - -``` -manifestPlaceholders = [auth0Domain: "YOUR_DOMAIN_HERE", auth0Scheme: "${applicationId}"] -``` +2. For Android, Change the `android:host` values in `example/android/app/src/main/AndroidManifest.xml` diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml index f09c63d4..3600e684 100644 --- a/example/android/app/src/main/AndroidManifest.xml +++ b/example/android/app/src/main/AndroidManifest.xml @@ -31,11 +31,11 @@ diff --git a/example/src/App.tsx b/example/src/App.tsx index 25edbbc7..9bb9910e 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -109,8 +109,8 @@ const SecondScreen = () => { const SecondScreenProvider = () => { return ( diff --git a/example/src/auth0-configuration.js b/example/src/auth0-configuration.js index 4e58654e..1c26d611 100644 --- a/example/src/auth0-configuration.js +++ b/example/src/auth0-configuration.js @@ -1,6 +1,8 @@ const config = { - clientId: 'u7KBoASmHOpIFqwp77tmGRHnUVaMupBj', - domain: 'brucke.auth0.com', + clientId: 'your-client-id', + domain: 'your-domain', + clientIdSecondScreen: 'your-client-id', + domainSecondScreen: 'your-domain', }; export default config;