google-oauth.js 2.2 KB
import { google } from 'googleapis'

/**
 * @param {{ clientId: string, clientSecret: string|null }} config
 * @returns {import('@drama/auth').OauthPort}
 */
export function createGoogleOauthPort({ clientId, clientSecret }) {
  if (!clientId) throw new Error('createGoogleOauthPort: clientId is required')
  const client = new google.auth.OAuth2(clientId, clientSecret || undefined)
  return {
    async exchange({ provider, code, oauth_flow, redirect_uri }) {
      if (provider !== 'google') throw new Error(`Unsupported OAuth provider: ${provider}`)
      if (oauth_flow === 'authorization_code') {
        if (!clientSecret) throw new Error('Google OAuth authorization code flow requires GOOGLE_OAUTH_CLIENT_SECRET')
        if (!redirect_uri) throw new Error('Google OAuth authorization code flow requires redirect_uri')
        const { tokens } = await client.getToken({ code, redirect_uri })
        if (!tokens.id_token) throw new Error('Google OAuth token response is missing id_token')
        const ticket = await client.verifyIdToken({ idToken: tokens.id_token, audience: clientId })
        const payload = ticket.getPayload()
        if (!payload?.sub || !payload.email) throw new Error('Google ID token is missing account identity')
        return {
          provider_user_id: payload.sub,
          email: payload.email,
          email_verified: payload.email_verified === true,
        }
      }
      if (oauth_flow !== 'access_token') throw new Error(`Unsupported Google OAuth flow: ${oauth_flow}`)
      const tokenInfo = await client.getTokenInfo(code)
      if (tokenInfo.aud !== clientId) throw new Error('Google access token audience does not match client ID')
      const response = await fetch('https://openidconnect.googleapis.com/v1/userinfo', {
        headers: { authorization: `Bearer ${code}` },
      })
      if (!response.ok) throw new Error(`Google userinfo request failed: HTTP ${response.status}`)
      const payload = await response.json()
      if (!payload.sub || !payload.email) throw new Error('Google userinfo is missing account identity')
      return {
        provider_user_id: payload.sub,
        email: payload.email,
        email_verified: payload.email_verified === true,
      }
    },
  }
}