From 217ea022614ae61ef4843e164bb9677ad1728cf4 Mon Sep 17 00:00:00 2001 From: kailash-b Date: Wed, 29 Jan 2025 15:55:58 +0530 Subject: [PATCH 1/3] Fix for incorrect userProfile type in ExchangeNativeSocialOptions --- .../__snapshots__/index.spec.js.snap | 2 +- src/auth/__tests__/index.spec.js | 172 +++++++++--------- src/types.ts | 2 +- 3 files changed, 87 insertions(+), 89 deletions(-) diff --git a/src/auth/__tests__/__snapshots__/index.spec.js.snap b/src/auth/__tests__/__snapshots__/index.spec.js.snap index 82e7d575..a4ca7d73 100644 --- a/src/auth/__tests__/__snapshots__/index.spec.js.snap +++ b/src/auth/__tests__/__snapshots__/index.spec.js.snap @@ -269,7 +269,7 @@ exports[`auth code exchange for native social should send correct payload with o [ "https://samples.auth0.com/oauth/token", { - "body": "{"subject_token":"a subject token","subject_token_type":"a subject token type","user_profile":{"name":{"firstName":"John","lastName":"Smith"}},"audience":"http://myapi.com","scope":"openid","client_id":"A_CLIENT_ID_OF_YOUR_ACCOUNT","grant_type":"urn:ietf:params:oauth:grant-type:token-exchange"}", + "body": "{"subject_token":"a subject token","subject_token_type":"a subject token type","user_profile":"{\\"name\\":{\\"firstName\\":\\"John\\",\\"lastName\\":\\"Smith\\"}}","audience":"http://myapi.com","scope":"openid","client_id":"A_CLIENT_ID_OF_YOUR_ACCOUNT","grant_type":"urn:ietf:params:oauth:grant-type:token-exchange"}", "headers": Headers { Symbol(map): { "Accept": [ diff --git a/src/auth/__tests__/index.spec.js b/src/auth/__tests__/index.spec.js index 8a4c82bf..2666e1fd 100644 --- a/src/auth/__tests__/index.spec.js +++ b/src/auth/__tests__/index.spec.js @@ -4,13 +4,13 @@ import fetchMock from 'fetch-mock'; describe('auth', () => { const baseUrl = 'samples.auth0.com'; const clientId = 'A_CLIENT_ID_OF_YOUR_ACCOUNT'; - const telemetry = {name: 'react-native-auth0', version: '1.0.0'}; + const telemetry = { name: 'react-native-auth0', version: '1.0.0' }; const redirectUri = 'https://mysite.com/callback'; const state = 'a random state for auth'; const emptySuccess = { status: 200, body: {}, - headers: {'Content-Type': 'application/json'}, + headers: { 'Content-Type': 'application/json' }, }; const tokens = { status: 200, @@ -21,7 +21,7 @@ describe('auth', () => { state, scope: 'openid', }, - headers: {'Content-Type': 'application/json'}, + headers: { 'Content-Type': 'application/json' }, }; const oauthError = { status: 400, @@ -29,35 +29,33 @@ describe('auth', () => { error: 'invalid_request', error_description: 'Invalid grant', }, - headers: {'Content-Type': 'application/json'}, + headers: { 'Content-Type': 'application/json' }, }; const unexpectedError = { status: 500, body: 'Internal Server Error....', - headers: {'Content-Type': 'text/plain'}, + headers: { 'Content-Type': 'text/plain' }, }; - const auth = new Auth({baseUrl, clientId, telemetry}); + const auth = new Auth({ baseUrl, clientId, telemetry }); beforeAll(() => { - jest - .useFakeTimers() - .setSystemTime(new Date('2023-01-01')); + jest.useFakeTimers().setSystemTime(new Date('2023-01-01')); }); beforeEach(fetchMock.restore); describe('constructor', () => { it('should build with domain', () => { - const auth = new Auth({baseUrl, clientId}); + const auth = new Auth({ baseUrl, clientId }); expect(auth.clientId).toEqual(clientId); }); it('should fail without clientId', () => { - expect(() => new Auth({baseUrl})).toThrowErrorMatchingSnapshot(); + expect(() => new Auth({ baseUrl })).toThrowErrorMatchingSnapshot(); }); it('should fail without domain', () => { - expect(() => new Auth({clientId})).toThrowErrorMatchingSnapshot(); + expect(() => new Auth({ clientId })).toThrowErrorMatchingSnapshot(); }); }); @@ -68,7 +66,7 @@ describe('auth', () => { responseType: 'code', redirectUri, state: 'a_random_state', - }), + }) ).toMatchSnapshot(); }); @@ -79,7 +77,7 @@ describe('auth', () => { redirectUri, state: 'a_random_state', connection: 'facebook', - }), + }) ).toMatchSnapshot(); }); }); @@ -95,7 +93,7 @@ describe('auth', () => { federated: true, clientId: 'CLIENT_ID', redirectTo: 'https://auth0.com', - }), + }) ).toMatchSnapshot(); }); @@ -104,7 +102,7 @@ describe('auth', () => { auth.logoutUrl({ federated: true, shouldNotBeThere: 'really', - }), + }) ).toMatchSnapshot(); }); }); @@ -152,7 +150,7 @@ describe('auth', () => { it('should handle unexpected error', async () => { fetchMock.postOnce( 'https://samples.auth0.com/oauth/token', - unexpectedError, + unexpectedError ); expect.assertions(1); const parameters = { @@ -183,12 +181,12 @@ describe('auth', () => { await auth.exchangeNativeSocial({ subjectToken: 'a subject token', subjectTokenType: 'a subject token type', - userProfile: { + userProfile: JSON.stringify({ name: { firstName: 'John', lastName: 'Smith', }, - }, + }), audience: 'http://myapi.com', scope: 'openid', }); @@ -203,7 +201,7 @@ describe('auth', () => { subjectTokenType: 'a subject token type', }; await expect( - auth.exchangeNativeSocial(parameters), + auth.exchangeNativeSocial(parameters) ).resolves.toMatchSnapshot(); }); @@ -213,17 +211,17 @@ describe('auth', () => { const parameters = { subjectToken: 'a subject token', subjectTokenType: 'a subject token type', - userProfile: { + userProfile: JSON.stringify({ name: { firstName: 'John', lastName: 'Smith', }, - }, + }), audience: 'http://myapi.com', scope: 'openid', }; await expect( - auth.exchangeNativeSocial(parameters), + auth.exchangeNativeSocial(parameters) ).resolves.toMatchSnapshot(); }); @@ -235,14 +233,14 @@ describe('auth', () => { subjectTokenType: 'a subject token type', }; await expect( - auth.exchangeNativeSocial(parameters), + auth.exchangeNativeSocial(parameters) ).rejects.toMatchSnapshot(); }); it('should handle unexpected error', async () => { fetchMock.postOnce( 'https://samples.auth0.com/oauth/token', - unexpectedError, + unexpectedError ); expect.assertions(1); const parameters = { @@ -250,7 +248,7 @@ describe('auth', () => { subjectTokenType: 'a subject token type', }; await expect( - auth.exchangeNativeSocial(parameters), + auth.exchangeNativeSocial(parameters) ).rejects.toMatchSnapshot(); }); }); @@ -260,7 +258,7 @@ describe('auth', () => { it('should begin with code', async () => { fetchMock.postOnce( 'https://samples.auth0.com/passwordless/start', - emptySuccess, + emptySuccess ); expect.assertions(1); await auth.passwordlessWithEmail({ @@ -273,7 +271,7 @@ describe('auth', () => { it('should begin with link', async () => { fetchMock.postOnce( 'https://samples.auth0.com/passwordless/start', - emptySuccess, + emptySuccess ); expect.assertions(1); await auth.passwordlessWithEmail({ @@ -286,7 +284,7 @@ describe('auth', () => { it('should begin with optional parameters', async () => { fetchMock.postOnce( 'https://samples.auth0.com/passwordless/start', - emptySuccess, + emptySuccess ); expect.assertions(1); await auth.passwordlessWithEmail({ @@ -302,7 +300,7 @@ describe('auth', () => { it('should begin with custom parameters', async () => { fetchMock.postOnce( 'https://samples.auth0.com/passwordless/start', - emptySuccess, + emptySuccess ); expect.assertions(1); await auth.passwordlessWithEmail({ @@ -356,7 +354,7 @@ describe('auth', () => { it('should begin with code', async () => { fetchMock.postOnce( 'https://samples.auth0.com/passwordless/start', - emptySuccess, + emptySuccess ); expect.assertions(1); await auth.passwordlessWithSMS({ @@ -369,7 +367,7 @@ describe('auth', () => { it('should begin with link', async () => { fetchMock.postOnce( 'https://samples.auth0.com/passwordless/start', - emptySuccess, + emptySuccess ); expect.assertions(1); await auth.passwordlessWithSMS({ @@ -382,7 +380,7 @@ describe('auth', () => { it('should begin with optional parameters', async () => { fetchMock.postOnce( 'https://samples.auth0.com/passwordless/start', - emptySuccess, + emptySuccess ); expect.assertions(1); await auth.passwordlessWithSMS({ @@ -398,7 +396,7 @@ describe('auth', () => { it('should begin with custom parameters', async () => { fetchMock.postOnce( 'https://samples.auth0.com/passwordless/start', - emptySuccess, + emptySuccess ); expect.assertions(1); await auth.passwordlessWithSMS({ @@ -479,7 +477,7 @@ describe('auth', () => { it('should handle unexpected error', async () => { fetchMock.postOnce( 'https://samples.auth0.com/oauth/token', - unexpectedError, + unexpectedError ); expect.assertions(1); await expect(auth.passwordRealm(parameters)).rejects.toMatchSnapshot(); @@ -488,7 +486,7 @@ describe('auth', () => { it('should send extra parameters', async () => { fetchMock.postOnce('https://samples.auth0.com/oauth/token', tokens); expect.assertions(1); - await auth.passwordRealm({...parameters, foo: 'bar'}); + await auth.passwordRealm({ ...parameters, foo: 'bar' }); expect(fetchMock.lastCall()).toMatchSnapshot(); }); }); @@ -521,7 +519,7 @@ describe('auth', () => { it('should handle unexpected error', async () => { fetchMock.postOnce( 'https://samples.auth0.com/oauth/token', - unexpectedError, + unexpectedError ); expect.assertions(1); await expect(auth.refreshToken(parameters)).rejects.toMatchSnapshot(); @@ -531,7 +529,7 @@ describe('auth', () => { fetchMock.postOnce('https://samples.auth0.com/oauth/token', { status: 401, body: {}, - headers: {'www-authenticate': 'Bearer error="invalid_token"'}, + headers: { 'www-authenticate': 'Bearer error="invalid_token"' }, }); expect.assertions(1); await expect(auth.refreshToken(parameters)).rejects.toMatchSnapshot(); @@ -549,11 +547,11 @@ describe('auth', () => { }); describe('revoke token', () => { - const parameters = {refreshToken: 'a refresh token of a user'}; + const parameters = { refreshToken: 'a refresh token of a user' }; const success = { status: 200, body: null, - headers: {'Content-Type': 'application/json'}, + headers: { 'Content-Type': 'application/json' }, }; it('should send correct payload', async () => { fetchMock.postOnce('https://samples.auth0.com/oauth/revoke', success); @@ -577,7 +575,7 @@ describe('auth', () => { it('should handle unexpected error', async () => { fetchMock.postOnce( 'https://samples.auth0.com/oauth/revoke', - unexpectedError, + unexpectedError ); expect.assertions(1); await expect(auth.revoke(parameters)).rejects.toMatchSnapshot(); @@ -585,22 +583,22 @@ describe('auth', () => { }); describe('user info', () => { - const parameters = {token: 'an access token of a user'}; + const parameters = { token: 'an access token of a user' }; const success = { status: 200, body: { - sub: '248289761001', - name: 'Jane Doe', - given_name: 'Jane', - family_name: 'Doe', - preferred_username: 'j.doe', - email: 'janedoe@example.com', - updated_at: 1497317424, - picture: 'http://example.com/janedoe/me.jpg', + 'sub': '248289761001', + 'name': 'Jane Doe', + 'given_name': 'Jane', + 'family_name': 'Doe', + 'preferred_username': 'j.doe', + 'email': 'janedoe@example.com', + 'updated_at': 1497317424, + 'picture': 'http://example.com/janedoe/me.jpg', 'http://mysite.com/claims/customer': 192837465, 'http://mysite.com/claims/status': 'closed', }, - headers: {'Content-Type': 'application/json'}, + headers: { 'Content-Type': 'application/json' }, }; it('should send correct payload', async () => { fetchMock.getOnce('https://samples.auth0.com/userinfo', success); @@ -644,12 +642,12 @@ describe('auth', () => { const success = { status: 200, body: "We've just sent you an email to reset your password.", - headers: {'Content-Type': 'text/html'}, + headers: { 'Content-Type': 'text/html' }, }; it('should send correct payload', async () => { fetchMock.postOnce( 'https://samples.auth0.com/dbconnections/change_password', - success, + success ); expect.assertions(1); await auth.resetPassword(parameters); @@ -659,7 +657,7 @@ describe('auth', () => { it('should return successful response', async () => { fetchMock.postOnce( 'https://samples.auth0.com/dbconnections/change_password', - success, + success ); expect.assertions(1); await expect(auth.resetPassword(parameters)).resolves.toMatchSnapshot(); @@ -668,7 +666,7 @@ describe('auth', () => { it('should handle oauth error', async () => { fetchMock.postOnce( 'https://samples.auth0.com/dbconnections/change_password', - oauthError, + oauthError ); expect.assertions(1); await expect(auth.resetPassword(parameters)).rejects.toMatchSnapshot(); @@ -677,7 +675,7 @@ describe('auth', () => { it('should handle unexpected error', async () => { fetchMock.postOnce( 'https://samples.auth0.com/dbconnections/change_password', - unexpectedError, + unexpectedError ); expect.assertions(1); await expect(auth.resetPassword(parameters)).rejects.toMatchSnapshot(); @@ -696,7 +694,7 @@ describe('auth', () => { email: 'info@auth0.com', email_verified: false, }, - headers: {'Content-Type': 'application/json'}, + headers: { 'Content-Type': 'application/json' }, }; const auth0Error = { status: 400, @@ -706,13 +704,13 @@ describe('auth', () => { name: 'BadRequestError', statusCode: 400, }, - headers: {'Content-Type': 'application/json'}, + headers: { 'Content-Type': 'application/json' }, }; it('should send correct payload', async () => { fetchMock.postOnce( 'https://samples.auth0.com/dbconnections/signup', - success, + success ); expect.assertions(1); await auth.createUser(parameters); @@ -722,27 +720,27 @@ describe('auth', () => { it('should send correct payload with username', async () => { fetchMock.postOnce( 'https://samples.auth0.com/dbconnections/signup', - success, + success ); expect.assertions(1); - await auth.createUser({...parameters, usename: 'info'}); + await auth.createUser({ ...parameters, usename: 'info' }); expect(fetchMock.lastCall()).toMatchSnapshot(); }); it('should send correct payload with metadata', async () => { fetchMock.postOnce( 'https://samples.auth0.com/dbconnections/signup', - success, + success ); expect.assertions(1); - await auth.createUser({...parameters, metadata: {customerId: 12345}}); + await auth.createUser({ ...parameters, metadata: { customerId: 12345 } }); expect(fetchMock.lastCall()).toMatchSnapshot(); }); it('should return successful response', async () => { fetchMock.postOnce( 'https://samples.auth0.com/dbconnections/signup', - success, + success ); expect.assertions(1); await expect(auth.createUser(parameters)).resolves.toMatchSnapshot(); @@ -751,7 +749,7 @@ describe('auth', () => { it('should handle auth0 error', async () => { fetchMock.postOnce( 'https://samples.auth0.com/dbconnections/signup', - auth0Error, + auth0Error ); expect.assertions(1); await expect(auth.createUser(parameters)).rejects.toMatchSnapshot(); @@ -760,7 +758,7 @@ describe('auth', () => { it('should handle unexpected error', async () => { fetchMock.postOnce( 'https://samples.auth0.com/dbconnections/signup', - unexpectedError, + unexpectedError ); expect.assertions(1); await expect(auth.createUser(parameters)).rejects.toMatchSnapshot(); @@ -781,7 +779,7 @@ describe('auth', () => { error_description: 'User is not enrolled. You can use /mfa/associate endpoint to enroll the first authenticator.', }, - headers: {'Content-Type': 'application/json'}, + headers: { 'Content-Type': 'application/json' }, }; const invalidOtpError = { @@ -791,7 +789,7 @@ describe('auth', () => { error: 'invalid_grant', error_description: 'Invalid otp_code.', }, - headers: {'Content-Type': 'application/json'}, + headers: { 'Content-Type': 'application/json' }, }; const success = { @@ -810,7 +808,7 @@ describe('auth', () => { it('should handle unexpected error', async () => { fetchMock.postOnce( 'https://samples.auth0.com/oauth/token', - unexpectedError, + unexpectedError ); expect.assertions(1); await expect(auth.loginWithOTP(parameters)).rejects.toMatchSnapshot(); @@ -819,7 +817,7 @@ describe('auth', () => { it('when MFA is not associated', async () => { fetchMock.postOnce( 'https://samples.auth0.com/oauth/token', - notAssociatedError, + notAssociatedError ); expect.assertions(1); await expect(auth.loginWithOTP(parameters)).rejects.toMatchSnapshot(); @@ -828,7 +826,7 @@ describe('auth', () => { it('when OTP Code is invalid', async () => { fetchMock.postOnce( 'https://samples.auth0.com/oauth/token', - invalidOtpError, + invalidOtpError ); expect.assertions(1); await expect(auth.loginWithOTP(parameters)).rejects.toMatchSnapshot(); @@ -864,7 +862,7 @@ describe('auth', () => { error: 'invalid_grant', error_description: 'Malformed oob_code', }, - headers: {'Content-Type': 'application/json'}, + headers: { 'Content-Type': 'application/json' }, }; const success = { @@ -889,7 +887,7 @@ describe('auth', () => { it('should handle unexpected error', async () => { fetchMock.postOnce( 'https://samples.auth0.com/oauth/token', - unexpectedError, + unexpectedError ); expect.assertions(1); await expect(auth.loginWithOOB(parameters)).rejects.toMatchSnapshot(); @@ -898,7 +896,7 @@ describe('auth', () => { it('should handle malformed OOB code', async () => { fetchMock.postOnce( 'https://samples.auth0.com/oauth/token', - malformedOOBError, + malformedOOBError ); expect.assertions(1); await expect(auth.loginWithOOB(parameters)).rejects.toMatchSnapshot(); @@ -949,35 +947,35 @@ describe('auth', () => { error: 'unsupported_challenge_type', error_description: 'User does not have a recovery-code.', }, - headers: {'Content-Type': 'application/json'}, + headers: { 'Content-Type': 'application/json' }, }; it('should require MFA Token and Recovery Code', async () => { expect.assertions(1); expect(() => - auth.loginWithRecoveryCode({}), + auth.loginWithRecoveryCode({}) ).toThrowErrorMatchingSnapshot(); }); it('when user does not have Recovery Code', async () => { fetchMock.postOnce( 'https://samples.auth0.com/oauth/token', - unAuthorizedClientError, + unAuthorizedClientError ); expect.assertions(1); await expect( - auth.loginWithRecoveryCode(parameters), + auth.loginWithRecoveryCode(parameters) ).rejects.toMatchSnapshot(); }); it('should handle unexpected error', async () => { fetchMock.postOnce( 'https://samples.auth0.com/oauth/token', - unexpectedError, + unexpectedError ); expect.assertions(1); await expect( - auth.loginWithRecoveryCode(parameters), + auth.loginWithRecoveryCode(parameters) ).rejects.toMatchSnapshot(); }); @@ -985,7 +983,7 @@ describe('auth', () => { fetchMock.postOnce('https://samples.auth0.com/oauth/token', success); expect.assertions(1); await expect( - auth.loginWithRecoveryCode(parameters), + auth.loginWithRecoveryCode(parameters) ).resolves.toMatchSnapshot(); }); @@ -1014,7 +1012,7 @@ describe('auth', () => { it('should require MFA Token', async () => { expect.assertions(1); expect(() => - auth.multifactorChallenge({}), + auth.multifactorChallenge({}) ).toThrowErrorMatchingSnapshot(); }); @@ -1022,18 +1020,18 @@ describe('auth', () => { fetchMock.postOnce('https://samples.auth0.com/mfa/challenge', {}); expect.assertions(1); await expect( - auth.multifactorChallenge(parameters), + auth.multifactorChallenge(parameters) ).resolves.toMatchSnapshot(); }); it('should handle unexpected error', async () => { fetchMock.postOnce( 'https://samples.auth0.com/mfa/challenge', - unexpectedError, + unexpectedError ); expect.assertions(1); await expect( - auth.multifactorChallenge(parameters), + auth.multifactorChallenge(parameters) ).rejects.toMatchSnapshot(); }); @@ -1041,7 +1039,7 @@ describe('auth', () => { fetchMock.postOnce('https://samples.auth0.com/mfa/challenge', success); expect.assertions(1); await expect( - auth.multifactorChallenge(parameters), + auth.multifactorChallenge(parameters) ).resolves.toMatchSnapshot(); }); @@ -1051,7 +1049,7 @@ describe('auth', () => { fetchMock.postOnce('https://samples.auth0.com/mfa/challenge', success); expect.assertions(1); await expect( - auth.multifactorChallenge(parameters), + auth.multifactorChallenge(parameters) ).resolves.toMatchSnapshot(); }); }); diff --git a/src/types.ts b/src/types.ts index 73213d05..b5ebdf6c 100644 --- a/src/types.ts +++ b/src/types.ts @@ -280,7 +280,7 @@ export interface ExchangeNativeSocialOptions { /** * Additional profile attributes to set or override, only on select native social authentication solutions */ - userProfile?: object; + userProfile?: string; /** * The API audience to request */ From c276ea09c7d0e4f7a5b8cbce88e68bef14c77b97 Mon Sep 17 00:00:00 2001 From: kailash-b Date: Wed, 29 Jan 2025 16:08:53 +0530 Subject: [PATCH 2/3] Adds hook for exchangeNativeSocial functionality --- src/hooks/__tests__/use-auth0.spec.jsx | 69 ++++++++++++++++++++++++++ src/hooks/auth0-context.ts | 8 +++ src/hooks/auth0-provider.tsx | 18 +++++++ src/hooks/use-auth0.ts | 1 + 4 files changed, 96 insertions(+) diff --git a/src/hooks/__tests__/use-auth0.spec.jsx b/src/hooks/__tests__/use-auth0.spec.jsx index d5a30fe2..b5d849cf 100644 --- a/src/hooks/__tests__/use-auth0.spec.jsx +++ b/src/hooks/__tests__/use-auth0.spec.jsx @@ -66,6 +66,7 @@ const mockAuth0 = { loginWithRecoveryCode: jest.fn().mockResolvedValue(mockCredentials), hasValidCredentials: jest.fn().mockResolvedValue(), passwordRealm: jest.fn().mockResolvedValue(mockCredentials), + exchangeNativeSocial: jest.fn().mockResolvedValue(mockCredentials), }, credentialsManager: { getCredentials: jest.fn().mockResolvedValue(mockCredentials), @@ -840,6 +841,74 @@ describe('The useAuth0 hook', () => { expect(result.current.error).toBe(mockAuthError); }); + it('can authorize with exchange social native, passing through all parameters', async () => { + const { result } = renderHook(() => useAuth0(), { + wrapper, + }); + + let promise = result.current.authorizeWithExchangeNativeSocial({ + subjectToken: 'subject-token', + subjectTokenType: 'urn:ietf:params:oauth:token-type:access_token', + userProfile: JSON.stringify({ + name: { + firstName: 'John', + lastName: 'Smith', + }, + }), + }); + + await waitFor(() => expect(result.current.isLoading).toBe(false)); + + expect(mockAuth0.auth.exchangeNativeSocial).toHaveBeenCalledWith({ + subjectToken: 'subject-token', + subjectTokenType: 'urn:ietf:params:oauth:token-type:access_token', + userProfile: JSON.stringify({ + name: { + firstName: 'John', + lastName: 'Smith', + }, + }), + }); + + 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.authorizeWithExchangeNativeSocial(); + 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.exchangeNativeSocial.mockRejectedValue(mockAuthError); + const { result } = renderHook(() => useAuth0(), { + wrapper, + }); + + result.current.authorizeWithExchangeNativeSocial(); + 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 84a8ecb9..4e3dc2b7 100644 --- a/src/hooks/auth0-context.ts +++ b/src/hooks/auth0-context.ts @@ -15,6 +15,7 @@ import { PasswordlessWithSMSOptions, ClearSessionOptions, PasswordRealmOptions, + ExchangeNativeSocialOptions, } from '../types'; export interface Auth0ContextInterface @@ -112,6 +113,12 @@ export interface Auth0ContextInterface authorizeWithPasswordRealm: ( parameters: PasswordRealmOptions ) => Promise; + /** + * Authorize user with credentials using the Password Realm Grant. See {@link Auth#passwordRealm} + */ + authorizeWithExchangeNativeSocial: ( + parameters: ExchangeNativeSocialOptions + ) => Promise; } export interface AuthState { @@ -151,6 +158,7 @@ const initialContext = { getCredentials: stub, clearCredentials: stub, authorizeWithPasswordRealm: stub, + authorizeWithExchangeNativeSocial: stub, }; const Auth0Context = createContext(initialContext); diff --git a/src/hooks/auth0-provider.tsx b/src/hooks/auth0-provider.tsx index ff259439..78f3ea0a 100644 --- a/src/hooks/auth0-provider.tsx +++ b/src/hooks/auth0-provider.tsx @@ -26,6 +26,7 @@ import { WebAuthorizeOptions, WebAuthorizeParameters, PasswordRealmOptions, + ExchangeNativeSocialOptions, } from '../types'; import { CustomJwtPayload } from '../internal-types'; import { convertUser } from '../utils/userConversion'; @@ -317,6 +318,21 @@ const Auth0Provider = ({ }, [client] ); + const authorizeWithExchangeNativeSocial = useCallback( + async (parameters: ExchangeNativeSocialOptions) => { + try { + const credentials = await client.auth.exchangeNativeSocial(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) => { @@ -352,6 +368,7 @@ const Auth0Provider = ({ getCredentials, clearCredentials, authorizeWithPasswordRealm, + authorizeWithExchangeNativeSocial, }), [ state, @@ -369,6 +386,7 @@ const Auth0Provider = ({ getCredentials, clearCredentials, authorizeWithPasswordRealm, + authorizeWithExchangeNativeSocial, ] ); diff --git a/src/hooks/use-auth0.ts b/src/hooks/use-auth0.ts index c4158c61..65c44192 100644 --- a/src/hooks/use-auth0.ts +++ b/src/hooks/use-auth0.ts @@ -27,6 +27,7 @@ import Auth0Context, { Auth0ContextInterface } from './auth0-context'; * clearCredentials, * requireLocalAuthentication, * authorizeWithPasswordRealm, + * authorizeWithExchangeNativeSocial * } = useAuth0(); * ``` * From cc6fc5ff7d12812e65d25ceb4b8010b6bdc6a306 Mon Sep 17 00:00:00 2001 From: kailash-b Date: Wed, 29 Jan 2025 17:03:09 +0530 Subject: [PATCH 3/3] Adds hook for revoke refresh tokens --- src/hooks/__tests__/use-auth0.spec.jsx | 17 +++++++++++++++++ src/hooks/auth0-context.ts | 7 +++++++ src/hooks/auth0-provider.tsx | 17 +++++++++++++++++ src/hooks/use-auth0.ts | 3 ++- 4 files changed, 43 insertions(+), 1 deletion(-) diff --git a/src/hooks/__tests__/use-auth0.spec.jsx b/src/hooks/__tests__/use-auth0.spec.jsx index b5d849cf..ded7c515 100644 --- a/src/hooks/__tests__/use-auth0.spec.jsx +++ b/src/hooks/__tests__/use-auth0.spec.jsx @@ -67,6 +67,7 @@ const mockAuth0 = { hasValidCredentials: jest.fn().mockResolvedValue(), passwordRealm: jest.fn().mockResolvedValue(mockCredentials), exchangeNativeSocial: jest.fn().mockResolvedValue(mockCredentials), + revoke: jest.fn().mockResolvedValue(mockCredentials), }, credentialsManager: { getCredentials: jest.fn().mockResolvedValue(mockCredentials), @@ -909,6 +910,22 @@ describe('The useAuth0 hook', () => { expect(result.current.error).toBe(mockAuthError); }); + it('can revoke refresh tokens, passing through all parameters', async () => { + const { result } = renderHook(() => useAuth0(), { + wrapper, + }); + + let promise = result.current.revokeRefreshToken({ + refreshToken: 'dummyToken', + }); + + await waitFor(() => expect(result.current.isLoading).toBe(false)); + + expect(mockAuth0.auth.revoke).toHaveBeenCalledWith({ + refreshToken: 'dummyToken', + }); + }); + 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 4e3dc2b7..0e92e316 100644 --- a/src/hooks/auth0-context.ts +++ b/src/hooks/auth0-context.ts @@ -16,6 +16,7 @@ import { ClearSessionOptions, PasswordRealmOptions, ExchangeNativeSocialOptions, + RevokeOptions, } from '../types'; export interface Auth0ContextInterface @@ -119,6 +120,11 @@ export interface Auth0ContextInterface authorizeWithExchangeNativeSocial: ( parameters: ExchangeNativeSocialOptions ) => Promise; + + /** + *Revokes an issued refresh token. See {@link Auth#revoke} + */ + revokeRefreshToken: (parameters: RevokeOptions) => Promise; } export interface AuthState { @@ -159,6 +165,7 @@ const initialContext = { clearCredentials: stub, authorizeWithPasswordRealm: stub, authorizeWithExchangeNativeSocial: stub, + revokeRefreshToken: stub, }; const Auth0Context = createContext(initialContext); diff --git a/src/hooks/auth0-provider.tsx b/src/hooks/auth0-provider.tsx index 78f3ea0a..00cfc8c7 100644 --- a/src/hooks/auth0-provider.tsx +++ b/src/hooks/auth0-provider.tsx @@ -27,6 +27,7 @@ import { WebAuthorizeParameters, PasswordRealmOptions, ExchangeNativeSocialOptions, + RevokeOptions, } from '../types'; import { CustomJwtPayload } from '../internal-types'; import { convertUser } from '../utils/userConversion'; @@ -318,6 +319,7 @@ const Auth0Provider = ({ }, [client] ); + const authorizeWithExchangeNativeSocial = useCallback( async (parameters: ExchangeNativeSocialOptions) => { try { @@ -334,6 +336,19 @@ const Auth0Provider = ({ [client] ); + const revokeRefreshToken = useCallback( + async (parameters: RevokeOptions) => { + try { + await client.auth.revoke(parameters); + return; + } catch (error) { + dispatch({ type: 'ERROR', error }); + return; + } + }, + [client] + ); + const hasValidCredentials = useCallback( async (minTtl: number = 0) => { return await client.credentialsManager.hasValidCredentials(minTtl); @@ -369,6 +384,7 @@ const Auth0Provider = ({ clearCredentials, authorizeWithPasswordRealm, authorizeWithExchangeNativeSocial, + revokeRefreshToken, }), [ state, @@ -387,6 +403,7 @@ const Auth0Provider = ({ clearCredentials, authorizeWithPasswordRealm, authorizeWithExchangeNativeSocial, + revokeRefreshToken, ] ); diff --git a/src/hooks/use-auth0.ts b/src/hooks/use-auth0.ts index 65c44192..0f9cb1d5 100644 --- a/src/hooks/use-auth0.ts +++ b/src/hooks/use-auth0.ts @@ -27,7 +27,8 @@ import Auth0Context, { Auth0ContextInterface } from './auth0-context'; * clearCredentials, * requireLocalAuthentication, * authorizeWithPasswordRealm, - * authorizeWithExchangeNativeSocial + * authorizeWithExchangeNativeSocial, + * revokeRefreshToken * } = useAuth0(); * ``` *