Integrating Google Sign-In with Amazon Cognito & Next.js: A Comprehensive OAuth 2.0 Tutorial

Nhyl Bryle Ibañez
13 min readDec 22, 2023

--

No Hosted UI, no client-side authentication with AWS Amplify, just your no-BS guide in implementing a Google Sign-In on the server using Amazon Cognito & Next.js. If you want to skip the hassle of reading the overwhelming documentation of Amazon Cognito then this guide is for you.

Note: This article assumes that you have initialized a Next.js with Tailwind project. If you haven’t yet, you can initialize your Next.js project here. This article also assumes that you have an AWS account.

Overview

Ever wondered how Google Sign In works under the hood? OAuth 2.0, OpenID Connect, you might have come across these terms while trying to setup your application’s social sign in. Before we dive in, here’s a simplified breakdown to help you understand the process:

OAuth 2.0 Flow

1. User Authentication and Consent:

  • The client application initiates Google Sign-In redirecting the user to Google’s authorization server.
  • The user authenticates and grants permission to client application to access their Google account.

2. Google Authorization Server:

  • Google’s authorization server issues an authorization code to the client after successful authentication and consent.

3. Exchange Authorization Code with Cognito:

  • The client takes the authorization code and exchanges it with Amazon Cognito’s authorization server (token endpoint) to obtain Cognito-specific tokens.

4. Cognito as OAuth 2.0 Provider:

  • Amazon Cognito validates the authorization code from Google and issues its own tokens, including an ID token and an access token.

5. Access Cognito-Protected Resources:

  • The client can then use the obtained tokens to access Cognito-protected resources, such as AWS services or APIs.

If the quick overview didn’t help much, the only thing to do left is get our hands dirty.

Setup

Before implementing a Google Sign In in your project, we must set up the necessary prerequisites. The following subsections will guide you in creating your Amazon Cognito User Pool, registering your application with Google, and integrating them by adding Google as a social identity provider with the User Pool you will create.

If you have already done these steps, then you can skip the following sections and proceed to the Google Sign In part.

Creating your Amazon Cognito User Pool

1. In your Amazon Cognito Console, choose Create user pool.

2. In Configure sign-in experience, choose the Federated identity providers.

3. At Cognito user pool sign-in options, you can select Email as the minimum. Then select Google at Federated sign-in options.

4. In Configure security requirements, you can leave Cognito defaults checked for the Password policy.

5. For testing purposes, choose No MFA for Multi-factor authentication.

6. Leave the User account recovery as it is.

7. In Configure sign-up experience section, keep the settings unchanged.

8. In Configure message delivery, choose Send email with Cognito for Email.

9. Skip Connect federated identity providers for now.

10. In Integrate your app, enter your desired User pool name and Cognito domain name.

11. Choose Confidential client in Initial app client, and enter a friendly App client name. Make sure Generate a client secret is selected.

12. Enter http://localhost:3000/api/auth/callback as your URL in Allowed callback URLs. Leave the rest of the settings unchanged.

13. Proceed to Review and create section and create your user pool.

Register your application with Google

1. Go and sign in to the Google Cloud Platform Console.

2. Choose Select a project from the top navigation bar and select NEW PROJECT.

3. Enter your desired Project name and choose CREATE.

4. On the left navigation bar, choose APIs and Services, then OAuth consent screen. Make sure your project is selected.

5. Choose External in User Type and choose CREATE.

6. Enter your desired App information, App Logo, and App Domain details.

7. In authorized domains, enter amazoncognito.com. Input your desired email address for Developer contact information. Then choose SAVE AND CONTINUE.

8. In Scopes, choose ADD OR REMOVE SCOPES.

9. Choose the following minimum OAuth scopes: …/auth/userinfo.email, …/auth/userinfo.profile, and openid.

10. Choose UPDATE and then proceed to SAVE AND CONTINUE.

