Skip to content

Search is only available in production builds. Try building and previewing the site to test it out locally.

OAuth Setup

Reactor Identity supports OAuth 2.0 / OpenID Connect for social login. This guide walks through provider registration, server configuration, and client-side integration.

Prerequisites: Running Reactor server with [auth] configured, a frontend app, OAuth app credentials from your provider.


  1. Go to GitHub Developer SettingsOAuth AppsNew OAuth App
  2. Set Authorization callback URL:
    https://api.myapp.com/auth/v1/callback/github
  3. Copy Client ID and generate a Client Secret

OAuth providers are stored in the auth schema. Register via the admin API or a migration:

-- migrations/002_oauth_providers.sql
INSERT INTO _reactor_auth.oauth_providers (id, name, type, config, enabled)
VALUES
(
'github',
'GitHub',
'oauth2',
'{
"client_id": "Ov23li...",
"client_secret": "vault:auth/github_secret",
"authorize_url": "https://github.com/login/oauth/authorize",
"token_url": "https://github.com/login/oauth/access_token",
"userinfo_url": "https://api.github.com/user",
"scopes": ["read:user", "user:email"],
"pkce": false
}'::jsonb,
true
),
(
'google',
'Google',
'oidc',
'{
"client_id": "123456789.apps.googleusercontent.com",
"client_secret": "vault:auth/google_secret",
"issuer": "https://accounts.google.com",
"scopes": ["openid", "email", "profile"],
"pkce": true
}'::jsonb,
true
);

Store client secrets in vault rather than inline:

Terminal window
# Via admin API or vault CLI
reactor vault set auth/github_secret "ghp_..."
reactor vault set auth/google_secret "GOCSPX-..."

Ensure Reactor.toml has the correct public URL:

[auth]
data_key = "{{ env REACTOR_AUTH_DATA_KEY }}"
public_url = "https://api.myapp.com"
jwt_issuer = "reactor-auth"
jwt_audience = "reactor"
access_ttl_secs = 3600
refresh_ttl_secs = 604800

For local development:

[auth]
public_url = "http://localhost:8000"

And register localhost callbacks with your OAuth provider:

http://localhost:8000/auth/v1/callback/github

src/lib/auth.ts
import { createClient } from "@reactor/client";
const reactor = createClient({ url: "https://api.myapp.com" });
export function signInWithGitHub() {
const redirectTo = `${window.location.origin}/auth/callback`;
window.location.href = reactor.auth.getOAuthUrl({
provider: "github",
redirect_to: redirectTo,
});
}
export function signInWithGoogle() {
const redirectTo = `${window.location.origin}/auth/callback`;
window.location.href = reactor.auth.getOAuthUrl({
provider: "google",
redirect_to: redirectTo,
});
}
src/pages/auth/callback.tsx
import { useEffect } from "react";
import { useNavigate, useSearchParams } from "react-router-dom";
import { reactor } from "../lib/auth";
export function AuthCallback() {
const [params] = useSearchParams();
const navigate = useNavigate();
useEffect(() => {
async function handleCallback() {
const code = params.get("code");
const provider = params.get("provider") ?? "github";
if (!code) {
navigate("/login?error=missing_code");
return;
}
const { session, user } = await reactor.auth.exchangeCodeForSession({
provider,
code,
redirect_to: `${window.location.origin}/auth/callback`,
});
// Store session
localStorage.setItem("reactor_session", JSON.stringify(session));
// Redirect based on new vs returning user
if (user.created_at === user.updated_at) {
navigate("/onboarding");
} else {
navigate("/dashboard");
}
}
handleCallback();
}, [params, navigate]);
return <p>Signing you in...</p>;
}
src/components/OAuthButtons.tsx
export function OAuthButtons() {
return (
<div className="flex flex-col gap-3">
<button onClick={signInWithGitHub} className="btn-oauth">
Continue with GitHub
</button>
<button onClick={signInWithGoogle} className="btn-oauth">
Continue with Google
</button>
</div>
);
}

Allow email/password users to link OAuth providers:

// Requires authenticated session
await reactor.auth.linkIdentity({
provider: "github",
redirect_to: `${window.location.origin}/settings/accounts/callback`,
});

Unlink:

await reactor.auth.unlinkIdentity({ provider: "github" });

Store provider tokens securely for calling third-party APIs on behalf of users:

// Server-side function only — never expose to client
const { data: identity } = await reactor.auth.admin.getUserIdentities(userId);
const githubToken = identity.find(i => i.provider === "github")?.access_token;
const repos = await fetch("https://api.github.com/user/repos", {
headers: { Authorization: `Bearer ${githubToken}` },
});

The same OAuth flow powers reactor cloud login:

Terminal window
reactor cloud login
# Opens browser → GitHub/Google OAuth
# Callback returns API key stored in OS keychain

This uses the control plane’s OAuth endpoints (/cloud/v1/auth/login), separate from project auth.


SymptomFix
redirect_uri_mismatchVerify callback URL matches provider registration exactly
invalid_clientCheck client ID and secret; ensure secret is in vault
User created but no emailAdd user:email scope (GitHub) or email scope (Google)
PKCE errorEnable pkce: true for providers that require it
CORS error on callbackCallback is a redirect, not XHR — ensure you’re using window.location.href
Terminal window
# Debug auth issues
reactor logs --capability auth --since 10m
curl -H "Authorization: Bearer $ADMIN_TOKEN" \
http://localhost:8000/_admin/doctor | jq '.auth'

  • Redirect URIs restricted to your domains only
  • Client secrets in vault, not in migrations committed to git
  • PKCE enabled for public clients
  • Minimal OAuth scopes requested
  • Provider tokens accessed server-side only