Migrating Firebase Authentication to Amazon Cognito with Amplify

sleroy · Jan 15, 2026 · 7 min read

Authentication migration is one of the highest-risk tasks in any platform transition. Users cannot be told “please re-register.” Passwords cannot be extracted from Firebase. And federated identity providers add another layer of complexity that most migration guides gloss over.

I recently led a migration from Firebase Authentication to Amazon Cognito User Pools for a mid-sized SaaS platform. The application used Google Sign-In as its primary authentication method, with a smaller cohort of email/password users. Here is what I learned — including the bugs, limitations, and workarounds that cost us days.

Watch the 30-second summary

Auto-generated summary — read the full article below for details.


Why Migrate Away from Firebase Auth?

The motivations vary, but in our case:

  1. Platform consolidation. The backend was already running on AWS. Having authentication on Google Cloud meant cross-cloud token validation, separate IAM policies, and duplicated monitoring.
  2. Fine-grained authorization needs. Cognito integrates directly with IAM roles, API Gateway authorizers, and AppSync — eliminating a custom middleware layer.
  3. Cost predictability. Firebase Auth pricing changes made budgeting difficult at scale.
  4. Compliance requirements. The customer needed all PII in a single cloud jurisdiction with a unified audit trail.

Architecture Overview

The target architecture involves three AWS components:

  • Cognito User Pool: stores user accounts, handles sign-up/sign-in flows, and manages password policies.
  • Cognito Identity Pool: federates identities from external providers (Google, Facebook) and exchanges tokens for temporary AWS credentials.
  • Amplify JS: client-side library that orchestrates the authentication flows in the frontend application.

The existing Firebase app used firebase.auth().signInWithPopup(googleProvider) for Google Sign-In and firebase.auth().signInWithEmailAndPassword() for email users.


Step 1: Setting Up the Cognito User Pool

Creating the User Pool is straightforward in the AWS Console or via Terraform/CDK. However, there is a critical decision at creation time: whether to generate a client secret.

If you create an app client WITH a client secret, every API call (sign-up, sign-in, forgot-password) must include a SECRET_HASH parameter — an HMAC-SHA256 of the username concatenated with the client ID, keyed with the client secret.

The SECRET_HASH Bug

In my experience, this is where most teams lose their first day. Amplify JS does NOT compute the SECRET_HASH automatically. If your User Pool app client has a secret enabled, Amplify calls will fail silently or return cryptic errors like:

NotAuthorizedException: Unable to verify secret hash for client <client_id>

The fix: Create your app client WITHOUT a client secret if you are using Amplify JS in a browser-based application. Browser apps cannot securely store secrets anyway. If you must use a secret (for server-side flows), compute the hash manually:

import hmac
import hashlib
import base64

def compute_secret_hash(client_id, client_secret, username):
    message = username + client_id
    dig = hmac.new(
        client_secret.encode('utf-8'),
        message.encode('utf-8'),
        hashlib.sha256
    ).digest()
    return base64.b64encode(dig).decode()

For the Amplify JS client, simply disable the secret on the app client and move on.


Step 2: Creating the Identity Pool

The Identity Pool serves a different purpose than the User Pool. It exchanges authenticated tokens (from Cognito User Pool, Google, Facebook, etc.) for temporary AWS credentials (STS AssumeRole).

Configuration steps:

  1. Create an Identity Pool in the Cognito console.
  2. Under “Authentication providers,” add your Cognito User Pool (User Pool ID + App Client ID).
  3. Add Google as an external provider using your Google OAuth Client ID.
  4. Define authenticated and unauthenticated IAM roles.

This dual-pool architecture lets you handle both email/password users (via User Pool) and federated Google users (via Identity Pool direct federation) with a single credential flow downstream.


Step 3: Integrating Amplify JS

The Amplify configuration in your existing app looks like this:

import { Amplify } from 'aws-amplify';

Amplify.configure({
  Auth: {
    Cognito: {
      userPoolId: 'eu-west-1_XXXXXXX',
      userPoolClientId: 'your-app-client-id',
      identityPoolId: 'eu-west-1:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx',
      loginWith: {
        oauth: {
          domain: 'your-domain.auth.eu-west-1.amazoncognito.com',
          scopes: ['openid', 'email', 'profile'],
          redirectSignIn: ['http://localhost:3000/'],
          redirectSignOut: ['http://localhost:3000/'],
          responseType: 'code'
        }
      }
    }
  }
});

For Google federated sign-in, you invoke:

import { signInWithRedirect } from 'aws-amplify/auth';

await signInWithRedirect({ provider: 'Google' });

The amplifyCredentials Pattern

After sign-in, retrieving AWS credentials for API calls follows this pattern:

import { fetchAuthSession } from 'aws-amplify/auth';

async function getCredentials() {
  const session = await fetchAuthSession();
  return {
    accessKeyId: session.credentials.accessKeyId,
    secretAccessKey: session.credentials.secretAccessKey,
    sessionToken: session.credentials.sessionToken
  };
}

These credentials are scoped to the IAM role attached to your Identity Pool’s authenticated role.


Step 4: The Federated User Migration Problem

Here is the most important caveat of this entire migration, and the one least documented anywhere:

Federated users from Firebase CANNOT be migrated via the Cognito Migrate User Lambda trigger.

The Migrate User Lambda trigger fires only for email/password sign-in attempts against the User Pool. When a user authenticates via Google federated sign-in, the token exchange happens at the Identity Pool level — it never hits the User Pool’s authentication flow, and therefore never invokes any Lambda trigger.

This means:

  • Email/password users CAN be migrated seamlessly using the Migrate User trigger (Cognito calls your Lambda on first sign-in, you validate against Firebase, and Cognito creates the user).
  • Google federated users will appear as NEW users in Cognito. Their Firebase UID will not carry over. You need a separate reconciliation process.

Reconciliation Strategy for Federated Users

What worked for us:

  1. On first Google sign-in to Cognito, capture the user’s email from the Google token.
  2. Look up the email in your application database (which still has the Firebase UID mapping).
  3. Link the new Cognito identity to the existing application user record.
  4. Mark the migration as complete for that user.

This is application-level logic — Cognito will not do it for you.


Step 5: Handling Polyfill Issues

If your application uses Vue.js or Angular, you will likely encounter polyfill errors when adding Amplify JS. The most common:

Module not found: Error: Can't resolve 'crypto'
Module not found: Error: Can't resolve 'stream'

Amplify v6 has reduced these issues significantly compared to v5, but some sub-dependencies still reference Node.js built-ins. Solutions:

For Vue (Vite):

// vite.config.js
import { defineConfig } from 'vite';

export default defineConfig({
  resolve: {
    alias: {
      './runtimeConfig': './runtimeConfig.browser'
    }
  }
});

For Angular:

Add to polyfills.ts:

(window as any).global = window;
(window as any).process = { env: { DEBUG: undefined } };

And install stream-browserify and crypto-browserify as dev dependencies if needed.


Step 6: The Migrate User Lambda (Email/Password Users)

For email/password users, the Migrate User Lambda trigger is elegant. Here is the flow:

  1. User attempts to sign in to Cognito with email and password.
  2. Cognito does not find the user in its pool.
  3. Cognito invokes your Migrate User Lambda with the username and password.
  4. Your Lambda validates against Firebase Admin SDK.
  5. If valid, return the user attributes — Cognito creates the user transparently.
const admin = require('firebase-admin');

exports.handler = async (event) => {
  if (event.triggerSource === 'UserMigration_Authentication') {
    const { userName, request } = event;
    try {
      const userRecord = await admin.auth().getUserByEmail(userName);
      // Verify password against Firebase
      // (requires Firebase Auth REST API - verifyPassword endpoint)
      
      event.response.userAttributes = {
        email: userRecord.email,
        email_verified: String(userRecord.emailVerified),
      };
      event.response.finalUserStatus = 'CONFIRMED';
      event.response.messageAction = 'SUPPRESS';
    } catch (err) {
      throw new Error('Bad credentials');
    }
  }
  return event;
};

Important: Firebase Admin SDK does not expose a “verify password” method. You must call the Firebase Auth REST API (identitytoolkit.googleapis.com/v1/accounts:signInWithPassword) to validate credentials during migration.


Common Pitfalls Summary

PitfallImpactResolution
Client secret enabled on app clientAmplify JS calls failDisable secret for browser clients
Assuming federated users trigger Migrate LambdaUsers not migratedBuild application-level reconciliation
Missing polyfills in Vue/AngularBuild failuresAdd browser polyfills per framework
Not setting finalUserStatus: CONFIRMEDUsers stuck in unconfirmed stateAlways set in Lambda response
Using Firebase Admin SDK to verify passwordsSDK does not support itUse REST API instead
Forgetting to configure OAuth domainFederated sign-in 404sSet up Cognito Hosted UI domain

Migration Timeline

In my experience, a typical Firebase-to-Cognito authentication migration for a medium application (10K-50K users) takes:

  • Week 1-2: Cognito infrastructure setup, Amplify integration, development environment testing.
  • Week 3-4: Migrate User Lambda development and testing, federated sign-in configuration.
  • Week 5-6: Reconciliation logic, parallel-run period (both Firebase and Cognito active).
  • Week 7-8: Gradual cutover, monitoring, edge case resolution.

The parallel-run period is non-negotiable. Run both systems simultaneously. Route a percentage of traffic to Cognito. Monitor error rates. Only cut over fully when the error rate matches or beats the Firebase baseline.


Conclusion

Authentication migration is never as simple as the documentation suggests. The two key takeaways from this project:

  1. Federated identity is the hard part. Email/password migration is solved elegantly by the Migrate User trigger. Federated users require custom reconciliation logic at the application layer.
  2. Test the full flow early. Do not spend weeks on infrastructure without testing end-to-end sign-in. The SECRET_HASH issue, polyfill problems, and OAuth domain configuration will all surface only when you actually try to authenticate.

If you are planning this migration, start with a proof-of-concept that covers BOTH email/password AND federated sign-in. If that works end-to-end in your framework, the rest is execution.

comments powered by Disqus