Amazon Cognito backendless PCKE

Short sample of how to get up and running with Cognito and without backend

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "5.94.1"
    }
  }
}

provider "aws" {
  region = "eu-central-1"
  # https://us-east-1.console.aws.amazon.com/iam/home?region=eu-central-1#/security_credentials
  # after experiments are done, do not forget to delete this credentials, just in case
  access_key = "********************"
  secret_key = "********************"
}

# this one will create "User pool"
# https://eu-central-1.console.aws.amazon.com/cognito/v2/idp/user-pools?region=eu-central-1
resource "aws_cognito_user_pool" "example" {
  name = "example"
}

# this one will add Google provider to user pool
# https://console.cloud.google.com/auth/clients/544615198383-ltp2c456gv5nt3v9l2aro77jq72c8fra.apps.googleusercontent.com?project=demo2-de02e
# it should have redirect uri: https://example42.auth.eu-central-1.amazoncognito.com/oauth2/idpresponse
resource "aws_cognito_identity_provider" "example" {
  user_pool_id  = aws_cognito_user_pool.example.id
  provider_name = "Google"
  provider_type = "Google"

  provider_details = {
    authorize_scopes = "email"
    client_id        = "xxxxxxxx.apps.googleusercontent.com"
    client_secret    = "***********************************"
  }

  attribute_mapping = {
    email    = "email"
    username = "sub"
  }
}

# this one creates app client
# https://eu-central-1.console.aws.amazon.com/cognito/v2/idp/user-pools/eu-central-1_oHTeOK341/applications/app-clients?region=eu-central-1
resource "aws_cognito_user_pool_client" "example" {
  name                                 = "example"
  user_pool_id                         = aws_cognito_user_pool.example.id
  allowed_oauth_flows                  = ["code", "implicit"]
  allowed_oauth_scopes                 = ["email", "openid"]
  callback_urls                        = ["http://localhost:3000"]
  supported_identity_providers         = ["COGNITO", "Google"]
  allowed_oauth_flows_user_pool_client = true
}

# this one creates identity pool wired up to user pool created above
# https://eu-central-1.console.aws.amazon.com/cognito/v2/identity/identity-pools?region=eu-central-1
resource "aws_cognito_identity_pool" "example" {
  identity_pool_name = "example"
  cognito_identity_providers {
    client_id               = aws_cognito_user_pool_client.example.id
    provider_name           = aws_cognito_user_pool.example.endpoint
    server_side_token_check = false
  }
}

# define the domain prefix to be used
resource "aws_cognito_user_pool_domain" "example" {
  domain       = "example42"
  user_pool_id = aws_cognito_user_pool.example.id
}

# client_id = "7q601uo92n5tfj957hg3nteien"
output "client_id" {
  value = aws_cognito_user_pool_client.example.id
}

# pool_id = "eu-central-1:8d4d87b2-f523-4738-9bef-d48eb9fa0b40"
output "pool_id" {
  value = aws_cognito_identity_pool.example.id
}

# domain = "example42"
output "domain" {
  value = aws_cognito_user_pool_domain.example.domain
}

Having this in place we should have Cognito configured with Google provider and client that allows us to authenticate from localhost

Here is sample html file to play with (i just run npx lite-server)

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>cognito</title>
  </head>
  <body>
    <h1>cognito</h1>
    <button onclick="login()">Login</button>
    <script>
      var client_id = '7q601uo92n5tfj957hg3nteien'
      var redirect_uri = 'http://localhost:3000'

      async function login() {
        var code_verifier = crypto.randomUUID()
        var code_challenge = base64urlencode(await crypto.subtle.digest('SHA-256', new TextEncoder().encode(code_verifier)))

        var url = new URL('https://example42.auth.eu-central-1.amazoncognito.com/oauth2/authorize')
        url.searchParams.set('response_type', 'code')
        url.searchParams.set('client_id', client_id)
        url.searchParams.set('redirect_uri', redirect_uri)
        url.searchParams.set('identity_provider', 'Google')
        url.searchParams.set('code_challenge', code_challenge)
        url.searchParams.set('code_challenge_method', 'S256')

        localStorage.setItem('code_verifier', code_verifier)
        window.location.href = url
      }

      function base64urlencode(str) {
        return btoa(String.fromCharCode.apply(null, new Uint8Array(str)))
          .replace(/\+/g, '-')
          .replace(/\//g, '_')
          .replace(/=+$/, '')
      }

      window.addEventListener('load', async () => {
        var code = new URLSearchParams(window.location.search).get('code')
        var code_verifier = localStorage.getItem('code_verifier')
        if (code && code_verifier) {
          var params = new URLSearchParams()
          params.set('grant_type', 'authorization_code')
          params.set('client_id', client_id)
          params.set('redirect_uri', redirect_uri)
          params.set('code', code)
          params.set('code_verifier', code_verifier)

          var data = await fetch(`https://example42.auth.eu-central-1.amazoncognito.com/oauth2/token`, {
            method: 'POST',
            headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
            body: params,
          }).then(r => r.json())

          document.body.innerHTML += `<h3>Tokens:</h3><pre>${JSON.stringify(data, null, 2)}</pre>`

          localStorage.removeItem('code_verifier')
          window.history.replaceState({}, document.title, redirect_uri) // clean URL
        }
      })
    </script>
  </body>
</html>

note: even so we are working without backend, we still using code flow with PKCE