Cruddur Google IdP Integration with AWS Cognito (without Hosted UI)

March 20, 2023

This article is based on the Cruddur app used in Andrew Brown's AWS Cloud Project Bootcamp.

I have implemented a solution that may not be perfect, but it serves its purpose. If you have any suggestions or encounter any issues, let me know.

Setup Google Auth Provider

Go to Google developer console and click on Select a project

select-project

To begin, click on the New Project button and provide a name for your project. Then, select Create to create the project. After the project is created, navigate to APIs & Services from the left-hand side menu. From there, select Credentials.

api-creds

Select Configure Consent Screen. Then, click on Create.

click-create

To set up the consent screen, provide the necessary details for the App Information and Developer Contact Information fields, then select Save and Continue three times (OAuth consent screen -> Scopes -> Test Users). After that, click on the Credentials tab from the left-hand menu. To create your OAuth 2.0 credentials, choose OAuth client ID from the Create credentials dropdown menu.

oauth-creds

To create your OAuth client, choose Web application as the application type and provide a name for your client. Then, select Create. Make sure to take note of Your client ID and Your Client Secret as you will need them for the next section. Finally, choose OK.

Add a social IdP to your user pool

To configure Google as your social IdP, first navigate to the Amazon Cognito console and select User Pools. Choose the cruddur-user-pool user pool from the list, and then click on the Sign-in experience tab. Under Federated sign-in, select Add an identity provider. From the list of options, choose Google and enter the app client ID and app client secret generated in the previous section. Finally, add the required authorized scopes as given below.

1
profile email openid

Map attributes from your IdP to your user pool.

Map attributes
User pool attributeGoogle attribute
emailemail
namename
preferred_usernamegiven_name
usernamesub

To add an identity provider, select Add identity provider. Then, from the App client integration tab, choose App client from the list and select Edit hosted UI settings.

Hosted UI

For now, add one URL http://localhost:3000 for the Allowed callback URLs and the Allowed sign-out URLs (optional).

Select Google from the Identity providers menu.

Make sure to set the OAuth 2.0 grant types to Implicit Grant. This specifies that the client should directly receive the access token (and optionally, the ID token based on scopes).

Implicit auth

There is a recommended option to use the authorization code, but in order to do so, we will need to implement Proof Key for Code Exchange (PKCE) in the backend, which will complicate things. Therefore, for now, stick with the option of sending the token directly to the client (Implicit grant).

  • From the OpenID Connect scopes menu, select OpenID and then aws.cognito.signin.user.admin, Email, and Profile.
  • Choose Save changes.

Note that we need to select aws.cognito.signin.user.admin for Sign Out to work. This scope is used to allow the user to sign out of their session from all devices. For more information, see this Stack Overflow post.

Although we have enabled Hosted UI, we won't be using it in our project. Hosted UI needs to be enabled for social IDP login to work. For more information, see Adding social identity providers to a user pool.

Add Cognito Domain to Google Developer Console

Go to the Google developer console. From the left navigation bar, choose Credentials.

Select the client you created in the first step and click the edit button.

add-domain-oauth

In the Authorized JavaScript origins field, enter your user pool domain.

In the Authorized redirect URIs field, enter your user pool domain with the /oauth2/idpresponse endpoint.

add-url-google

Click Save to save your changes.

Note: If you see an error message that says "Invalid Redirect: domain must be added to the authorized domains list before submitting" when adding the endpoint, please go to the authorized domains list and add the domain. More info: Social sign-in (OAuth)

Test your social IdP configuration

Go back to the cruddur-user-pool console and, from the Domain menu under the Actions dropdown, select Create Cognito domain.

Create Domain

Enter a unique domain prefix.

Unique Domain

To view the Hosted UI, update the URL given below with your credentials and use it to access the Hosted UI authentication page:

  • Replace <your_user_pool_domain> with the domain URL you just created
  • Replace <your_client_id> with the Client ID of your user pool app
1
https://<your_user_pool_domain>/login?response_type=token&client_id=<your_client_id>&redirect_uri=http://localhost:3000

This is the page that will be displayed when you visit this URL.

Hosted UI Google Button

Try signing in using this button. If you encounter any errors, make sure you followed all the previous steps correctly.

After a successful sign-up, you will be redirected to localhost:3000, and since nothing is running on port 3000, you will see a This site can't be reached error. Don't worry; we will fix this later.

Update App.js

Add this environment variable to the docker-compose.yml file under the frontend service:

1
REACT_APP_FRONTEND_URL: "https://3000-${GITPOD_WORKSPACE_ID}.${GITPOD_WORKSPACE_CLUSTER_HOST}";

Update the frontend-react-js/src/App.js file with the following changes:

1
Amplify.configure({
2
AWS_PROJECT_REGION: process.env.REACT_APP_AWS_PROJECT_REGION,
3
aws_cognito_region: process.env.REACT_APP_AWS_COGNITO_REGION,
4
aws_user_pools_id: process.env.REACT_APP_AWS_USER_POOLS_ID,
5
aws_user_pools_web_client_id: process.env.REACT_APP_CLIENT_ID,
6
oauth: {
7
domain: "<your_user_pool_domain_without_http>",
8
scope: ["email", "profile", "openid", "aws.cognito.signin.user.admin"],
9
redirectSignIn: process.env.REACT_APP_FRONTEND_URL,
10
redirectSignOut: process.env.REACT_APP_FRONTEND_URL,
11
responseType: "token", // or 'token', note that REFRESH token will only be generated when the responseType is code
12
},
13
14
Auth: {
15
// We are not using an Identity Pool
16
// identityPoolId: process.env.REACT_APP_IDENTITY_POOL_ID, // REQUIRED - Amazon Cognito Identity Pool ID
17
region: process.env.REACT_APP_AWS_PROJECT_REGION, // REQUIRED - Amazon Cognito Region
18
userPoolId: process.env.REACT_APP_AWS_USER_POOLS_ID, // OPTIONAL - Amazon Cognito User Pool ID
19
userPoolWebClientId: process.env.REACT_APP_CLIENT_ID, // OPTIONAL - Amazon Cognito Web Client ID (26-char alphanumeric string)
20
},
21
});

