The Rarely Known Fact-Using Google Service Account for M2M JWT Authentication and Authorization

| March 20, 2025

Learn how to leverage Google service accounts for secure machine-to-machine (M2M) authentication and authorization using JWTs, with a practical example in NestJS.

TL;DR

Google service accounts can securely handle machine-to-machine (M2M) authentication and authorization. This post demonstrates how to generate and verify JSON Web Tokens (JWTs) with custom claims (e.g., roles) using NestJS, taking full advantage of Google’s key management infrastructure.

Here are the related nestjs repos you can test out: service-b service-c

Table of Contents

Overview of M2M Authentication with Google Service Accounts

In microservices architectures, secure communication between services is crucial—especially when no end user is involved. Google service accounts provide a robust method for machine-to-machine (M2M) authentication by generating JWTs that include both standard and custom claims. These tokens are signed with a private key and later verified using Google’s public certificates, ensuring secure and reliable validation.

Setting Up the Google Service Account

Before diving into code, set up your Google service account:

  1. Create a Service Account:

    • Go to the Google Cloud Console.
    • Navigate to IAM & Admin > Service Accounts.
    • Create a new service account and download its JSON key file (e.g., service-account-key.json).
  2. Understand the Key File:

    • private_key: Used for signing JWTs.
    • client_email: Serves as the issuer (iss) in JWT payloads.
    • private_key_id: Included in the JWT header (kid) to help identify the correct public key during verification.

Generating a JWT in Service B

In Service B, you generate a JWT that includes a custom role claim. Here’s a NestJS example demonstrating how to create a JWT using the service account’s credentials:

import * as jwt from 'jsonwebtoken';
import { ConfigService } from '@nestjs/config';
import { Injectable } from '@nestjs/common';
@Injectable()
export class AuthService {
constructor(private configService: ConfigService) {}
generateToken(role: string): string {
const privateKey = this.configService.get<string>('private_key');
const clientEmail = this.configService.get<string>('client_email');
const privateKeyId = this.configService.get<string>('private_key_id');
const payload = {
iss: clientEmail,
aud: 'http://localhost:3001', // Audience: Service C’s endpoint
iat: Math.floor(Date.now() / 1000),
exp: Math.floor(Date.now() / 1000) + 3600, // Token expires in 1 hour
role, // Custom claim for authorization
};
return jwt.sign(payload, privateKey, {
algorithm: 'RS256',
keyid: privateKeyId,
});
}
}

Key Points:

  • Payload: Combines standard claims (iss, aud, iat, exp) with a custom role claim.
  • Signing Process: Uses RS256 with the provided private key and includes the kid header for easy verification.

Verifying the JWT in Service C

Service C is responsible for verifying the JWT. It fetches Google’s public certificates to validate the token and checks the custom claim to ensure proper authorization:

import * as jwt from 'jsonwebtoken';
import * as https from 'https';
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
@Injectable()
export class AuthGuard implements CanActivate {
private serviceAccountEmail = '[email protected]';
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
const token = request.headers.authorization?.split(' ')[1];
if (!token) return false;
const certs = await this.getCerts();
const decodedHeader = jwt.decode(token, { complete: true })?.header;
const publicKey = certs[decodedHeader?.kid];
const decoded = jwt.verify(token, publicKey, {
algorithms: ['RS256'],
audience: 'http://localhost:3001',
issuer: this.serviceAccountEmail,
}) as { role?: string };
return decoded.role === 'admin'; // Grant access only if the role is 'admin'
}
private async getCerts(): Promise<{ [key: string]: string }> {
return new Promise((resolve, reject) => {
https.get(
`https://www.googleapis.com/service_accounts/v1/metadata/x509/${this.serviceAccountEmail}`,
(res) => {
let data = '';
res.on('data', (chunk) => (data += chunk));
res.on('end', () => resolve(JSON.parse(data)));
}
).on('error', reject);
});
}
}

Highlights:

  • Certificate Retrieval: The public keys are fetched from Google’s endpoint.
  • Token Verification: Validates the token’s signature, audience, and issuer.
  • Authorization Logic: Access is allowed only if the decoded token’s role is admin.

Benefits and Conclusion

Performance and Security Benefits

  • Efficiency:

    • Google automates key rotation and certificate distribution, reducing manual overhead.
    • JWT verification is fast and efficient due to pre-fetched certificates.
  • Security:

    • The private key remains secure within Service B.
    • Verification with public certificates ensures the token’s integrity.
  • Scalability:

    • Centralized IAM management simplifies secure communication across multiple services.

Conclusion

Leveraging Google service accounts for M2M JWT authentication and authorization offers a secure, scalable, and efficient solution for microservices communication. By generating JWTs with custom claims in one service and verifying them in another, you enhance your system’s security without added complexity. The provided NestJS examples offer a practical starting point for integrating this approach into your projects.

Give it a try and streamline your authentication workflows with confidence!