11. In Test users, choose ADD USERS and enter the email addresses of your desired authorized test users. Choose SAVE AND CONTINUE.

12. On the left navigation bar, choose APIs and Services, then Credentials.

13. Choose CREATE CREDENTIALS then OAuth client ID.

14. Choose the Web application for Application type and enter your desired client name. Under Authorized Javascript origins, add the following URIs:

- https://<your-user-pool-domain>
- http://localhost
- http://localhost:3000

You can find your user pool domain in the App integration tab of your Cognito console. Ex: https://<name>.auth.<region>.amazoncognito.com

15. Under Authorized redirect URIs, choose ADD URI and enter https://<your-user-pool-domain>/oauth2/idpresponse. Choose CREATE.

16. Securely store your client ID and client secret as you will need it in the next section.

Adding Google as social Identity Provider to your Cognito User Pool

1. In your Amazon Cognito console, navigate to the user pool we have created earlier. Choose the Sign-in experience tab.

2. Under the Federated identity provider sign-in, choose Add identity provider.

3. Choose Google.

4. Add the client ID and client secret generated in the previous section. Under Authorized scopes, enter “profile email openid”. Note that the scopes should be separated with spaces.

5. Under Map attributes between Google and your user pool, choose email for your Google attribute. Then, choose Add identity provider.

6. Navigate to the App integration tab. At the bottom of the page, under App client list, choose your app client.

7. Locate the Hosted UI and choose Edit.

8. Under Identity Providers, choose Select identity providers and select Google.

9. In OpenID Connect scopes, make sure that Email, OpenID, and Profile are selected. Then choose Save changes.

Signing In With Google

Environment Variables

To implement a Google Sign In, we must first set the environment variables we will need in our project.

  • Cognito Domain: Located in the App integration tab of your Cognito console. Ex: https://<name>.auth.<region>.amazoncognito.com
  • App Client ID: At the bottom of the App integration page, choose your app client from the App client list. Locate your Client ID in the App client information section.
  • App Client secret: If you have found your Client ID, the Client secret should just be directly below it.

Your .env.local file should look like this:

// .env.local

COGNITO_DOMAIN=<your-cognito-domain>
COGNITO_APP_CLIENT_ID=<your-app-client-id>
COGNITO_APP_CLIENT_SECRET=<your-app-client-secret>

Authorize Endpoint

The GET /oauth2/authorize endpoint is a redirection point that will redirect your user to the Google Sign In page. You will need to call this API route in the client.

We must create our GET request to the /oauth2/authorize endpoint with the parameters: response_type, client_id, redirect_uri, state, identity_provider, and scope. The most important parameter here is the identity_provider as the inclusion of this will redirect your user to your identity provider’s sign-in page and not the Hosted UI. Visit the Authorize endpoint reference for more information.

// app/api/auth/google-sign-in/route.ts

import { NextRequest, NextResponse } from "next/server"
import crypto from 'crypto'

const {
COGNITO_DOMAIN,
COGNITO_APP_CLIENT_ID
} = process.env

export async function GET(request: NextRequest) {
let authorizeParams = new URLSearchParams()
const origin = request.nextUrl.origin

const state = crypto.randomBytes(16).toString('hex')

authorizeParams.append('response_type', 'code')
authorizeParams.append('client_id', COGNITO_APP_CLIENT_ID as string)
authorizeParams.append('redirect_uri', `${origin}/api/auth/callback`)
authorizeParams.append('state', state)
authorizeParams.append('identity_provider', 'Google')
authorizeParams.append('scope', 'profile email openid')

return NextResponse.redirect(`${COGNITO_DOMAIN}/oauth2/authorize?${authorizeParams.toString()}`)
}

To call your API route on the client, we can make a GET request using the form tag. Your front-end code could be as simple as this:

// /app/login/page.tsx

