Skip to content
Albert edited this page Jun 6, 2024 · 6 revisions

Custom Login React TS Cognito & Identity Pool User

What is Amazon Cognito

Amazon Cognito is an identity platform for web and mobile apps.

It’s a user directory, an authentication server, and an authorization service for OAuth 2.0 access tokens and AWS credentials.

With Amazon Cognito, you can authenticate and authorize users from the built-in user directory, from your enterprise directory, and from consumer identity providers like Google and Facebook.

image

Flow

authentication-api

Authentication flows

AWS Cognito Authentication Flows

AWS Cognito provides multiple authentication flows to cater to different security and user experience needs. These flows ensure flexible, secure, and efficient authentication for your applications.

  1. USER_PASSWORD_AUTH:

    • Users authenticate using their username and password.
    • The application directly sends the credentials to AWS Cognito for verification.
  2. ALLOW_CUSTOM_AUTH:

    • Supports custom authentication flows.
    • Developers can define their own challenge/response mechanisms for user authentication.
  3. ALLOW_USER_SRP_AUTH:

    • Secure Remote Password (SRP) protocol is used for authentication.
    • Provides enhanced security by allowing password verification without sending the actual password over the network.
  4. ALLOW_REFRESH_TOKEN_AUTH:

    • Uses a refresh token to obtain a new access token without re-authenticating the user.
    • Useful for maintaining long-lived sessions without repeatedly asking for user credentials.

image

cognito-user-pool-auth-flow-srp

USER_SRP_AUTH: The USER_SRP_AUTH flow uses the SRP protocol (Secure Remote Password) where the password never leaves the client and is unknown to the server. This is the recommended flow and is used by default.

USER_SRP_AUTH takes in USERNAME and SRP_A and returns the SRP variables to be used for next challenge execution. USER_PASSWORD_AUTH takes in USERNAME and PASSWORD and returns the next challenge or tokens.

image

App client information

App clients create integration between your app and your user pool. App clients can use their own subset of authentication flows, token characteristics, and security from your user pool.

image

Social

scenario-authentication-social

Types

This table summarizes the three different approaches to designing a login authentication system in AWS.

AWS provides various tools and services to help developers design and implement robust login authentication systems for their web and mobile applications. This table highlights three different approaches to designing a login authentication system in AWS:

Title Approach Description Tools Services
Amplify Create a React app by using AWS Amplify and add authentication with Amazon Cognito Demonstrates how to use AWS Amplify to create a React-based app and add authentication with Amazon Cognito. Amplify Libraries, Amplify Studio AWS Amplify, Amazon Cognito
AWS-UI Integrating Amazon Cognito authentication and authorization with web and mobile apps Provides a comprehensive guide to integrating Amazon Cognito authentication and authorization with web and mobile apps. AWS Amplify, Amazon Cognito Console AWS Amplify, Amazon Cognito
custom Set up an example React single page application Creates a React single page application for testing user sign-up, confirmation, and sign-in with Amazon Cognito. React, Amazon Cognito Console Amazon Cognito, AWS SDK JS v3

Approach: Custom

Identity pool user

Note

An identity user pool is a service provided by platforms like AWS Cognito that helps manage user authentication and authorization.

It allows you to create and maintain a user directory, where users can sign up, sign in, and manage their profiles.

The user pool handles the storage and verification of user credentials, such as passwords and multi-factor authentication (MFA).

It also supports integration with social identity providers like Google and Facebook. This simplifies building secure applications by handling user management and authentication workflows for developers.

image image image image image image image image image image

You can also check how to create a identity pool as a guest

React code TS

The AWS SDK for JavaScript v3 enables you to easily work with Amazon Web Services , but has a modular architecture with a separate package for each service.

It also includes many frequently requested features, such as first-class TypeScript support and a new middleware stack

Note

With the Amazon Cognito user pools API, you can configure user pools and authenticate users. To authenticate users from third-party identity providers (IdPs) in this API, you can link IdP users to native user profiles

