Single Sign-On with Azure AD and Amazon Cognito using OIDC and AWS Amplify
Tags: serverless, sso, amplify, aws
Published: 2023-05-17 08:17:11

Single sign-on (SSO) is often the preferred way of accessing applications as it relieves users from the burden of having to remember yet another, probably insecure password. In my latest project, it was very convenient to have SSO as all our users are using Microsoft accounts. We implemented identity federation between Azure AD and our Cognito User Pool using OpenID Connect. This post will quickly show how to do it according to best practices using the [amplify-js](https://github.com/aws-amplify/amplify-js) library, while also including CloudFormation templates for the User Pool and accompanying resources.

My assumptions about the readers of this post:

- You have your own Azure Active Directory running and are able to configure an App Registration
- You have an AWS account and aws-cli configured
- You already have or can run a simple front-end project in any of the popular front-end frameworks

## Setting up an App Registration in Azure AD

In the Azure Portal, we need to create a new App Registration that represents our application and will be used as an OIDC provider with Cognito. After creating it, we will also need to generate a new secret under the "Certificates & secrets" section. Once generated, copy the secret and securely store it in the [Systems Manager Parameter Store](https://docs.aws.amazon.com/systems-manager/latest/userguide/systems-manager-parameter-store.html) in your AWS account. Do the same with the client id, which is located in the Overview page of your App Registration under the label "Application (client) ID". We will reference it together with the client id in the CloudFormation template in the next section, so make sure to give these two parameters some meaningful names, for example `my-azure-clientid` and `my-azure-clientsecret`.

## Setting up Amazon Cognito

All of the required AWS resources will be defined in a single CloudFormation stack, e.g. in a file named `cognito.yml`. First, we define a User Pool and a User Pool Domain:

```yml
Resources:
  MyUserPool:
    Type: AWS::Cognito::UserPool
    Properties:
      UserPoolName: my-user-pool
  MyUserPoolDomain:
    Type: AWS::Cognito::UserPoolDomain
    Properties:
      Domain: my-user-pool # domain name
      UserPoolId: !Ref MyUserPool
```

The User Pool Domain will be referenced by Azure AD during the authentication flow. When deployed, the domain will receive a value similar to `https://my-user-pool.auth.{region}.amazoncognito.com`.

After that, we add an OIDC User Pool Identity Provider and a corresponding User Pool Client in the `cognito.yml`:

```yml
MyUserPoolIdentityProvider:
  Type: AWS::Cognito::UserPoolIdentityProvider
  Properties:
    UserPoolId: !Ref MyUserPool
    ProviderName: "MyCompany" # your custom name
    ProviderType: "OIDC"
    ProviderDetails:
      client_id: ${ssm:my-azure-clientid}
      client_secret: ${ssm:my-azure-clientsecret}
      attributes_request_method: 'GET'
      oidc_issuer: 'https://login.microsoftonline.com/{tenant_id}/v2.0'
      authorize_scopes: 'email profile openid'
    AttributeMapping:
      name: 'name'
      preferred_username: 'preferred_username'
      given_name: 'unique_name'
      email: 'email'
      username: 'sub'
```

Pay attention to the `oidc_issuer` field below - the `tenant_id` should be swapped with the real value under the label "Directory (tenant) ID" on the App Registration's Overview page. The AttributeMapping section specifies how to map certain attributes from the authenticated user from AD to the Cognito User Pool. That way, we automatically capture the user's name and email address, for example.

The final component to make this come together is a User Pool Client, specified below:

```yml
MyUserPoolClient:
  Type: AWS::Cognito::UserPoolClient
  DependsOn: MyUserPoolIdentityProvider
  Properties:
    AllowedOAuthFlows:
      - code
    AllowedOAuthFlowsUserPoolClient: True
    AllowedOAuthScopes:
      - email
      - openid
      - profile
    CallbackURLs:
      - 'http://localhost:4200'
    LogoutURLs:
      - 'http://localhost:4200'
    ClientName: my-user-pool-client
    PreventUserExistenceErrors: "ENABLED"
    SupportedIdentityProviders:
      - MyCompany # Refer to the OIDC provider name
    UserPoolId: !Ref MyUserPool
```

This approach uses the authorization_code flow, which is defined under `AllowedOAuthFlows`. You probably notice that `CallbackURLs` and `LogoutURLs` point to localhost, and by the port value, you can guess that I have an Angular single-page application to test this locally. These two values contain the addresses where the user will be redirected after a successful authentication and logout, respectively. If your front-end app is deployed somewhere, these values should reflect that remote address.

Deploy this template using aws-cli using the command below:
```shell
aws cloudformation deploy --stack-name my-cognito --template-file cognito.yml
```

After successful deployment, there is still one more thing to configure in the Azure Portal before the whole flow works. Remember the Cognito User Pool Domain? That is the final piece that will make the AD "see" our User Pool.

That is configured in the App Registration under Authentication. When clicking the "Add platform" button, a side sheet will open with several options of web and mobile applications to choose from. **Even though we are creating a single-page application, the correct choice here is "Web". That is because the AD is actually interacting with Cognito, not with our single-page app directly.** After picking the Web platform, in the Redirect URIs input field we paste our User Pool Domain, appended with **"/oauth2/idpresponse"** so it looks like `https://my-user-pool.auth.{region}.amazoncognito.com/oauth2/idpresponse`.

## Using @aws-amplify/auth library

Why do I think you should use this library? Firstly, it performs all the communication with [the Cognito API](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-reference.html). More importantly, it does so using security best current practices, like using [authorization code with PKCE](https://auth0.com/docs/get-started/authentication-and-authorization-flow/authorization-code-flow-with-proof-key-for-code-exchange-pkce).

As I mentioned, I used Angular to test this, but any other framework should have it in a similar way. After adding `@aws-amplify/auth` and `@aws-amplify/core` dependencies to the project, Amplify Auth should be configured:

```javascript
Amplify.configure({
  Auth: {
    region: '',
    userPoolId: '',
    userPoolWebClientId: '',
    oauth: {
      domain: '',
      scope: ['openid', 'email', 'profile'],
      redirectSignIn: 'http://localhost:4200',
      redirectSignOut: 'http://localhost:4200',
      responseType: 'code',
    },
  },
});
```

The properties `userPoolId`, `userPoolWebClientId`, and `domain` can be found in the AWS Console under the Cognito deployment, and `region` is the AWS region where the stack is deployed. On your app's login page, a button to initiate the login flow should call the following method:

```javascript
Auth.federatedSignIn({ customProvider: 'MyProvider' });
```

The user will be redirected to log in with the provider (Microsoft), after which they will be redirected back to the app where Amplify Auth will automatically exchange the given authorization code for tokens from Cognito. All of this is done securely and according to the best practices.

To get the currently logged-in user, a call to the `Auth.currentAuthenticatedUser()` method should do the job.

## Conclusion

By following the steps outlined in this post, you can set up the necessary AWS resources and Azure configurations to try out SSO using the amplify-js library. It's worth noting an interesting observation: [Cognito pricing](https://aws.amazon.com/cognito/pricing/) for users logging in with SAML or OIDC is almost 3 times higher compared to regular users. Also, the free tier is capped at only 50 OIDC monthly active users, compared to 50k regular monthly active users. For example, if you are out of the free tier, and have 1000 monthly active users using OIDC, that would cost $15. 

I hope you found this guide helpful and informative. If you have any questions or would like to share your experiences with implementing SSO using Cognito, I invite you to leave your comments below. I look forward to engaging with you and addressing any inquiries you may have.