@questlabs/auth-js
v1.0.1
Published
OAuth SDK for React — supports Google, Apple, and more
Keywords
Readme
@questlabs/auth-js
OAuth SDK for React — supports Google (and more providers in future). Includes a self-hostable Express broker that keeps client_secret server-side with full PKCE.
Architecture
Browser (React app) Broker (Express server)
───────────────────── ────────────────────────
signInWithOAuth()
↓ if standalone
full page redirect ── GET /~oauth/initiate ──→ generate PKCE
redirect to Google
Google OAuth ──────────→
GET /~oauth/callback ←── Google redirect
exchange code for tokens
←─────────── redirect back to app with #tokens ───────────
↓ if in iframe (builder)
open popup ──────── GET /~oauth/initiate ──→ generate PKCE
redirect to Google
GET /~oauth/callback ←── Google redirect
redirect to /~oauth/relay on app's own origin
←──────── window.opener.postMessage { authorization_response } ──
validate state → tokens returned directlyKey rules:
client_secretnever reaches the browser — PKCE lives in the broker.- State parameter prevents CSRF on both sides.
- Iframe/builder flow uses a relay page at the app's own origin so
window.openeris preserved.
Quick start
1. Install
npm install @questlabs/auth-js2. Use the hosted broker
A broker is already running at https://oauth.greta.sh. No setup needed — it's the default.
To self-host:
GOOGLE_CLIENT_ID=xxx GOOGLE_CLIENT_SECRET=yyy PORT=3001 node node_modules/@questlabs/auth-js/dist/broker/server.jsRegister https://oauth.greta.sh/~oauth/callback (or your broker's callback URL) as an authorised redirect URI in your Google Cloud project.
3. Sign in
import { createGretaAuth } from "@questlabs/auth-js"
const auth = createGretaAuth()
// Custom broker:
// const auth = createGretaAuth({ oauthBrokerUrl: "http://localhost:3001/~oauth/initiate" })
const result = await auth.signInWithOAuth("google", {
redirect_uri: window.location.origin,
})
if (result.tokens) {
console.log(result.tokens.access_token)
} else if (result.redirected) {
// standalone flow — page navigated away, handle on return with useGretaAuthCallback
} else {
console.error(result.error)
}Components & hooks
useGretaAuthCallback() — handle redirect return
Call this on the page the user lands on after Google redirects back (redirect/standalone flow).
import { useGretaAuthCallback } from "@questlabs/auth-js"
function App() {
useGretaAuthCallback({
onUser: (user, accessToken) => {
// user = { sub, email, name, picture, ... }
// persist session here
},
})
}<GoogleSignInButton>
import { GoogleSignInButton } from "@questlabs/auth-js"
<GoogleSignInButton
theme="light"
shape="rectangular"
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
Wrap your app in <GoogleAuthProvider> first.
import { GoogleAuthProvider, useGoogleAuth } from "@questlabs/auth-js"
function App() {
return (
<GoogleAuthProvider config={{ oauthBrokerUrl: "https://oauth.greta.sh/~oauth/initiate" }}>
<YourRoutes />
</GoogleAuthProvider>
)
}
function Profile() {
const { user, loading, 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>
)
}useGoogleSignIn() — lightweight hook
import { useGoogleSignIn } from "@questlabs/auth-js"
function LoginPage() {
const { signIn, status, error } = useGoogleSignIn({
onSuccess: (tokens) => saveTokens(tokens),
onError: (err) => alert(err.message),
})
return (
<button onClick={() => signIn()} disabled={status === "loading"}>
{status === "loading" ? "Signing in…" : "Sign in"}
</button>
)
}<RequireAuth>
import { RequireAuth, GoogleSignInButton } from "@questlabs/auth-js"
<RequireAuth
fallback={<p>Loading…</p>}
unauthenticated={<GoogleSignInButton onSuccess={...} />}
>
<Dashboard />
</RequireAuth>withGoogleAuth() — higher-order component
import { withGoogleAuth, type WithGoogleAuthProps } from "@questlabs/auth-js"
function Header({ title, googleAuth }: { title: string } & WithGoogleAuthProps) {
return (
<header>
<h1>{title}</h1>
{googleAuth.user && <button onClick={googleAuth.signOut}>Sign out</button>}
</header>
)
}
export default withGoogleAuth(Header)Core API
import { createGretaAuth } from "@questlabs/auth-js"
const auth = createGretaAuth({
oauthBrokerUrl: "https://oauth.greta.sh/~oauth/initiate",
supportedOAuthOrigins: ["https://oauth.greta.sh"],
})
const result = await auth.signInWithOAuth("google", {
redirect_uri: window.location.origin,
})Broker API
GET /~oauth/initiate
| Query param | Required | Description |
|-------------|----------|-------------|
| provider | Yes | e.g. "google" |
| state | Yes | CSRF nonce from the browser |
| redirect_uri | No | App origin for redirect flow |
| response_mode | No | "web_message" for iframe/popup flow |
| …extraParams | No | Forwarded to provider (e.g. login_hint) |
GET /~oauth/callback
Google redirects here. Exchanges code for tokens via PKCE, then either redirects back to the app (redirect flow) or to the relay page (iframe flow).
Security
- PKCE (S256) —
code_verifiergenerated per-request, stored server-side for ≤5 minutes. - State validation — client generates nonce, broker echoes it, client verifies before accepting tokens.
- Origin validation — client only accepts postMessages from
supportedOAuthOrigins. client_secretisolation — never sent to the browser.- Session cleanup — expired PKCE sessions purged every 60 seconds.
Building
npm run build # emit dist/
npm run dev # watch mode
npm run type-check # tsc --noEmit