Authentication systems often feel like black boxes—especially when constraints block standard paths. When a NestJS stack inherited an AWS Cognito user pool but lacked console access and couldn’t use Cognito’s hosted UI, the team faced a critical challenge: enabling Google Sign-In without compromising on design or flexibility. The solution didn’t require hacking Cognito itself—just rethinking how identity flows could be orchestrated from the backend.
The result wasn’t a flaw in Cognito, but a workaround born from necessity. By validating Google tokens server-side and automating user creation via Cognito’s admin APIs, the team built a silent bridge between Google’s identity layer and Cognito’s user store. Here’s how it worked—and why it might be the right approach when standard federation isn’t possible.
The Hidden Cost of Console Lockdowns
Access control in cloud platforms is essential, but when it strips away operational agility, even routine tasks become bottlenecks. In this case, the absence of AWS console access meant every configuration change—from user pool settings to identity provider mappings—required coordination with a manager. That constraint turned debugging into an exercise in precision: every API call needed to be intentional, every assumption tested before execution.
The stack was straightforward on paper: NestJS backend, AWS Cognito as the identity provider, and a frontend demanding a custom “Sign in with Google” flow. But the missing piece wasn’t technical—it was operational. Without the ability to tweak Cognito’s hosted UI or configure federated identity providers directly, the team had to find a way to make Google Sign-In work around Cognito’s defaults, not through them.
Why Cognito Resists Custom UI Flows
Cognito is designed to guide users through a predictable authentication journey. Its hosted UI handles OAuth redirects, session management, and token exchange seamlessly—but those features come with strict expectations. When organizations insist on embedding “Sign in with Google” directly into their own UI—without redirects or branded pages—Cognito’s default federation flow becomes a roadblock.
The standard approach requires configuring Google as a federated identity provider via the AWS console and relying on Cognito’s hosted UI to complete the OAuth handshake. But when console access is restricted and UI customization isn’t an option, that path is blocked. The team needed an alternative: a way to trust Google’s identity verification while still storing users in Cognito, all without exposing users to Cognito’s interface.
A Backend-Centric Identity Bridge
The breakthrough came from recognizing two truths: Google’s ID tokens are already cryptographically verified by Google, and Cognito provides server-side admin APIs for programmatic user management. If the backend could trust the Google token and create Cognito users automatically, the missing pieces could be stitched together without user-facing redirects.
The flow shifted from a multi-step user journey to a direct server-to-server conversation:
- User clicks “Sign in with Google” in the frontend.
- Frontend sends the Google ID token to the backend.
- Backend validates the token using Google’s public keys.
- Backend extracts user details (email, name, Google ID) from the token.
- Backend creates or updates the user in Cognito using admin APIs.
- Backend generates a deterministic password tied to the user’s Cognito ID.
- Backend returns a session token to the frontend.
From the user’s perspective, they signed in with Google. From Cognito’s perspective, a standard username/password user was created. The handoff was invisible to both sides.
The Implementation: Three Functions, One Endpoint
The solution lives within a single NestJS controller method that branches based on whether a Google token is present. This keeps the code clean and avoids mixing authentication strategies.
@Post('signup')
@UseInterceptors(FileInterceptor('client_logo'))
async signup(
@UploadedFile() clientLogoFile: Express.Multer.File,
@Body('googleToken') googleToken: string,
@Body('request_create_client_dto') requestCreateClientDto: string,
@Body('request_signup_user_dto') requestSignupUserDto?: string,
) {
const createClientDto = JSON.parse(requestCreateClientDto);
if (googleToken) {
return await this.authService.googleSignupCompany(
googleToken,
createClientDto,
clientLogoFile,
);
}
if (!requestSignupUserDto) {
throw new BadRequestException('Signup user data is required');
}
const signupUserDto = JSON.parse(requestSignupUserDto);
return await this.authService.signupWithClientAndUser(
createClientDto,
signupUserDto,
clientLogoFile,
);
}The logic is split cleanly:
- If a
googleTokenis provided, the Google signup path is triggered. - Otherwise, the system falls back to a traditional signup flow.
- A file upload for client logos is supported in both paths.
Step 1: Server-Side Google Token Validation
Before any user data touches Cognito, the backend must verify the Google ID token. This is done using the google-auth-library in Node.js, which handles signature verification and payload extraction.
async verifyGoogleToken(googleToken: string) {
try {
const ticket = await this.googleClient.verifyIdToken({
idToken: googleToken,
audience: process.env.GOOGLE_CLIENT_ID,
});
return ticket.getPayload();
} catch (error) {
throw new BadRequestException('Invalid Google token');
}
}Key points:
- The
audiencemust match the client ID registered in Google Cloud Console. - The token is validated using Google’s public keys, ensuring authenticity.
- The payload includes verified claims like
email,email_verified,sub(Google’s user ID), andname.
Step 2: Deterministic Password Generation
Cognito requires a password for every user, even if they’ll never use it. To avoid storing arbitrary or insecure passwords, the team implemented a deterministic generator:
async generatePassword(userId: string, secret: string): Promise<string> {
const salt = 'cognito-generated-password-salt';
const hash = crypto
.createHmac('sha256', secret)
.update(userId + salt)
.digest('hex');
return hash.substring(0, 20);
}This method:
- Uses a fixed salt and server-side secret for consistency.
- Produces the same password every time for the same
userId. - Generates a 20-character alphanumeric string, suitable for Cognito.
- Ensures login consistency without exposing the password to users or frontend.
Step 3: Cognito Admin User Creation
With the user details and password ready, the backend uses Cognito’s AdminCreateUser API to create the user directly in the user pool. This bypasses email verification flows and hosted UI entirely.
async createCognitoUser(email: string, password: string) {
const params = {
UserPoolId: process.env.AWS_COGNITO_USER_POOL_ID,
Username: email,
UserAttributes: [
{ Name: 'email', Value: email },
{ Name: 'email_verified', Value: 'true' },
],
TemporaryPassword: password,
MessageAction: 'SUPPRESS',
};
return await this.cognito.adminCreateUser(params).promise();
}Notable details:
email_verifiedis set totruebecause Google’s token already confirms email ownership.MessageAction: 'SUPPRESS'prevents Cognito from sending welcome emails.- The user is created programmatically, with no user interaction required.
When This Approach Is—and Isn’t—Appropriate
This workaround excels in environments where:
- Console access is restricted or slow.
- UI customization is mandatory.
- Federation must happen without hosted pages or redirects.
- Email verification can be outsourced to a trusted provider like Google.
However, it’s not suitable when:
- Security policies require email verification via Cognito’s native flow.
- Multi-factor authentication (MFA) is mandatory from the start.
- The user pool must support legacy login methods (e.g., SMS or OTP).
- Audit trails or compliance require Cognito to handle all identity events.
The Future of Identity: Flexibility Over Convention
Identity systems are evolving toward modularity, and this hack reflects a broader trend: software should adapt to constraints, not the other way around. While Cognito’s hosted UI offers convenience, real-world deployments often demand deviation. The key isn’t to break the system—it’s to extend it thoughtfully.
Developers now have a blueprint for integrating external identity providers without sacrificing control. And as cloud platforms continue to evolve, the line between “supported” and “unsupported” flows will blur further. The real innovation isn’t in overcoming limitations—it’s in redefining what’s possible within them.
For teams facing similar roadblocks, the takeaway is clear: trust the tokens, automate the users, and let the backend do the heavy lifting.
AI summary
Learn how to integrate Google Sign-In with AWS Cognito without using the hosted UI or console access. Includes server-side token validation, deterministic passwords, and NestJS code.
Tags