⚠️ Note: Make sure that the user pool domain does not have a HTTPS prefix.

Automatically set redirect URLs in cognito

Remember the step where we added localhost:3000 to the Allowed callback URLs and Allowed sign-out URLs - optional

We need the redirect URL in Cognito Hosted UI settings to be the same as our frontend URL. To achieve this, create a /bin folder inside frontend-react-js.

Inside the bin folder, create a bash script called auto-redirect to automatically update the URL whenever we open up a workspace in Gitpod. Add this command:

1
#! /usr/bin/bash
2
3
aws cognito-idp update-user-pool-client \
4
--user-pool-id $AWS_USER_POOLS_ID \
5
--client-id $CLIENT_ID \
6
--callback-urls https://3000-$GITPOD_WORKSPACE_ID.$GITPOD_WORKSPACE_CLUSTER_HOST \
7
--logout-urls https://3000-$GITPOD_WORKSPACE_ID.$GITPOD_WORKSPACE_CLUSTER_HOST \
8
--supported-identity-providers Google \
9
--allowed-o-auth-flows-user-pool-client \
10
--allowed-o-auth-flows implicit \
11
--allowed-o-auth-scopes {email,openid,profile,aws.cognito.signin.user.admin}

Ensure that you make the script executable by running chmod u+x ./frontend-react-js/bin/auto-redirect from the root folder.

Run this command from terminal and verify if you get a valid response and if changes have been made in cruddur-user-poolApp Integration → Select App client → Hosted UI

The Hosted UI status should be Available.

Hosted UI Available

Add this line to the .gitpod.yml file to enable the auto-redirect script to run automatically when starting a new workspace:

1
source "$THEIA_WORKSPACE_ROOT/frontend-react-js/bin/auto-redirect"

Adding Google Sign In button

Add the following CSS to SigninPage.css

1
@import url(https://fonts.googleapis.com/css?family=Roboto:500);
2
.google-btn {
3
cursor: pointer;
4
width: 184px;
5
height: 42px;
6
background-color: #4285f4;
7
border-radius: 2px;
8
box-shadow: 0 3px 4px 0 rgba(0, 0, 0, 0.25);
9
}
10
.google-btn .google-icon-wrapper {
11
position: absolute;
12
margin-top: 1px;
13
margin-left: 1px;
14
width: 40px;
15
height: 40px;
16
border-radius: 2px;
17
background-color: #fff;
18
}
19
.google-btn .google-icon {
20
position: absolute;
21
margin-top: 11px;
22
margin-left: 11px;
23
width: 18px;
24
height: 18px;
25
}
26
.google-btn .btn-text {
27
cursor: pointer;
28
float: right;
29
margin: 11px 11px 0 0;
30
background-color: transparent;
31
border: none;
32
font-size: 14px;
33
letter-spacing: 0.2px;
34
font-family: "Roboto";
35
}
36
.google-btn:hover {
37
box-shadow: 0 0 6px #4285f4;
38
}
39
.google-btn:active {
40
background: #1669f2;
41
}
42
.center-a-div {
43
display: flex;
44
justify-content: center;
45
align-items: center;
46
height: 300px;
47
}

Add the JSX code just below the closing tag of the form in pages/SigninPage.js

1
<div className="center-a-div">
2
<div className="google-btn" onClick={() => Auth.federatedSignIn({ provider: 'Google' })}>
3
<div className="google-icon-wrapper">
4
<img className="google-icon" src="https://upload.wikimedia.org/wikipedia/commons/5/53/Google_%22G%22_Logo.svg" />
5
</div>
6
<p class="btn-text"><b>Sign in with google</b></p>
7
</div>
8
</div>

This will create a 'Sign in with Google' button.

Google Login Button

Collect Access Token

Add this code to pages/HomeFeedPage.js

1
const getIdToken = async () => {
2
Auth.currentSession().then((res) => {
3
let accessToken = res.getAccessToken();
4
5
localStorage.setItem("access_token", accessToken.jwtToken);
6
7
loadData();
8
checkAuth();
9
});
10
};

Update React.useEffect to this:

1
React.useEffect(() => {
2
//prevents double call
3
if (dataFetchedRef.current) return;
4
dataFetchedRef.current = true;
5
6
getIdToken();
7
}, []);

Final Steps

Make sure the parameters in lambda trigger code is passed as List/tuples instead of using the unpacking operator otherwise you might run into some issues.

1
cur.execute(sql, parameters)

Change the order of localStorage.removeItem to the first code that’s executed inside the signOut function in components/ProfileInfo.js.

1
const signOut = async () => {
2
try {
3
localStorage.removeItem("access_token");
4
await Auth.signOut({ global: true });
5
window.location.href = "/";
6
} catch (error) {
7
console.log("error signing out: ", error);
8
}
9
};

I had issues with logout when localStorage.remove remove was called after Auth.signOut

Finally, run docker compose up to start the application. Click the "Sign in with Google" button to open the Google sign-in page. After signing in, you will be redirected back to the frontend.

working gif

Reference: