Setting up authentication and authorization using AWS

Skip straight to code

  • Installing Dependencies

  • To get started, install required dependencies

    Shell
    npm add @aws-amplify/react-native aws-amplify @react-native-community/netinfo @react-native-async-storage/async-storage
    
  • Building Context

  • Context is a component that makes data available to component in tree, no matter how nested the component resides in.

    To create a context we can use built in API.

    src/contexts/Auth/index.tsx
    
    
    import { createContext } from 'react';
    import {
      AuthContextType,
      AuthProviderProps,
      ConfirmUserProps,
      CreateUserProps,
    } from './types';
    import { signUp, confirmSignUp } from 'aws-amplify/auth';
    
    const AuthContext = createContext<AuthProviderProps | null>(null);
    
    const AuthProvider = ({ children }: AuthContextType) => {
    
      const register = async ({ username, password }: CreateUserProps) => {
        try {
          await signUp({ username, password });
        } catch (e) {
          console.error(e);
          throw e;
        }
      };
    
      const confirmUser = async ({
        username,
        confirmationCode,
      }: ConfirmUserProps) => {
        try {
          await confirmSignUp({ username, confirmationCode });
        }catch (e) {
          console.error(e);
          throw e;
        }
      };
    
      return (
        <AuthContext.Provider
          value={{
            register,
            confirmUser,
          }}
        >
          {children}
        </AuthContext.Provider>
      );
    };
    
    export { AuthContext, AuthProvider };
    
    
    

    For brevity purpose, create a separate file for types

    src/contexts/Auth/types.ts
    
    
    import { ReactNode } from 'react';
    import {
      ConfirmSignUpInput,
      SignUpInput,
    } from 'aws-amplify/auth';
    
    export type AuthContextType = {
      children: ReactNode;
    };
    
    export type CreateUserProps = SignUpInput;
    
    export type ConfirmUserProps = ConfirmSignUpInput;
    
    export interface AuthProviderProps {
      register: (args: CreateUserProps) => Promise<void>;
      confirmUser: (args: ConfirmUserProps) => Promise<void>;
    }
    
    

    To get the data from context we can create a custom hook that wraps, React's useContext hook

    src/hooks/useAuth.ts
    
    
    import {useContext} from 'react';
    import {AuthContext} from '../contexts';
    
    export const useAuth = () => {
      const authContext = useContext(AuthContext);
      if (authContext == undefined) {
        throw new Error('useAuth must be used within a AuthProvider')
      }
      return authContext;
    }
    
    

    Don't forget to add Auth context to entry file

    App.tsx
    
    
    import {AuthProvider} from "./src/contexts/Auth";
    
    export function App() {
      return (
        <AuthProvider>
          <SomeComponents />
        </AuthProvider>
      );
    }
    
  • Configure Cognito

  • Provide your cognito user pool and user client id. These values can be obtained from AWS Console.

    Store these values in .env file and then pass these value to Amplify.configure function.

    App.tsx
    
    
    import {Amplify} from "aws-amplify";
    
    Amplify.configure({
      Auth: {
        Cognito: {
          userPoolId: `${process.env.EXPO_PUBLIC_USER_POOL_ID}`,
          userPoolClientId: `${process.env.EXPO_PUBLIC_USER_POOL_CLIENT_ID}`,
        },
      }
    });
    
    
  • Building Components

  • For simplicity, lets use UI component library to build components.

    For this example, we are using tamagui

    Shell
    npm add tamagui @tamagui/config @tamagui/babel-plugin
    

    After the installation is complete, update babel.config.js

    babel.config.js
    
    
    module.exports = function (api) {
      api.cache(true)
      return {
        presets: ['babel-preset-expo'],
        plugins: [
          [
            '@tamagui/babel-plugin',
            {
              components: ['tamagui'],
              config: './tamagui.config.ts',
              logTimings: true,
              disableExtraction: process.env.NODE_ENV === 'development',
            },
          ],
    
          'react-native-reanimated/plugin',
        ],
      }
    }
    

    For now, lets use default configuration provided by Tamagui for theme

    tamagui.config.ts
    
    
    import {createTamagui} from "tamagui";
    import {config as tamaguiConfig} from "@tamagui/config/v4";
    
    export const config = createTamagui(tamaguiConfig);
    
    

    Add TamaguiProvider to our entry file

    App.tsx
    
    
    import {AuthProvider} from "./src/contexts";
    import {TamaguiProvider} from "tamagui";
    import {config} from "./tamagui.config";
    
    export function App() {
      return (
        <TamaguiProvider config={config}>
          <AuthProvider>
            <SomeComponents />
          </AuthProvider>
        </TamaguiProvider>
      );
    }
    
    
  • Unit testing

  • jest is a popular testing framework.

    Native testing library provides queries that work with React Native and not rely on DOM

    jest-expo mocks out native module, making it easier to test projects that use expo packages

    Shell
    npm add jest jest-expo @testing-library/react-native @types/react-test-renderer react-test-renderer @types/jest --save-dev
    

    Create a jest.config.ts, setupTests.ts file at the root of the project. We can then add our jest configuration in this file. While you can use package.json to add jest config, its better to separate configuration from dependency file

    jest.config.ts
    
    
    import type {Config} from 'jest';
    
    const config: Config = {
      clearMocks: true,
      collectCoverage: true,
      coverageDirectory: 'coverage',
      coverageProvider: 'v8',
      preset: 'jest-expo',
      setupFilesAfterEnv: ['./setupTests.ts'],
      transformIgnorePatterns: [
        'node_modules/(?!((jest-)?react-native|@react-native(-community)?)|expo(nent)?|@expo(nent)?/.*|@expo-google-fonts/.*|react-navigation|@react-navigation/.*|@unimodules/.*|unimodules|sentry-expo|native-base|react-native-svg)/expo-modules-core|aws-amplify/.*|aws-amplify|@aws-amplify/react-native'
      ],
    };
    
    export default config;
    
    setupTests.ts
    
    
    jest.useFakeTimers();
    jest.mock('@react-native-async-storage/async-storage', () =>
      require('@react-native-async-storage/async-storage/jest/async-storage-mock')
    );
    
    jest.mock('@aws-amplify/react-native', () => ({
      loadBase64: jest.fn().mockImplementation(() => ({
        encode: jest.fn(),
      })),
      loadGetRandomValues: jest.fn(),
      loadUrlPolyfill: jest.fn(),
      loadAsyncStorage: jest.fn(),
      loadAppState: jest.fn(() => ({
        addEventListener: jest.fn(),
      })),
    }));
    
    

    Unit test for AuthContext

    __tests__/contexts/Auth.test.tsx
    
    
    import * as React from 'react';
    import { render, screen, userEvent } from '@testing-library/react-native';
    import * as Auth from 'aws-amplify/auth';
    import { AuthProvider, Button, ThemeProvider, useAuth } from '../../src';
    
    jest.mock('aws-amplify/auth');
    
    type TriggerProps = {
      triggerType: TriggerType;
      username?: string;
      password?: string;
      confirmationCode?: string;
    };
    
    const Trigger = ({
      triggerType,
      username = '',
      password = '',
      confirmationCode = '',
    }: TriggerProps)  => {
      const { register, confirmUser } = useAuth();
      const onPress = async () => {
        const lookUp = {
          register: () => register({ username, password }),
          confirmUser: () => confirmUser({ username, confirmationCode }),
        }[triggerType];
        await lookUp();
      };
      return (
        <Button testID="test-button" onPress={onPress}>
          Press Me
        </Button>
      );
    };
    export type TriggerType = 'register' | 'confirmUser' | 'login' | 'logout';
    const ComponentUnderTest = ({ triggerType }: { triggerType?: TriggerType }) => {
      return (
        <ThemeProvider defaultTheme="dark">
          <AuthProvider>
            <Trigger triggerType={triggerType} />
          </AuthProvider>
        </ThemeProvider>
      );
    };
    describe('Auth Context', () => {
      it('renders without crashing', () => {
        render(<ComponentUnderTest />);
        expect(screen.getByText('Press Me')).toBeTruthy();
      });
    
      it(`calls signUp from amplify when register is called`, async () => {
        const user = userEvent.setup();
        const mockedSignUp = jest.fn();
        const expectedParams = { password: '', username: '' };
    
        (Auth as jest.Mocked<typeof Auth>).signUp = mockedSignUp.mockResolvedValue(
          {}
        );
        render(<ComponentUnderTest triggerType="register" />);
        const element = screen.getByTestId('test-button');
        await user.press(element);
        expect(mockedSignUp).toHaveBeenCalledTimes(1);
        expect(mockedSignUp).toHaveBeenCalledWith(expectedParams);
      });
    
      it('calls confirmSignUp from amplify when confirmUser is called', async () => {
        const user = userEvent.setup();
        const mockedConfirmSignUp = jest.fn();
        const expectedParams = { confirmationCode: '', username: '' };
        (Auth as jest.Mocked<typeof Auth>).confirmSignUp =
          mockedConfirmSignUp.mockResolvedValue({});
        render(<ComponentUnderTest triggerType="confirmUser" />);
        const element = screen.getByTestId('test-button');
        await user.press(element);
        expect(mockedConfirmSignUp).toHaveBeenCalledTimes(1);
        expect(mockedConfirmSignUp).toHaveBeenCalledWith(expectedParams);
      });
    });
    
    
    
  • Integration testing

  • Detox is a popular library that enables testing an application on simulator. It can be used to add integration testing to a product.

    API calls can be mocked if need be.

    References