We will create a React single page application where we can test:

  • user sign-up,
  • confirmation,
  • sign-in,
  • forgot password,
  • resend code
  • and log-out.

This example application demonstrates some basic functions of Amazon Cognito user pools.

If you're already experienced in web app development with React, download the example app from GitHub from AWS repo and documetation.

You also would clone these repos:

  • code github TS: sign-up, sign-in, confirmation, log-out (basic version) with TypeScript
  • code github JS: sign-up, sign-in, confirmation, log-out (basic version) with JS
  • code github JS: basic version with JS and Forgot password feature
  • code github JS: basic version with JS, forgot password feature and Resend CODE

Dependencies

react-router-dom

npm i react-router-dom

client-cognito-identity-provider

AWS SDK for JavaScript CognitoIdentityProvider Client for Node.js, Browser and React Native.

With the Amazon Cognito user pools API, you can configure user pools and authenticate users. To authenticate users from third-party identity providers (IdPs) in this API, you can link IdP users to native user profiles. Learn more about the authentication and authorization of federated users at Adding user pool sign-in through a third party and in the User pool federation endpoints and hosted UI reference.

npm i @aws-sdk/client-cognito-identity-provider

Code

isAuthenticated Function

This is a JavaScript function named isAuthenticated that checks if a user is authenticated based on the presence of an access token in the browser's session storage.

const isAuthenticated = () => {
const accessToken = sessionStorage.getItem('accessToken');
return!!accessToken;
};
  1. const isAuthenticated = () => {: This line defines a function named isAuthenticated that takes no arguments and returns a value.
  2. const accessToken = sessionStorage.getItem('accessToken');: This line retrieves the value of a key named 'accessToken' from the browser's session storage. Session storage is a mechanism that allows storing small amounts of data locally in the browser, and it is cleared when the user closes the browser.
  3. return!!accessToken;: This line returns a boolean value indicating whether the user is authenticated or not.

The !! operator is used to convert the accessToken value to a boolean. In JavaScript, any value can be converted to a boolean using the !! operator. Here's how it works:

  • If accessToken is a truthy value (e.g., a string, a number, or an object), the !! operator returns true.
  • If accessToken is a falsy value (e.g., null, undefined, or an empty string), the !! operator returns false. In this specific case, if accessToken is present in the session storage, the function returns true, indicating that the user is authenticated. If accessToken is not present, the function returns false, indicating that the user is not authenticated.

The purpose of this function is likely to check whether the user has already authenticated and obtained an access token, which can be used to authorize subsequent requests to a server or API.

App.tsx
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
import LoginPage from './loginPage';
import HomePage from './homePage';
import ConfirmUserPage from './confirmUserPage';
import './App.css'

const App = () => {
  const isAuthenticated = () => {
    const accessToken = sessionStorage.getItem('accessToken');
    return !!accessToken;
  };

  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={isAuthenticated() ? <Navigate replace to="/home" /> : <Navigate replace to="/login" />} />
        <Route path="/login" element={<LoginPage />} />
        <Route path="/confirm" element={<ConfirmUserPage />} />
        <Route path="/home" element={isAuthenticated() ? <HomePage /> : <Navigate replace to="/login" />} />
      </Routes>
    </BrowserRouter>
  );
};

export default App;

Note

It is important to imports connection ids: import config from "./config.json";

image

AuthService.tsx
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

import { CognitoIdentityProviderClient, InitiateAuthCommand, SignUpCommand, ConfirmSignUpCommand } from "@aws-sdk/client-cognito-identity-provider";
import config from "./config.json";

export const cognitoClient = new CognitoIdentityProviderClient({
  region: config.region,
});

