In this guide, we'll explore a streamlined approach to authenticating users via Google OAuth 2.0 in Nitro by UnJS, powered by Deno Land. By the end, you'll have a solid foundation for implementing Google User Authentication on the Edge.
Essential Functions for Google OAuth 2.0
Parsing and Signing Keys
The following functions play a crucial role in parsing and signing keys.
// File: utils.ts
import { createHash, createHmac } from 'https://deno.land/std@0.177.0/node/crypto.ts'
// Generates a SHA-256 hash from the provided key using the Node.js crypto library.
export function parseKey(key) {
return createHash('sha256').update(key).digest()
}
// Creates a SHA-256 HMAC signature for the given data using the provided secret key.
// The resulting signature is then encoded in base64 URL format.
export function sign(data, secret) {
const key = parseKey(secret)
const hmac = createHmac('sha256', key)
hmac.update(data)
const signature = hmac.digest('base64')
return base64UrlEncode(signature)
}
// Transforms a base64-encoded string into a URL-safe format.
// It replaces characters to adhere to URL encoding standards,
// converting + to -, / to _, and removing trailing equal signs (=).
export function base64UrlEncode(str) {
const base64 = btoa(str)
return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')
}
Generating and Decoding JSON Web Tokens (JWTs)
The following functions play a crucial role in generating and decoding JSON Web Tokens (JWTs).
// File: utils.ts
// Reverses the URL-safe encoding process by converting a
// base64 URL-encoded string back to its original base64 format.
// It replaces URL-safe characters (- to +, _ to /) and
// handles padding to ensure the correct length before decoding using atob.
export function base64UrlDecode(str) {
const base64 = str.replace(/-/g, '+').replace(/_/g, '/')
const padding = base64.length % 4 === 0 ? 0 : 4 - (base64.length % 4)
const paddedBase64 = base64 + '==='.slice(0, padding)
return atob(paddedBase64)
}
// Takes a JWT (JSON Web Token) as input, splits it into encoded header
// and payload segments, and then decodes each segment using base64 URL decoding.
// The resulting decoded header and payload are parsed as JSON objects,
// and the function returns an object containing both.
export function decodeJWT(token) {
const [encodedHeader, encodedPayload] = token.split('.')
const header = JSON.parse(base64UrlDecode(encodedHeader))
const payload = JSON.parse(base64UrlDecode(encodedPayload))
return { header, payload }
}
// Transforms a base64-encoded string into a URL-safe format.
// It replaces characters to adhere to URL encoding standards,
// converting + to -, / to _, and removing trailing equal signs (=).
export function base64UrlEncode(str) {
const base64 = btoa(str)
return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')
}
// Constructs a JSON Web Token (JWT) by encoding the header and payload using base64 URL encoding.
// Then, signs the concatenated header and payload with the provided secret key using the sign function.
// The final JWT is formed by combining the encoded header, payload, and signature.
export function generateJWT(payload, secret, expiresIn) {
const header = { alg: 'HS256', typ: 'JWT' }
const encodedHeader = base64UrlEncode(JSON.stringify(header))
const encodedPayload = base64UrlEncode(payload)
const signature = sign(`${encodedHeader}.${encodedPayload}`, secret)
return `${encodedHeader}.${encodedPayload}.${signature}`
}
Handling Cookies for User Authentication
The following function plays a crucial role in parsing Cookies
and returning the one named cookieName
.
// File: utils.ts
// Extracts the value of a specific cookie from a provided cookie header.
// It splits the cookie header into individual cookies, iterates through them,
// and returns the value of the specified cookie if found.
// If the cookie is not present, it returns null.
export function parseCookie(cookieHeader, cookieName) {
if (!cookieHeader) return null
const cookies = cookieHeader.split(';')
for (const cookie of cookies) {
const [name, value] = cookie.split('=')
if (name.trim() === cookieName) {
return value.trim()
}
}
return null
}
Implementing Google OAuth Authentication in Nitro Routes
For all the routes below, we need to remember that custom_auth
is the cookie name which contains a signed key with the user data obtained from Google.
Home Route Logic (routes/index.ts
)
On the homepage we show the data stored in the cookie (custom_auth
) by decoding the JSON Web Token. In case no auth is found, we return an empty object.
This comes handy in testing as soon as user is authenticated with the rest of the flow.
// File: routes/index.ts
import { decodeJWT, parseCookie } from '../utils'
export default defineEventHandler((event) => {
// Get the cookie header in Nitro
const cookieHeader = event.headers.get('Cookie')
// Parse the custom_auth cookie to get the user auth values (if logged in)
const cookie = parseCookie(cookieHeader, 'custom_auth')
if (cookie) {
const decodedToken = decodeJWT(cookie)
// Just for demonstration purposes
if (decodedToken) return event.respondWith(new Response(JSON.stringify(decodedToken), { headers: { 'Content-Type': 'application/json' } }))
}
return event.respondWith(new Response(JSON.stringify({}), { headers: { 'Content-Type': 'application/json' } }))
})
Google OAuth Initiation Logic (routes/auth/google.ts
)
On the page, /auth/google
, we want to redirect users to Google login screen. To do that, we generate the authorization url which will in turn will call the CALLBACK_URL
once user authenticates.
// File: routes/auth/google.ts
export default defineEventHandler((event) => {
const authorizationUrl = new URL('https://accounts.google.com/o/oauth2/v2/auth')
// Get the Google Client ID from the env
authorizationUrl.searchParams.set('client_id', process.env.GOOGLE_CLIENT_ID)
// Add your own callback URL
authorizationUrl.searchParams.set('redirect_uri', process.env.GOOGLE_CLIENT_CALLBACK_URL)
authorizationUrl.searchParams.set('prompt', 'consent')
authorizationUrl.searchParams.set('response_type', 'code')
authorizationUrl.searchParams.set('scope', 'openid email profile')
authorizationUrl.searchParams.set('access_type', 'offline')
// Redirect the user to Google Login
return event.respondWith(
new Response(null, {
status: 302,
headers: {
Location: authorizationUrl.toString(),
},
}),
)
})
Google Callback After Google Authentication (routes/auth/callback/google.ts
)
On the page, /auth/callback/google
, we want to fetch the authenticated user information and secure it in the cookie custom_auth
. To do that, in this callback
handler, we obtain a token from google to fetch user info, generate a JWT, and finally set the cookie, custom_auth
.
Once all this is done, we redirect the user back to the home page.
// File: routes/auth/callback/google.ts
import { generateJWT } from '../../../utils'
export default defineEventHandler(async (event) => {
const code = new URL(event.node.req.url, 'https://a.b').searchParams.get('code')
if (!code) return event.respondWith(new Response())
try {
const tokenEndpoint = new URL('https://accounts.google.com/o/oauth2/token')
tokenEndpoint.searchParams.set('code', code)
tokenEndpoint.searchParams.set('grant_type', 'authorization_code')
// Get the Google Client ID from the env
tokenEndpoint.searchParams.set('client_id', process.env.GOOGLE_CLIENT_ID)
// Get the Google Secret from the env
tokenEndpoint.searchParams.set('client_secret', process.env.GOOGLE_CLIENT_SECRET)
// Add your own callback URL
tokenEndpoint.searchParams.set('redirect_uri', process.env.GOOGLE_CLIENT_CALLBACK_URL)
const tokenResponse = await fetch(tokenEndpoint.origin + tokenEndpoint.pathname, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: tokenEndpoint.searchParams.toString(),
})
const tokenData = await tokenResponse.json()
// Get the access_token from the Token fetch response
const accessToken = tokenData.access_token
const userInfoResponse = await fetch('https://www.googleapis.com/oauth2/v2/userinfo', {
headers: {
Authorization: `Bearer ${accessToken}`,
},
})
// Get user info via that fetched access_token
const userInfo = await userInfoResponse.json()
// Destructure email, name, picture from the users' Google Account Info
const { email, name, picture } = userInfo
const tokenPayload = JSON.stringify({ email, name, picture })
// Create a Cookie for the payload, i.e. user info as above
// Set the expiration to say 1 hour
const cookie = generateJWT(tokenPayload, 'My Secret Variable', '1h')
return event.respondWith(
new Response(null, {
status: 302,
headers: {
Location: '/',
// This is the key here, place the cookie in the browser
'Set-Cookie': `custom_auth=${cookie}; Path=/; HttpOnly`,
},
}),
)
} catch (error) {
console.error('Error fetching user info:', error)
}
})
Deployment and Live Demo
All done, let's deploy our app now!
Deploying to Deno Land
To deploy your Nitro project to Deno Land, follow these steps:
# Build the Nitro project for Deno
NITRO_PRESET=deno_deploy npm run build
# Change the CLI to the output by Nitro
cd .output
# Deploy to Deno Land
deployctl deploy --project=auth2 server/index.ts --prod
Live Demo
Explore the live example at auth2.deno.dev, and witness the Google OAuth 2.0 authentication in action.
Top comments (0)