Skip to content

Commit

Permalink
fix(CognitoAuth-useSigv4Client): BREAKING CHANGE - Update the useSigv…
Browse files Browse the repository at this point in the history
…4Client to be a callback (#884)

This PR is to 
* Update the useSigv4Client to be a async callback since no data needs to be kept in the state. Consumers can use try-catch block to handle both initialization errors or network call errors.
* Add two utilities function into CognitoAuthContext: getAuthenticatedUserAttributes and getAuthenticatedUserSession
  • Loading branch information
jessieweiyi authored May 10, 2023
1 parent fea8c4d commit 2140022
Show file tree
Hide file tree
Showing 7 changed files with 122 additions and 69 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,14 @@ The following authentication flows are not supported in the current version of C
* [Cognito hosted UI](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-app-integration.html)

## useSigv4Client

A React hook returning an instance of Sigv4Client to be used to run fetch call with AWS signed API requests.
Refer to [Docs](https://aws.github.io/aws-northstar/?path=/docs/components-cognitoauth-sigv4client-docs--page) for more details.

## Usage

```jsx
import CognitoAuth from '@aws-northstar/ui/components/CognitoAuth';

export const MyComponent = (args: CognitoAuthProps) => {
return (
<CognitoAuth {...args}>
Expand Down
24 changes: 20 additions & 4 deletions packages/ui/src/components/CognitoAuth/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,13 @@
limitations under the License. *
******************************************************************************************************************** */
import { createContext, useContext } from 'react';
import { CognitoUserPool, CognitoUser } from 'amazon-cognito-identity-js';
import {
CognitoUserPool,
CognitoUser,
CognitoUserSession,
CognitoUserAttribute,
GetSessionOptions,
} from 'amazon-cognito-identity-js';

export interface CognitoAuthContextAPI {
/**
Expand All @@ -38,18 +44,28 @@ export interface CognitoAuthContextAPI {
*/
onSignOut: () => void;
/**
* Returns an instance of current authenticated CognitoUser.
* Returns current authenticated CognitoUser.
* The returned cognitoUser object does not include session.
* Use <cognitoUser>.getSession callback to retrieve session tokens.
* Use getAuthenticatedUserSession to retrieve session tokens.
*/
getAuthenticatedUser?: () => CognitoUser | null;
getAuthenticatedUser: () => CognitoUser | null;
/**
* Returns current authenticated CognitoUser user session.
*/
getAuthenticatedUserSession: (options?: GetSessionOptions) => Promise<CognitoUserSession | undefined>;
/**
* Returns urrent authenticated CongitoUser user attributes.
*/
getAuthenticatedUserAttributes: () => Promise<CognitoUserAttribute[] | undefined>;
}

const initialState = {
userPoolId: '',
userPool: null,
onSignOut: () => {},
getAuthenticatedUser: () => null,
getAuthenticatedUserSession: () => Promise.resolve(undefined),
getAuthenticatedUserAttributes: () => Promise.resolve(undefined),
};

export const CognitoAuthContext = createContext<CognitoAuthContextAPI>(initialState);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,16 +35,14 @@ const AuthenticatedContent = () => {
const { onSignOut } = useCognitoAuthContext();
const [value, setValue] = useState('');
const [response, setResponse] = useState();
const [responseError, setResponseError] = useState<Error>();
const { client, error } = useSigv4Client();
const [error, setError] = useState<Error>();
const client = useSigv4Client();
const handleFetch = useCallback(async () => {
if (client.current) {
try {
const response = await client.current(value);
setResponse(await response.json());
} catch (err) {
setResponseError(err);
}
try {
const response = await client(value);
setResponse(await response.json());
} catch (err) {
setError(err);
}
}, [client, value]);

Expand All @@ -70,11 +68,6 @@ const AuthenticatedContent = () => {
{error.message}
</Alert>
)}
{responseError && (
<Alert type="error" header="Error">
{responseError.message}
</Alert>
)}
{response && (
<Alert type="info" header="Response">
{JSON.stringify(response)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,16 @@
******************************************************************************************************************** */
import useSigv4Client from '.';
import { renderHook } from '@testing-library/react-hooks';
import delay from '../../../../utils/delay';

const mockGetAuthenticatedUser = jest.fn();
const mockFetcher = jest.fn();
const testRegion = 'ap-southeast-2';
const testIdentityPoolId = 'testIdentityPoolId';
const testUserPoolId = 'testUserPoolId';
const testUrl = 'http://test.com';
const testOption = {
method: 'POST',
};

jest.mock('../../context', () => ({
useCognitoAuthContext: jest.fn().mockImplementation(() => {
Expand Down Expand Up @@ -49,20 +52,17 @@ describe('useSigv4Client', () => {

it('should return fetch client', async () => {
mockGetAuthenticatedUser.mockReturnValue('testCognitoUser');
const { result, rerender } = renderHook(() => useSigv4Client());
const { result } = renderHook(() => useSigv4Client());

await delay(1000);
rerender();
await result.current(testUrl, testOption);

expect(result.current.error).toBeUndefined();
expect(result.current.client.current).toBe(mockFetcher);
expect(mockFetcher).toHaveBeenCalledWith(testUrl, testOption);
});

it('should throw error', async () => {
mockGetAuthenticatedUser.mockReturnValue(undefined);
const { result } = renderHook(() => useSigv4Client());

expect(result.current.error?.message).toBe('CognitoUser is empty');
expect(result.current.client.current).toBeUndefined();
await expect(result.current(testUrl, testOption)).rejects.toThrow('CognitoUser is empty');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -13,46 +13,33 @@
See the License for the specific language governing permissions and
limitations under the License. *
******************************************************************************************************************** */
import { useState, useEffect, useRef } from 'react';
import { useCallback } from 'react';
import { createSignedFetcher } from './utils/awsSigv4Fetch';
import { useCognitoAuthContext } from '../../context';
import getCredentials from './utils/getCredentials';
import EmptyArgumentError from './EmptyArgumentError';

const useSigv4Client = (service: string = 'execute-api') => {
const { getAuthenticatedUser, region, identityPoolId, userPoolId } = useCognitoAuthContext();
const [error, setError] = useState<Error>();
const client = useRef<(input: RequestInfo | URL, init?: RequestInit | undefined) => Promise<Response>>();

useEffect(() => {
const getClient = async () => {
setError(undefined);
try {
const cognitoUser = getAuthenticatedUser?.();
return useCallback(
async (input: RequestInfo | URL, init?: RequestInit | undefined) => {
const cognitoUser = getAuthenticatedUser?.();

if (!cognitoUser) {
throw new EmptyArgumentError('CognitoUser is empty');
}

const fetcher = createSignedFetcher({
service,
region: region || 'us-east-1',
credentials: () => getCredentials(cognitoUser, region, identityPoolId, userPoolId),
});

client.current = fetcher;
} catch (error: any) {
setError(error);
if (!cognitoUser) {
throw new EmptyArgumentError('CognitoUser is empty');
}
};

getClient();
}, [getAuthenticatedUser, region, identityPoolId, userPoolId, service]);
const fetcher = createSignedFetcher({
service,
region: region || 'us-east-1',
credentials: () => getCredentials(cognitoUser, region, identityPoolId, userPoolId),
});

return {
client,
error,
};
return fetcher(input, init);
},
[getAuthenticatedUser, region, identityPoolId, userPoolId, service]
);
};

export default useSigv4Client;
Original file line number Diff line number Diff line change
Expand Up @@ -13,23 +13,24 @@ useSigv4Client should be used within **CognitoAuth** context.
**identityPoolId** and **region** should be provided as props of CognitoAuth component.

```jsx
// Get Sigv4Client and error (if there is any) object
const { client, error } = useSigv4Client();
const [response, setResponse] = useState();
const [responseError, setResponseError] = useState<Error>();

// Use the client in the fetch call.
const handleFetch = useCallback(async () => {
if (client.current) {
import useSigv4Client from '@aws-northstar/ui/components/CognitoAuth/hooks/useSigv4Client';

...
// Get Sigv4Client fetch callback
const client = useSigv4Client();
const [response, setResponse] = useState();
const [error, setError] = useState<Error>();

const handleFetch = useCallback(async () => {
try {
// The client object returned is a React.Ref object, so use the client.current to retrieve the latest fetch client.
const response = await client.current(value);
const response = await client(<url>, <RequestInit>);
setResponse(await response.json());
} catch (err) {
setResponseError(err);
setError(err);
}
}
}, [client, value]);
}, [client, url]);
...

```


57 changes: 56 additions & 1 deletion packages/ui/src/components/CognitoAuth/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,13 @@
limitations under the License. *
******************************************************************************************************************** */
import { FC, ReactNode, useState, useCallback, useMemo, useReducer } from 'react';
import { CognitoUserPool, CognitoUser } from 'amazon-cognito-identity-js';
import {
CognitoUserPool,
CognitoUser,
CognitoUserSession,
GetSessionOptions,
CognitoUserAttribute,
} from 'amazon-cognito-identity-js';
import Tabs from '@cloudscape-design/components/tabs';
import Container from './components/Container';
import ConfigError from './components/ConfigError';
Expand Down Expand Up @@ -134,6 +140,53 @@ const CognitoAuth: FC<CognitoAuthProps> = ({
return userPool?.getCurrentUser() || null;
}, [userPool]);

const getAuthenticatedUserSession: (options?: GetSessionOptions) => Promise<CognitoUserSession | undefined> =
useCallback(
(options?: GetSessionOptions) => {
return new Promise((resolve, reject) => {
const cognitoUser = userPool?.getCurrentUser();
if (!cognitoUser) {
resolve(undefined);
} else {
cognitoUser.getSession((error: Error | null, session: CognitoUserSession | null) => {
if (error) {
reject(error);
} else {
resolve(session || undefined);
}
}, options);
}
});
},
[userPool]
);

const getAuthenticatedUserAttributes: () => Promise<CognitoUserAttribute[] | undefined> = useCallback(() => {
return new Promise((resolve, reject) => {
const cognitoUser = userPool?.getCurrentUser();
if (!cognitoUser) {
resolve(undefined);
} else {
cognitoUser.getSession((errorGetSession: Error | null, _session: CognitoUserSession | null) => {
if (errorGetSession) {
reject(errorGetSession);
return;
}

cognitoUser.getUserAttributes(
(error: Error | undefined, result: CognitoUserAttribute[] | undefined) => {
if (error) {
reject(error);
} else {
resolve(result || undefined);
}
}
);
});
}
});
}, [userPool]);

const handleMFARequired: MFAEventHandler = useCallback(
(cognitoUser, challengeName, challengeParams) => {
setTransition(
Expand Down Expand Up @@ -219,6 +272,8 @@ const CognitoAuth: FC<CognitoAuthProps> = ({
identityPoolId,
onSignOut: handleSignOut,
getAuthenticatedUser,
getAuthenticatedUserAttributes,
getAuthenticatedUserSession,
}}
>
{typeof children === 'function' ? children(handleSignOut, user) : children}
Expand Down

0 comments on commit 2140022

Please sign in to comment.