export const signIn = async (username: string, password: string) => {
  const params = {
    AuthFlow: "USER_PASSWORD_AUTH",
    ClientId: config.clientId,
    AuthParameters: {
      USERNAME: username,
      PASSWORD: password,
    },
  };
  try {
    const command = new InitiateAuthCommand(params);
    const { AuthenticationResult } = await cognitoClient.send(command);
    if (AuthenticationResult) {
      sessionStorage.setItem("idToken", AuthenticationResult.IdToken || '');
      sessionStorage.setItem("accessToken", AuthenticationResult.AccessToken || '');
      sessionStorage.setItem("refreshToken", AuthenticationResult.RefreshToken || '');
      return AuthenticationResult;
    }
  } catch (error) {
    console.error("Error signing in: ", error);
    throw error;
  }
};

export const signUp = async (email: string, password: string) => {
  const params = {
    ClientId: config.clientId,
    Username: email,
    Password: password,
    UserAttributes: [
      {
        Name: "email",
        Value: email,
      },
    ],
  };
  try {
    const command = new SignUpCommand(params);
    const response = await cognitoClient.send(command);
    console.log("Sign up success: ", response);
    return response;
  } catch (error) {
    console.error("Error signing up: ", error);
    throw error;
  }
};

export const confirmSignUp = async (username: string, code: string) => {
  const params = {
    ClientId: config.clientId,
    Username: username,
    ConfirmationCode: code,
  };
  try {
    const command = new ConfirmSignUpCommand(params);
    await cognitoClient.send(command);
    console.log("User confirmed successfully");
    return true;
  } catch (error) {
    console.error("Error confirming sign up: ", error);
    throw error;
  }
};

image image

LoginPage.tsx
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { signIn, signUp } from './authService';

const LoginPage = () => {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [confirmPassword, setConfirmPassword] = useState('');
  const [isSignUp, setIsSignUp] = useState(false);
  const navigate = useNavigate();

  const handleSignIn = async (e: { preventDefault: () => void; }) => {
    e.preventDefault();
    try {
      const session = await signIn(email, password);
      console.log('Sign in successful', session);
      if (session && typeof session.AccessToken !== 'undefined') {
        sessionStorage.setItem('accessToken', session.AccessToken);
        if (sessionStorage.getItem('accessToken')) {
          window.location.href = '/home';
        } else {
          console.error('Session token was not set properly.');
        }
      } else {
        console.error('SignIn session or AccessToken is undefined.');
      }
    } catch (error) {
      alert(`Sign in failed: ${error}`);
    }
  };

  const handleSignUp = async (e: { preventDefault: () => void; }) => {
    e.preventDefault();
    if (password !== confirmPassword) {
      alert('Passwords do not match');
      return;
    }
    try {
      await signUp(email, password);
      navigate('/confirm', { state: { email } });
    } catch (error) {
      alert(`Sign up failed: ${error}`);
    }
  };

  return (
    <div className="loginForm">
      <h1>Welcome</h1>
      <h4>{isSignUp ? 'Sign up to create an account' : 'Sign in to your account'}</h4>
      <form onSubmit={isSignUp ? handleSignUp : handleSignIn}>
        <div>
          <input
            className="inputText"
            id="email"
            type="email"
            value={email}
            onChange={(e) => setEmail(e.target.value)}
            placeholder="Email"
            required
          />
        </div>
        <div>
          <input
            className="inputText"
            id="password"
            type="password"
            value={password}
            onChange={(e) => setPassword(e.target.value)}
            placeholder="Password"
            required
          />
        </div>
        {isSignUp && (
          <div>
            <input
              className="inputText"
              id="confirmPassword"
              type="password"
              value={confirmPassword}
              onChange={(e) => setConfirmPassword(e.target.value)}
              placeholder="Confirm Password"
              required
            />
          </div>
        )}
        <button type="submit">{isSignUp ? 'Sign Up' : 'Sign In'}</button>
      </form>
      <button onClick={() => setIsSignUp(!isSignUp)}>
        {isSignUp ? 'Already have an account? Sign In' : 'Need an account? Sign Up'}
      </button>
    </div>
  );
};