export default function Page() {
return (
<div className="min-h-screen bg-gray-100 flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
<form
className="max-w-md w-full space-y-8 p-6 bg-white shadow rounded-md"
action="/api/auth/google-sign-in"
method="GET"
>
<div className="text-center">
<h2 className="mt-6 text-3xl font-bold text-gray-900">Sign In</h2>
<p className="mt-2 text-sm text-gray-600">Please sign in with your Google account.</p>
</div>
<div className="mt-8 space-y-6">
<button
className="w-full py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50"
type="submit"
>
Sign In With Google
</button>
</div>
</form>
</div>
)
}

Clicking your Sign In With Google button should lead you to the Google Sign In Page.

Token Endpoint

After your user signs in with their chosen account, your callback function will receive an authorization code in the search params which you will use for the next step: making a POST request to the /oauth2/token endpoint. For more details about the token endpoint, visit the Token endpoint reference.

// app/api/auth/callback/route.ts

import { NextResponse, type NextRequest } from 'next/server'
import { cookies } from 'next/headers'

const {
COGNITO_DOMAIN,
COGNITO_APP_CLIENT_ID,
COGNITO_APP_CLIENT_SECRET
} = process.env

export async function GET(request: NextRequest) {
try {
const origin = request.nextUrl.origin
const searchParams = request.nextUrl.searchParams
const code = searchParams.get('code') as string

if (!code) {
const error = searchParams.get('error')
return NextResponse.json({ error: error || 'Unknown error' })
}

const authorizationHeader = `Basic ${Buffer.from(`${COGNITO_APP_CLIENT_ID}:${COGNITO_APP_CLIENT_SECRET}`).toString('base64')}`

const requestBody = new URLSearchParams({
grant_type: 'authorization_code',
client_id: COGNITO_APP_CLIENT_ID as string,
code: code,
redirect_uri: `${origin}/api/auth/callback`
})

// Get tokens
const res = await fetch(`${COGNITO_DOMAIN}/oauth2/token`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Authorization': authorizationHeader
},
body: requestBody
})

const data = await res.json()

if (!res.ok) {
return NextResponse.json({
error: data.error,
error_description: data.error_description
})
}

// Store tokens in cookies
const cookieStore = cookies()
cookieStore.set('id_token', data.id_token)
cookieStore.set('access_token', data.access_token)
cookieStore.set('refresh_token', data.refresh_token)

return NextResponse.redirect(new URL('/', request.nextUrl))
} catch (error) {
return NextResponse.json({ error: error })
}
}

A successful request to the token endpoint yields ID, access, and refresh tokens. The presence of the access token signifies the signed-in state of your user. You should also be redirected back to your home page and to further check if your request was successful, go to your browser’s developer tools and select the storage tab to check your cookies. You will see your access_token, id_token, and refresh_token cookies with their corresponding values.

Note: For simplicity, this guide implements the usage of cookies, but it’s important to be aware that this approach introduces security risks, including vulnerabilities to cross-site scripting attacks (XSS). Learn more about enhancing the security of your cookies here.

Sign Out

Revoke Endpoint

Of course, you have to let your users sign out of your application. To implement a sign-out functionality, you will need to delete our cookies that contain the credentials and revoke our access tokens by making a POST request to the /oauth2/revoke endpoint.

// app/api/auth/signout/route.ts

import { NextRequest, NextResponse } from "next/server";
import { cookies } from "next/headers";

const {
COGNITO_DOMAIN,
COGNITO_APP_CLIENT_ID,
COGNITO_APP_CLIENT_SECRET
} = process.env

export async function GET(request: NextRequest) {
const cookieStore = cookies()

const idTokenExists = cookieStore.has('id_token')
const accessTokenExists = cookieStore.has('access_token')
const refreshTokenExists = cookieStore.has('refresh_token')

if (!refreshTokenExists) {
return NextResponse.redirect(new URL('/login', request.nextUrl))
}

const token = cookieStore.get('refresh_token')
const authorizationHeader = `Basic ${Buffer.from(`${COGNITO_APP_CLIENT_ID}:${COGNITO_APP_CLIENT_SECRET}`).toString('base64')}`

const response = await fetch(`${COGNITO_DOMAIN}/oauth2/revoke`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Authorization': authorizationHeader,
},
body : new URLSearchParams({
token: token?.value!
})
})

