Adam M. Lechnos

DevOps engineer with an Infosec Consulting and Finance hobbyist background.

AWS CDK - Using Amazon Cognito Authentication and Authorization

22 Feb 2023 » aws, devops, cdk, typescript

Diagram

Amazon Cognito Client Workflow (draw.io viewer)

Amazon Cognito Client Workflow

In my previous blog post, “AWS CDK - Testing Amazon Cognito Authentication and Authorization”, I go-over testing the Auth Service stack, ensuring we receive back proper JWT tokens. I will now illustrate how to make use of the Auth Service stack within a hypothetical React application.

Components

ComponentFile NameDescription
Authentication ServiceAuthService.tsPerform user challenge and authentication and authorization, returning JWT Tokens and AWS Credentials.
Login ComponentLoginComponent.tsxA very simple react component, which will perform the authentication, using the AuthService.ts service file.
Frontend RouterApp.tsxA very simple React frontend web application which will present a router, with ‘/login’ making a call to LoginComponent.tsx.

Overview

The components are stored and bundled into an S3 Bucket and served as static content. Hypothetically, in addition to the Authentication Service, APIs called via API Gateway can be used to trigger Lambda functions, which can make use of the custom scopes as injected by a Cognito Identity Pool client and/or, the ID Token used as an Authorization header to the API, whereby API Gateway can desingate Cognito as an Authorizer to check for access. We may also parse group memberships within the token to determine whether certain API methods are allowed or not.

Breaking It Down

Authentication Service

The authentication service code has been discussed at length, from the previous two blog posts, we perform a deep dive into creating a testing the auth service stack. The code here is identical to what we previously covered.

Some additional changes made to the AuthService file are as follows:

private async generateTemporaryCredentials() {
    const cognitoIdentityPool = `cognito-idp.${awsRegion}.amazonaws.com/${AuthStack.SpaceUserPoolId}`;
    const cognitoIdentity = new CognitoIdentityClient({
        credentials: fromCognitoIdentityPool({
            clientConfig: {
                region: awsRegion
            },
            identityPoolId: AuthStack.SpaceIdentityPoolId,
            logins: {
                [cognitoIdentityPool]: this.jwtToken!
            }
        })
    });
    const credentials = await cognitoIdentity.config.credentials();
    return credentials;
}
  • The function above will use Cognito for access to the temporary AWS Credentials by supplying the JWT Auth Token. When the Web Application wants to perform a task on behalf of the user, such as uploading a photo to an S3 bucket, these credentials may be used.
    • The Assumed IAM Role for the Credentials will be the Authenticated default for Cognito. Any additional Cognito groups with designated IAM roles will also provide those permissions as well.
      • This is useful for delineating group permissions according to membership type, such as premium vs non-premium members.

Login Component

Here we break down the code used for performing the login and ensuring the user authenticated. We make extensive use of State Hooks, which provide a clean way to initialize and update states across our web app.

type LoginProps = {
  authService: AuthService;
  setUserNameCb: (userName: string) => void;
};

