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 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 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:
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
:
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:
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:
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. More importantly, it does so using security best current practices, like using authorization code with 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:
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:
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 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.
Top comments (0)