August 26, 2024, written by
Andrii Minchekov
As of September 30, 2024, Google will turn off access to Less Secure Apps (LSA), including traditional user/password authentication methods for sending emails via Google SMTP servers. This shift necessitates moving towards OAuth2-based authentication, which provides a more secure and robust way to authenticate and authorize applications. This guide will walk you through the steps to authenticate to Google APIs using OAuth2, with a focus on sending an email via the Gmail API.
Google's decision to turn off access to Less Secure Apps] (LSA) means that any applications using basic authentication (username and password) will no longer be able to access Google services like Gmail. This change is designed to enhance security by preventing unauthorized access and reducing the risk of credential compromise.
OAuth2, on the other hand, is a token-based authentication protocol that provides secure delegated access. It allows applications to access user data without exposing user credentials, significantly enhancing security. OAuth2 is the recommended method for accessing Google APIs, including Gmail.
To implement OAuth2 authentication for the Gmail API to send emails, we will use a Server-to-Server approach using a Google Service Account. This method does not require user interaction, making it ideal for server-side applications.
Go to Google Admin Console:
Navigate to API Controls:
Add a New API Client:
https://www.googleapis.com/auth/gmail.send
For this example, we use Deno, a secure runtime for JavaScript and TypeScript, to write our server-side application.
import { create } from "https://deno.land/x/djwt@v2.8/mod.ts";
import { encodeBase64 } from "https://deno.land/std@0.203.0/encoding/base64.ts";
// Load Service Account credentials
const serviceAccountKey = JSON.parse(await Deno.readTextFile("./send-email-gmail-api-oauth-e4a749c0e47b.json"));
const { client_email, private_key } = serviceAccountKey;
// JWT header and claims
const header = { alg: "RS256", typ: "JWT" };
const iat = Math.floor(Date.now() / 1000);
const exp = iat + 3600; // Token valid for 1 hour
const scope = "https://www.googleapis.com/auth/gmail.send";
const userToImpersonate = "sender@example.com"; // The user to impersonate which will be the sender of email
// Import the private key
async function importPrivateKey(pem: string): Promise<CryptoKey> {
const pemHeader = "-----BEGIN PRIVATE KEY-----";
const pemFooter = "-----END PRIVATE KEY-----\n";
const pemContents = pem.substring(pemHeader.length, pem.length - pemFooter.length);
const binaryDerString = atob(pemContents);
const binaryDer = Uint8Array.from(binaryDerString, c => c.charCodeAt(0));
return await crypto.subtle.importKey(
"pkcs8",
binaryDer.buffer,
{
name: "RSASSA-PKCS1-v1_5",
hash: "SHA-256",
},
true,
["sign"],
);
}
// Generate JWT token for OAuth2
async function generateJwtToken() {
const key = await importPrivateKey(private_key);
const payload = {
iss: client_email,
scope,
aud: "https://oauth2.googleapis.com/token",
exp,
iat,
sub: userToImpersonate, // The user on behalf of whom you are acting
};
const jwt = await create(header, payload, key);
return jwt;
}
// Get OAuth2 Access Token using JWT
async function getAccessToken() {
const jwt = await generateJwtToken();
const response = await fetch("https://oauth2.googleapis.com/token", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
assertion: jwt,
}),
});
if (!response.ok) {
console.error("Failed to get access token:", await response.text());
throw new Error("Failed to get access token");
}
const data = await response.json();
return data.access_token;
}
// Function to send an email using Gmail API
async function sendEmail(accessToken: string, message: string) {
const encodedMessage = encodeBase64(new TextEncoder().encode(message))
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=+$/, "");
const response = await fetch("https://gmail.googleapis.com/gmail/v1/users/me/messages/send", {
method: "POST",
headers: {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
raw: encodedMessage,
}),
});
if (!response.ok) {
console.error("Failed to send email:", await response.text());
throw new Error(`Failed to send email: ${response.status}`);
}
console.log("Email sent successfully!");
}
function createMimeMessage(from: string, to: string, subject: string, htmlBody: string): string {
return [
`From: ${from}`,
`To: ${to}`,
`Subject: ${subject}`,
`MIME-Version: 1.0`,
`Content-Type: text/html; charset="UTF-8"`,
`Content-Transfer-Encoding: 7bit`,
``,
htmlBody
].join("\r\n");
}
// Simple server to trigger email sending
Deno.serve(async () => {
try {
const accessToken = await getAccessToken();
const from = "your-email@example.com"; // Replace with your email
const to = "recipient@example.com";
const subject = "Test Email from Deno";
const htmlBody = `<p>This is a test email sent using <strong>Gmail API</strong> with OAuth2 and Deno.</p>`;
const message = createMimeMessage(from, to, subject, htmlBody);
await sendEmail(accessToken, message);
return new Response("Email sent successfully!", { status: 200 });
} catch (error) {
console.error("Error:", error);
return new Response("Failed to send email", { status: 500 });
}
});
Run the script using Deno:
deno run --allow-net --allow-read send-email.ts
This script will:
Moving from basic Google authentication (user/password) to OAuth2 is essential for security and compliance, especially with Google's deprecation of Less Secure Apps (LSA) starting September 30, 2024. Google OAuth2 offers a robust, token-based authentication mechanism that ensures secure and delegated access to Google APIs, including Gmail API.
By following the steps outlined in this guide, you can authenticate your server-side application to Google APIs using OAuth2, enabling secure operations such as sending emails via Gmail API. This approach not only complies with Google's new policies but also enhances the overall security of your application.