export default function LoginComponent({ authService, setUserNameCb }: LoginProps) {
  const [userName, setUserName] = useState<string>("");
  const [password, setPassword] = useState<string>("");
  const [errorMessage, setErrorMessage] = useState<string>("");
  const [loginSuccess, setLoginSuccess] = useState<boolean>(false);
  • A function labeled,LoginComponent is created with one argument of type LoginProps. We defined the ‘type’ above, via the type LoginProps line.
    • Hence, this one argument will require a type which contains two components, ‘authService’ of type ‘AuthService’, and ‘setUserNameCb’ which is a callback function that takes a string without a return.
  • We then initialize four State Hooks, each empty (and boolean as false) accessible via userName, password, errorMessage, and loginSuccess.
    • We can now set and use these variables by calling their ‘setIndex’ for setting the state, and referencing each variable where they each apply.
const handleSubmit = async (event: SyntheticEvent) => {
    event.preventDefault();
    if (userName && password) {
      const loginResponse = await authService.login(userName, password);
      const userName2 = authService.getUserName();
      if (userName2) {
        setUserNameCb(userName2);
      }

      if (loginResponse) {
        setLoginSuccess(true);
      } else {
        setErrorMessage("invalid credentials");
      }
    } else {
      setErrorMessage("UserName and password required!");
    }
  };
  • The handleSubmit will take in one ‘event’ argument, a SyntheticEvent type which will be explained further below. This will grab the end-user action and perform the following:
    • If the user provided a username and password within the login form, the loginResponse variable will perform a call to the authService.login method, which was supplied by the ‘LoginProps’ type argument.
    • The method is supplied with the username and password, now being handed off to the Authentication Service component, will use Cognito to authenticate the user, then store the JWT tokens and make available the AWS Credentials.
    • The ‘username2’ variable will contain the username if the authService.getUserName() succeeds. If set, set as the argument to the setUserNameCb callback supplied to the ‘LoginProps’ type argument.
    • If LoginResponse was populated from the Auth Service login function, the State Hook for loginSuccess variable gets called, setLoginSuccess, setting it to ‘true’.
return (
    <div role="main">
      {loginSuccess && <Navigate to="/profile" replace={true} />}
      <h2>Please login</h2>
      <form onSubmit={(e) => handleSubmit(e)}>
        <label>User name</label>
        <input
          value={userName}
          onChange={(e) => setUserName(e.target.value)}
        />
        <br/>
        <label>Password</label>
        <input
          value={password}
          onChange={(e) => setPassword(e.target.value)}
          type="password"
        />
        <br/>
        <input type="submit" value="Login" />
      </form>
      <br/>
      {renderLoginResult()}
    </div>
  );
  • The Login Component returns the React frontend component for logging in, the form for supplying the username and password, and a Submit button to set the variables for username and password via the Set Hooks, setUsername and setPassword.
    • Notice the event being supplied to the ‘handleSubmit’ function ‘event’ parameter as e, which carries over each of their respective value vars supplied by the form to an anonymous function, which is supplied as e.target.value to each of the State Hooks’ ‘setIndexes’, such as setUserName(e.target.value).
    • CLicking ‘Submit’ triggers the ‘setIndex’ calls to the handleSubmit function.

Application Router

Finally, the Application Router presents the code required for rendering a component based on the URL path.

const authService = new AuthService();
const dataService = new DataService(authService);

function App() {
  const [userName, setUserName] = useState<string | undefined>(undefined);

  const router = createBrowserRouter([
    {
      element: (
        <>
          <NavBar userName={userName}/>
          <Outlet />
        </>
      ),
      children:[
        {
          path: "/",
          element: <div>Hello world!</div>,
        },
        {
          path: "/login",
          element: <LoginComponent authService={authService} setUserNameCb={setUserName}/>,
        },
        {
          path: "/profile",
          element: <div>Profile page</div>,
        },
        {
          path: "/createSpace",
          element: <CreateSpace dataService={dataService}/>,
        },
        {
          path: "/spaces",
          element: <Spaces dataService={dataService}/>,
        },
      ]
    },
  ]);
  • Here, we have a router which will perform a component depending on where the user is redirected. Their would be a navbar at the entry point, which then routes to each of the provided path values. Based on the ‘path’, the element is dynamically rendered.
  • For path: "/login", the LoginComponent service is called as follows: LoginComponent authService={authService} setUserNameCb={setUserName}
    • Notice the argument adheres to the LoginProps type declared in the LoginComponents above, with the authService being the imported AuthService() function assigned to the variable at the top of the code, along with,
    • setUserNameCb, which is a callback function, called back by the LoginComponent when its username form field is set. The callback triggers the ‘setIndex’ for the userName State Hook: const [userName, setUserName] = useState<string | undefined>(undefined).
    • Recall the LoginComponent’s function decleration, which declared the paramters as function LoginComponent({ authService, setUserNameCb }: LoginProps).
      • setUserNameCb in LoginComponent is the triggers the setUserName ‘setIndex’ in App.tsx, as it’s called back.
      • LoginProps type declared ‘setUserNameCb’ as an anonymous function, returning void as: setUserNameCb: (userName: string) => void

Original code snippets by Alex Dan via Udemy

Buy Me A Coffee