AWS Cognito + Azure AD + React + Amplify
Introduction
Recently I’ve had to uplift a solution to integrate its authentication into Azure AD.
While there have been several great blog posts on how to configure AWS Cognito to use Azure AD as a SAML Provider what happens after that has been sparse pickings.
Here are the good blogs that cover off configuring Cognito with Azure AD as this blog post will not re-invent this wheel:
- https://medium.com/@zippicoder/setup-aws-cognito-user-pool-with-an-azure-ad-identity-provider-to-perform-single-sign-on-sso-7ff5aa36fc2a
- https://www.idea11.com.au/how-to-set-up-aws-cognito-federation-office365/
- https://aws-amplify.github.io/docs/js/authentication
High Level Steps to Configure Azure AD as your SSO Provider of choice with an AWS Amplify React App using Cognito
The following are the end to end high level tasks you’ll need to undertake to get this working:
- Create your amplify / react application locally using the command lines / standard processes
- Use amplify to create a cognito user pool
- Setup your Enterprise Application in Azure AD
- Setup your Cognito User pool to use the Azure AD as its provider
- Setup your applications amplify authentication const correctly
- Setup Cognito to use the right Signin URL for SSO with Azure AD
- Setup a Button for Sign In if you haven’t come from Azure Portal.
Once you have done this then there will be two ways you can access your application.
- Via https://portal.microsoft.com as a tile provisioned by the Azure AD Enterprise Application
- Directly by hitting the applications URL
What happens differs between them both. I’m going to go over the reactions of the access models first. This should aid in your understanding later in this blog.
Via Microsoft Portal
The process to use Microsoft Portal to get to the application is as follows:
- Login to https://portal.microsoft.com with your Companys identity / login id.
- Find the Apps / All Apps section
- Locate the Application Tile
- Click it
What happens next is that Azure AD will use the settings in the Enterprise Application SAML SSO settings to work out what URLs are needed to be used to redirect you (the client) to the application.
Azure AD will do some verification around your capability to use the application in question (i.e. are you allowed to use it) and then create a SAML Token / Session that is then passed down to the WebApp.
The WebApp is redirected too with the SAML payload. The WebApp invokes Amplify and its sub method for checking Authentication. You will be seen as authenticated and the app will use the configuration in the aws-exports.js to know what URLs it should use for redirection.
Directly by hitting the applications URL
The process to use WebApps URL directly to get to the application is as follows:
- Open a browser
- Type the URL of the WebApp into the location bar
- Hit enter
What happens next is based on the following lines of code ( in the case of my baseline example code):
render() {
const { authState } = this.state;
// main return routine
return (
<div className="App">
{
// if authState is null display loading message.
authState === null && (<div>loading...</div>)
}
{
// if authState is set to signIn then show the login page with the single button for O365 javascript redirect
authState === 'signIn' && <OAuthButton/>
}
{
// if authState is signedIn then we've got a logged in user - lets start our app up!
// or rather lets just show a sign out button for now.. sigh.
authState === 'signedIn' ?
(
<div class='signout'>
<button onClick={this.signOut}>Sign out of application {user_givenname} {user_email}</button>
</div>
) : null
}
</div>
);
}
A constant is being set called authState from this.state:
const { authState } = this.state;
This has been created by the other methods in the class. If the authState is null show Loading.. If the authState is SignIn then you need to login so we display a login button. If the authState is SignedIn then you are logged on so we show the app, or in our case a sign out button with some extra info in it as a test.
Ok so how did authState get set?
Yes you are correct in thinking its the typical Amplify mechanisms in play as defined here: https://aws-amplify.github.io/docs/js/authentication
However the missing piece to the puzzle lies in three places:
- The “oauth”: key in the awsmobile constant in the aws-exports.js needs a jiggle
- The sign in URL in the Azure AD SSO settings need a tweak
- Login button uses a “Javascript redirect” to Azure, not the withOAuth() method that normally redirects to a Cognito Hosted UI. We’re avoiding the withOAuth() to get seamless sign on if you’re authenticated already.
awsmobile oauth changes required
Even though this says not to manually edit this file; you need too. As you are unlikely to modify the amplify add auth you did once it has been set up this addition should stay there and be somewhat safe.
One needs to add the client_id into the oauth declaration and the responseType must be Token. As follows:
//file: aws-exports.js // WARNING: DO NOT EDIT. This file is automatically generated by AWS Amplify. It will be overwritten. const awsmobile = { "aws_project_region": "ap-southeast-2", "aws_cognito_identity_pool_id": "ap-southeast-2:d177381b-d5b4-46b6-ae0b-12345678", "aws_cognito_region": "ap-southeast-2", "aws_user_pools_id": "ap-southeast-2_weoweo", "aws_user_pools_web_client_id": "iamaclientidnot", "oauth": { "domain": "mydomain-dev.auth.ap-southeast-2.amazoncognito.com", "scope": [ "phone", "email", "openid", "profile", "aws.cognito.signin.user.admin" ], "redirectSignIn": "https://testapp.dunlop.geek.nz/", "redirectSignOut": "https://testapp.dunlop.geek.nz/", "responseType": "token", "client_id": "thecognitoclientidgoeshere" }, "federationTarget": "COGNITO_USER_POOLS", "aws_content_delivery_bucket": "abucket-dev", "aws_content_delivery_bucket_region": "ap-southeast-2", "aws_content_delivery_url": "https://aurl.cloudfront.net" }; export default awsmobile;
Azure AD SSO SignIn URL Tweak
The Azure AD Enterprise Application Sign On URL needs to be set in the following format:
https://COGNITOAUTHDOMAIN/oauth2/authorize?identity_provider=COGNITOSAMLIDENTITYPROVIDERNAME&redirect_uri=https://testapp.dunlop.geek.nz/&response_type=TOKEN&client_id=COGNITOCLIENTID
Please note: &scope=aws.cognito.signin.user.admin%20%email%20%openid%20%phone%20%profile has been removed from the URL as it caused an Error Code 0 failure thanks to an Azure AD back end change by Microsoft on or around the 10th of June 2019.
Javascript redirect
The redirect on the button example looks like this in the oauthbutton.js file. Note: the withOAuth() doesn’t really matter.
// file: OAuthButton.js import { withOAuth } from 'aws-amplify-react'; import { withRouter } from 'react-router-dom'; import React, { Component } from 'react'; class OAuthButton extends React.Component { handleClick() { // do something meaningful, Promises, if/else, whatever, and then window.location.assign('https://weo-dev.auth.ap-southeast-2.amazoncognito.com/oauth2/authorize?identity_provider=AzureAD&redirect_uri=https://testapp.dunlop.geek.nz/&response_type=TOKEN&client_id=IAMANID&scope=aws.cognito.signin.user.admin email openid phone profile'); } render() { return ( <div class='login'> <button onClick={this.handleClick}>Log back into application with O365</button> </div> ) } } //export default OAuthButton; export default withOAuth(OAuthButton);
App.js example
/* =================================================== App.js : Core App.JS with Azure AD integrated authentication Author: Zenstorm Date: 10/05/2019 Version: 1.00 */ // =================================================== // Imports // from libraries: import React, { Component } from 'react'; import Amplify, {Auth, Hub} from 'aws-amplify'; import awsmobile from './aws-exports'; // Amplify configuration import { I18n, ConsoleLogger as Logger } from '@aws-amplify/core'; // from files: import OAuthButton from './OAuthButton'; // Our login button "aka form" for now import './App.css'; // Custom Style Sheet // =================================================== // setup Amplify and its Auth model Amplify.configure(awsmobile); Auth.configure({ awsmobile }); // https://aws-amplify.github.io/amplify-js/api/classes/authclass.html // =================================================== // set up logging properly so we can use logger.debug logger.error logger.info etc const logger = new Logger('AppLog','INFO'); // =================================================== // Main class for our Application that gets injected into the root in index.html via index.js class App extends Component { // setup props constructor(props) { super(props); this.signOut = this.signOut.bind(this); } // define vars in state (could be done in props constructor) state = { authState: 'signIn', // used to check login in render() token: null, // used to check if we've got a valid login user: null // used to store the user object returned from currentAuthenticatedUser() } // ==================================================== // getuserinfo(): custom function to get tokencode // getuserinfo function to retrieve via amplify & cognito the currentAuthenticatedUser getuserinfo = async () => { // call a promise to waith for currentAuthenticatedUser() to return const user = await Auth.currentAuthenticatedUser(); // do a debug log entry logger.debug(user); // setup some variables out of our current user object const token = user.signInUserSession.idToken.jwtToken; const user_givenname = user.attributes.name; const user_email = user.attributes.email; // set the variables into the classes state. this.setState({ token: token }); this.setState({ user: user }); this.setState({ user_givenname: user_givenname }); this.setState({ user_email: user_email }); } // ==================================================== // componentDidMount(): react core function // is invoked immediately after a component is mounted // https://reactjs.org/docs/react-component.html componentDidMount() { // Setup a hub listenr on the auth events // https://aws-amplify.github.io/docs/js/hub Hub.listen("auth", ({ payload: { event, data } }) => { switch (event) { case "signIn": this.setState({ authState: 'signedIn'}); this.getuserinfo(); break; case "signOut": this.setState({ authState: 'signIn'}); this.setState({ user: null }); break; } }); } // ==================================================== // signOut() : used to sign out user // custom sign out function; has been bound in constructor(props) as well signOut() { Auth.signOut() .then(() => { this.setState({ authState: 'signIn'}); this.setState({ user: null }); }) .catch(err => { logger.error(err); }); } // ==================================================== // render(): mandatory react render function render() { // vars for fun - should be buried in app const { authState } = this.state; const { token } = this.state; const { user_givenname } = this.state; const { user_email } = this.state; // main return routine return ( <div className="App"> { // if authState is null display loading message. authState === null && (<div>loading...</div>) } { // if authState is set to signIn then show the login page with the single button for O365 javascript redirect authState === 'signIn' && <OAuthButton/> } { // if authState is signedIn then we've got a logged in user - lets start our app up! // or rather lets just show a sign out button for now.. sigh. authState === 'signedIn' ? ( <div class='signout'> <button onClick={this.signOut}>Sign out of application {user_givenname} {user_email}</button> </div> ) : null } </div> ); } } // Export the App. No Auth Wrapper required for AzureAD as this is undertaken in the OAuthButton.js export default App;