if (!response.ok) {
const data = await response.json()

return NextResponse.json({
error: data.error,
error_description: data.error_description,
})
}

if (response.ok) {
if (idTokenExists) {
cookieStore.delete('id_token')
}

if (accessTokenExists) {
cookieStore.delete('access_token')
}

if (refreshTokenExists) {
cookieStore.delete('refresh_token')
}

return NextResponse.redirect(new URL('/login', request.nextUrl))
}
}

Let’s create a simple home page with a sign out button in it.

// /app/page.tsx

import Link from "next/link"

export default function Home() {
return (
<main className="min-h-screen bg-gray-100 py-12 px-4 sm:px-6 lg:px-8">
<div className="max-w-md mx-auto">
<div className="text-center">
<h2 className="text-3xl font-extrabold text-gray-900">Welcome to your homepage</h2>
<p className="mt-2 text-sm text-gray-600">This is your personal space. Enjoy your time here!</p>
</div>
<div className="mt-12">
<div className="rounded-lg shadow-md overflow-hidden">
<div className="flex p-6 text-lg font-medium text-gray-900 bg-white">
<h3>Your Account Info</h3>
</div>
<div className="p-6 bg-white">
<div className="flex items-center">
<div className="text-sm">
<p className="text-gray-900 font-medium">John Doe</p>
<p className="text-gray-500">johndoe@example.com</p>
</div>
</div>
</div>
</div>
<div className="mt-6">
<Link className="underline text-sm text-gray-600 hover:text-gray-500" href="#">
Edit Profile
</Link>
</div>
<form action='/api/auth/signout' method="GET" className="mt-6">
<button
type="submit"
className="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-red-600 hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500">
Sign out
</button>
</form>
</div>
</div>
</main>
)
}

When you click the sign out button, you should be redirected to the login page. If you check your cookies, your tokens should be gone already.

Connecting the Dots

So, what have we accomplished? We’ve manually implemented the OAuth2 Flow to enable Federated Google Sign-In. Here’s a little recap:

  1. We redirected the user to the /oauth2/authorize endpoint which leads to the Google Sign In Page.
  2. After a user successfully logs in with their account, Google calls the callback function adding an authorization code in the URL search parameters.
  3. We made a token request to the token endpoint by presenting the authorization code obtained from the callback.
  4. Your user pool OAuth 2.0 authorization server validates the token request and returns a successful response that includes an ID, access, and refresh token.

This guide only helps you get the tokens that you will need to access the protected resources in the resource server. In simpler terms, we achieved helping your user get into a signed-in state. The rest of the logic such as routing and authorization is for you to implement and fulfill application requirements.

Next Steps

Security Measures

For enhanced app security, employ PKCE during your authorization-code sign-in processes. PKCE ensures that the user presenting an authorization code is indeed the same user who underwent authentication.

Authorizer

To restrict access in your APIs, construct an authorizer and include it in your route handlers or middleware. An authorizer plays a vital role in managing authentication and authorization processes, ensuring controlled access to your application’s resources. This step enhances security by allowing you to define and enforce fine-grained access control based on your application’s specific requirements.

Explore

Learn more about OAuth 2.0 and OpenID Connect to further expand your knowledge on authorization and authentication. Explore these foundational concepts to deepen your understanding of secure identity management and robust access control mechanisms in the realm of modern web applications.

Feedback

To enhance the quality of this guide, receiving feedback is essential. I welcome your thoughts and critiques to further refine this guide, ensuring that the next reader receives the best possible assistance. You can message me in LinkedIn or reach me through my email: nbryleibanez@gmail.com.

--

--