@questlabs/react-google-auth
v1.1.0
Published
Google OAuth SDK for React
Keywords
Readme
@greta/react-google-auth
A Google OAuth SDK for React using a popup + postMessage architecture. Includes a self-hostable Express broker that keeps client_secret server-side and does full PKCE.
Architecture
Browser (React app) Broker (Express server)
───────────────────── ────────────────────────
signInWithOAuth()
↓ if in iframe
open popup ─────────── GET /~oauth/initiate ──→ generate PKCE
redirect to Google
Google OAuth ──────────→
GET /~oauth/callback ←── Google redirect
exchange code for tokens
←──────────── postMessage { authorization_response } ─────
validate state
fetch Google userinfo
store in sessionStorageKey rules:
client_secretnever reaches the browser — PKCE lives in the broker.- State parameter prevents CSRF on both sides.
- Same-origin broker works out of the box; cross-origin broker requires setting
supportedOAuthOrigins.
Quick start
1. Install
npm install @greta/react-google-auth express2. Start the broker
Managed mode (env vars):
GOOGLE_CLIENT_ID=xxx GOOGLE_CLIENT_SECRET=yyy PORT=3001 node dist/broker/server.jsBYOK mode — pass credentials at call time (useful for multi-tenant apps):
GET /~oauth/initiate?provider=google&state=…&client_id=xxx&client_secret=yyyRegister http://localhost:3001/~oauth/callback as an authorised redirect URI in your Google Cloud project.
3. Wrap your app
import { GoogleAuthProvider } from "@greta/react-google-auth"
const config = {
oauthBrokerUrl: "http://localhost:3001/~oauth/initiate",
// If broker is on a different origin, add it here:
supportedOAuthOrigins: ["http://localhost:3001"],
}
function App() {
return (
<GoogleAuthProvider config={config}>
<YourRoutes />
</GoogleAuthProvider>
)
}Same-origin broker — if you mount the broker on the same domain (e.g. via a reverse-proxy at
/~oauth), omit both options entirely and use the defaults.
Components & hooks
<GoogleSignInButton>
Pixel-accurate Google sign-in button following the Google brand guidelines.
import { GoogleSignInButton } from "@greta/react-google-auth"
<GoogleSignInButton
theme="light" // "light" | "dark"
shape="rectangular" // "rectangular" | "pill"
variant="standard" // "standard" | "icon"
text="Sign in with Google"
onSuccess={(tokens) => console.log(tokens)}
onError={(err) => console.error(err)}
/>| Prop | Type | Default | Description |
|------|------|---------|-------------|
| theme | "light" \| "dark" | "light" | Button colour scheme |
| shape | "rectangular" \| "pill" | "rectangular" | Border radius |
| variant | "standard" \| "icon" | "standard" | Show label or icon only |
| text | string | "Sign in with Google" | Button label |
| onSuccess | (tokens: OAuthTokens) => void | — | Called on success |
| onError | (error: Error) => void | — | Called on failure |
| config | GretaAuthConfig | — | Override broker URL / origins |
| options | SignInWithOAuthOptions | — | redirect_uri, extraParams |
| disabled | boolean | — | Disable the button |
useGoogleAuth() — session context
import { useGoogleAuth } from "@greta/react-google-auth"
function Profile() {
const { user, loading, error, signIn, signOut, initialized } = useGoogleAuth()
if (!initialized) return <p>Loading…</p>
if (!user) return <button onClick={() => signIn()}>Sign in</button>
return (
<div>
<img src={user.picture} alt={user.name} />
<p>{user.email}</p>
<button onClick={signOut}>Sign out</button>
</div>
)
}| Value | Type | Description |
|-------|------|-------------|
| user | GoogleUser \| null | Signed-in user from Google userinfo endpoint |
| tokens | OAuthTokens \| null | Raw access + refresh tokens |
| loading | boolean | true during sign-in / session restore |
| error | Error \| null | Last error |
| initialized | boolean | true once sessionStorage has been read |
| signIn | (opts?) => Promise<void> | Trigger OAuth flow |
| signOut | () => void | Clear session |
The GoogleUser shape mirrors the Google userinfo v3 endpoint:
interface GoogleUser {
sub: string
email: string
email_verified: boolean
name: string
picture: string
given_name: string
family_name: string
}useGoogleSignIn() — lightweight hook
Use this when you don't need full session management — just a sign-in button with loading/error state.
import { useGoogleSignIn } from "@greta/react-google-auth"
function LoginPage() {
const { signIn, status, error, reset } = useGoogleSignIn({
onSuccess: (tokens) => saveTokens(tokens),
onError: (err) => alert(err.message),
})
return (
<button onClick={() => signIn()} disabled={status === "loading"}>
{status === "loading" ? "Signing in…" : "Sign in"}
</button>
)
}Returns { signIn, status, loading, error, reset } where status is "idle" | "loading" | "success" | "error".
<RequireAuth>
Render children only when authenticated.
import { RequireAuth, GoogleSignInButton } from "@greta/react-google-auth"
<RequireAuth
fallback={<p>Loading session…</p>}
unauthenticated={<GoogleSignInButton onSuccess={...} />}
>
<Dashboard />
</RequireAuth>| Prop | Description |
|------|-------------|
| fallback | Shown while initialized is false |
| unauthenticated | Shown when initialized but no user |
| children | Shown when authenticated |
withGoogleAuth() — higher-order component
import { withGoogleAuth, type WithGoogleAuthProps } from "@greta/react-google-auth"
interface Props extends WithGoogleAuthProps {
title: string
}
function Header({ title, googleAuth }: Props) {
return (
<header>
<h1>{title}</h1>
{googleAuth.user && (
<button onClick={googleAuth.signOut}>Sign out</button>
)}
</header>
)
}
export default withGoogleAuth(Header)
// Usage:
<WrappedHeader title="My App" /> // googleAuth is injected automaticallyCore API (core.ts)
The pure TypeScript engine — no React dependency. Use directly if you need auth outside a component tree.
import { signInWithOAuth, generateState, isInIframe } from "@greta/react-google-auth"
const result = await signInWithOAuth("google", {
oauthBrokerUrl: "/~oauth/initiate",
supportedOAuthOrigins: ["https://your-broker.example.com"],
})
if (result.redirected) {
// navigating away — nothing to do
} else if (result.error) {
console.error(result.error)
} else {
console.log(result.tokens.access_token)
}Constants
| Name | Value |
|------|-------|
| EXPECTED_MESSAGE_TYPE | "authorization_response" |
| DEFAULT_OAUTH_BROKER_URL | "/~oauth/initiate" |
| DEFAULT_SUPPORTED_OAUTH_ORIGINS | [] (falls back to window.location.origin) |
| DEFAULT_MOBILE_DEEP_LINK_REDIRECT_URI | "greta://oauth-callback" |
| DEFAULT_DESKTOP_LOCALHOST_REDIRECT_URI | "http://127.0.0.1/iframe-oauth/callback" |
| POPUP_CHECK_INTERVAL_MS | 500 |
| IFRAME_FALLBACK_TIMEOUT_MS | 120000 |
Broker API
GET /~oauth/initiate
| Query param | Required | Description |
|-------------|----------|-------------|
| provider | Yes | Must be "google" |
| state | Yes | CSRF nonce from the browser client |
| redirect_uri | No | Override for mobile/desktop apps |
| client_id | BYOK | Google OAuth client ID |
| client_secret | BYOK | Google OAuth client secret |
| mode | No | "web_message" (default) or "redirect" |
| …extraParams | No | Forwarded verbatim to Google (e.g. login_hint) |
GET /~oauth/callback
Google redirects here. Handles PKCE code exchange and delivers tokens via postMessage or URL hash depending on mode.
Security notes
- PKCE (S256) prevents authorization code interception. The
code_verifieris generated per-request and stored server-side for ≤5 minutes. - State validation — the browser client generates a random state nonce; the broker returns it unchanged; the client checks it matches before accepting tokens.
- Origin validation — the client only accepts postMessages from
supportedOAuthOrigins. client_secretisolation — the browser never sees the secret; it lives only in the broker process.- Session cleanup — expired PKCE sessions are purged every 60 seconds.
Building
npm run build # emit dist/
npm run dev # watch mode
npm run type-check # tsc --noEmitOutput layout:
dist/
index.js # CJS — React SDK
index.mjs # ESM — React SDK
index.d.ts # Types
broker/
server.js # CJS — broker (Node 18+)
server.d.ts