export default LoginPage;

image

Function: parseJwt

This function takes a JSON Web Token (JWT) as input and returns the decoded JSON payload.

The parseJwt function is used to extract the JSON payload from a JWT. A JWT is a string that contains three parts:

  • Header
  • Payload (also known as the claim set)
  • Signature

The function takes the JWT as a string and extracts the second part (the payload) by splitting the string at the dots (.). It then replaces any hyphens (-) and underscores (_) with their corresponding URL-safe characters (+ and /). Next, the function uses the window.atob method to decode the base64-encoded payload. The decoded payload is then converted to a string using the split`` and mapmethods. Themapmethod is used to convert each character in the string to its corresponding hexadecimal value using thecharCodeAtandtoString` methods.

Finally, the function uses the JSON.parse method to parse the decoded payload into a JSON object.

Note: The /*eslint-disable*/ comment at the top of the code is used to disable ESLint warnings for this specific function. This is because the function uses window.atob, which is not recommended in modern JavaScript code. However, in this case, it is used to maintain backwards compatibility with older browsers.

HomePage.tsx
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

import { useNavigate } from 'react-router-dom';

/*eslint-disable*/
function parseJwt (token) {
    var base64Url = token.split('.')[1];
    var base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
    var jsonPayload = decodeURIComponent(window.atob(base64).split('').map(function(c) {
        return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
    }).join(''));
    return JSON.parse(jsonPayload);
}

const HomePage = () => {
  const navigate = useNavigate();
  var idToken = parseJwt(sessionStorage.idToken.toString());
  var accessToken = parseJwt(sessionStorage.accessToken.toString());
  console.log ("Amazon Cognito ID token encoded: " + sessionStorage.idToken.toString());
  console.log ("Amazon Cognito ID token decoded: ");
  console.log ( idToken );
  console.log ("Amazon Cognito access token encoded: " + sessionStorage.accessToken.toString());
  console.log ("Amazon Cognito access token decoded: ");
  console.log ( accessToken );
  console.log ("Amazon Cognito refresh token: ");
  console.log ( sessionStorage.refreshToken );
  console.log ("Amazon Cognito example application. Not for use in production applications.");
  
  
  const handleLogout = () => {
    sessionStorage.clear();
    navigate('/login');
  };
/*eslint-enable*/

  return (
    <div>
      <h1>Hello World</h1>
      <p>See console log for Amazon Cognito user tokens.</p>
      <button onClick={handleLogout}>Logout</button>
    </div>
  );
};

export default HomePage;

image

ConfirmPage.tsx
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

import React, { useState } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import { confirmSignUp } from './authService';

const ConfirmUserPage = () => {
  const navigate = useNavigate();
  const location = useLocation();
  // eslint-disable-next-line
  const [email, setEmail] = useState(location.state?.email || '');
  const [confirmationCode, setConfirmationCode] = useState('');

  const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    try {
      await confirmSignUp(email, confirmationCode);
      alert("Account confirmed successfully!\nSign in on next page.");
      navigate('/login');
    } catch (error) {
      alert(`Failed to confirm account: ${error}`);
    }
  };

return (
  <div className="loginForm">
    <h2>Confirm Account</h2>
    <form onSubmit={handleSubmit}>
      <div>
        <input
          className="inputText"
          type="email"
          value={email}
          onChange={(e) => setEmail(e.target.value)}
          placeholder="Email"
          required
        />
      </div>
      <div>
        <input
          className="inputText"
          type="text"
          value={confirmationCode}
          onChange={(e) => setConfirmationCode(e.target.value)}
          placeholder="Confirmation Code"
          required />
      </div>
      <button type="submit">Confirm Account</button>
    </form>
  </div>
);

};

export default ConfirmUserPage;

References

Output

image

image

image

image

image

image

image

image

Resend code

image