@threekit/goto-auth
v0.1.0
Published
React OAuth 2.0 PKCE client library for authenticating against the Threekit platform
Readme
@goto/auth
React OAuth 2.0 PKCE client library for authenticating against the Threekit platform.
Installation
npm install @goto/authPeer dependency: React 18+
Quick Start
import { AuthProvider, AuthCallbackHandler, ProtectedRoute, useAuth } from '@goto/auth';
// 1. Wrap your app with AuthProvider
function App() {
return (
<AuthProvider config={{ clientId: 'YOUR_CLIENT_ID', redirectUri: 'http://localhost:3000/callback' }}>
<Router>
<Route path="/callback" element={<AuthCallbackHandler onSuccess={() => navigate('/')} />} />
<Route path="/" element={
<ProtectedRoute fallback={<LoginPage />}>
<Dashboard />
</ProtectedRoute>
} />
</Router>
</AuthProvider>
);
}
// 2. Use the hook anywhere inside AuthProvider
function Dashboard() {
const { user, logout } = useAuth();
return (
<div>
<p>Hello, {user?.name}</p>
<button onClick={logout}>Log out</button>
</div>
);
}
// 3. Trigger login from your login page
function LoginPage() {
const { login } = useAuth();
return <button onClick={() => login()}>Sign in with Threekit</button>;
}Configuration
Pass an AuthConfig object to <AuthProvider>:
<AuthProvider
config={{
// Required
clientId: 'your-oauth-client-id',
redirectUri: 'https://yourapp.com/callback',
// Optional (shown with defaults)
baseUrl: 'https://app.threekit.ai',
storage: 'memory',
refreshBufferSeconds: 60,
scopes: undefined,
// Override derived endpoint URLs
endpoints: {
authorization: 'https://app.threekit.ai/api/oauth/auth',
token: 'https://app.threekit.ai/api/oauth/token',
revocation: 'https://app.threekit.ai/api/oauth/token/revocation',
},
// Called when background token refresh fails
onTokenRefreshError: (error) => {
console.error('Refresh failed:', error);
},
}}
>
{children}
</AuthProvider>| Property | Type | Default | Description |
|---|---|---|---|
| clientId | string | required | OAuth client ID |
| redirectUri | string | required | Redirect URI (must match registered URI) |
| baseUrl | string | 'https://app.threekit.ai' | Platform base URL |
| scopes | string[] | undefined | Scopes to request. When omitted, uses the client's registered scopes |
| storage | 'memory' \| 'localStorage' \| 'sessionStorage' | 'memory' | Where to persist tokens |
| refreshBufferSeconds | number | 60 | Seconds before expiry to trigger token refresh |
| endpoints | object | Derived from baseUrl | Override OAuth endpoint URLs |
| onTokenRefreshError | (error: AuthError) => void | undefined | Callback for background refresh failures |
Storage strategies
memory(default) — tokens are held in-memory and cleared on page refresh. Most secure; suitable for SPAs where re-login on refresh is acceptable.localStorage— tokens survive page refreshes and new tabs. Use when you need persistent sessions.sessionStorage— tokens survive page refreshes but not new tabs.
Endpoint defaults
When endpoints is omitted, URLs are derived from baseUrl:
| Endpoint | Default path |
|---|---|
| authorization | {baseUrl}/api/oauth/auth |
| token | {baseUrl}/api/oauth/token |
| revocation | {baseUrl}/api/oauth/token/revocation |
Components
<AuthProvider>
Wraps your application and provides authentication context to all child components.
<AuthProvider config={authConfig}>
<App />
</AuthProvider>| Prop | Type | Description |
|---|---|---|
| config | AuthConfig | Configuration object (see above) |
| children | ReactNode | Your application |
On mount, AuthProvider checks for existing tokens in storage and restores the session if valid. On unmount, it cleans up background refresh timers.
<AuthCallbackHandler>
Mount this component at your redirectUri route. It processes the OAuth callback, exchanges the authorization code for tokens, and then either calls onSuccess or displays an error.
<Route path="/callback" element={
<AuthCallbackHandler
onSuccess={() => navigate('/dashboard')}
onError={(err) => console.error(err)}
loadingComponent={<Spinner />}
/>
} />| Prop | Type | Default | Description |
|---|---|---|---|
| onSuccess | () => void | Redirects to '/' | Called after successful token exchange |
| onError | (error: AuthError) => void | undefined | Called when callback fails |
| loadingComponent | ReactNode | "Completing sign in..." | Shown while processing |
If the callback fails, the component renders a default error UI with the error message and a "Go back" button. You can handle errors yourself via onError.
<ProtectedRoute>
Renders children only when the user is authenticated. Shows fallback while loading or when not authenticated.
<ProtectedRoute fallback={<LoginPage />} loginOptions={{ prompt: 'login' }}>
<Dashboard />
</ProtectedRoute>| Prop | Type | Default | Description |
|---|---|---|---|
| children | ReactNode | required | Content to show when authenticated |
| fallback | ReactNode | null | Shown while loading or when not authenticated |
| loginOptions | LoginOptions | undefined | Options passed to login() if auto-login is triggered |
Hook: useAuth()
Access authentication state and methods from any component inside <AuthProvider>.
const {
// State
isAuthenticated, // boolean — whether the user has a valid session
isLoading, // boolean — true during initialization and login/callback flow
user, // TokenUser | null — decoded JWT claims
error, // AuthError | null — last authentication error
// Methods
login, // (options?: LoginOptions) => Promise<void>
logout, // () => Promise<void>
getAccessToken, // () => Promise<string>
handleCallback, // () => Promise<void>
} = useAuth();Throws an error if called outside of <AuthProvider>.
login(options?)
Initiates the OAuth PKCE flow by redirecting the user to the authorization server.
await login();
// With options
await login({
scopes: ['openid', 'profile', 'email'],
prompt: 'login', // Force re-authentication
loginHint: '[email protected]', // Pre-fill email
state: 'custom-state', // Custom state parameter
});| Option | Type | Description |
|---|---|---|
| scopes | string[] | Override default scopes for this login |
| prompt | 'login' \| 'consent' | Force re-authentication or consent screen |
| loginHint | string | Pre-fill the email field on the login page |
| state | string | Custom state parameter (a random state is always generated for CSRF protection) |
logout()
Clears stored tokens, revokes the refresh token on the server, and resets auth state.
await logout();getAccessToken()
Returns a valid access token. If the current token is expired or about to expire, it automatically refreshes before returning.
const token = await getAccessToken();Concurrent calls to getAccessToken() share a single in-flight refresh request (single-flight deduplication).
handleCallback()
Processes the OAuth callback. This is called internally by <AuthCallbackHandler> — you typically don't need to call it directly.
HTTP Interceptors
Two utilities for automatically attaching the Bearer token to outgoing requests.
createAuthFetch
Wraps the native fetch API:
import { createAuthFetch, useAuth } from '@goto/auth';
function ApiClient() {
const { getAccessToken } = useAuth();
const authFetch = createAuthFetch(getAccessToken);
async function loadData() {
const res = await authFetch('https://api.example.com/data');
return res.json();
}
}The returned function has the same signature as fetch. It injects Authorization: Bearer <token> into every request, refreshing the token automatically if needed.
createAxiosInterceptor
Creates a request interceptor for axios:
import axios from 'axios';
import { createAxiosInterceptor, useAuth } from '@goto/auth';
function useApiClient() {
const { getAccessToken } = useAuth();
const api = axios.create({ baseURL: 'https://api.example.com' });
const { request } = createAxiosInterceptor(getAccessToken);
api.interceptors.request.use(request);
return api;
}Error Classes
All errors extend AuthError, which extends Error and includes a code property.
import { AuthError, TokenExpiredError, TokenRefreshError, CallbackError } from '@goto/auth';| Class | code | When |
|---|---|---|
| AuthError | 'auth_error' | Base class for all auth errors |
| TokenExpiredError | 'token_expired' | Token has expired and cannot be refreshed |
| TokenRefreshError | 'token_refresh_error' | Background token refresh failed |
| CallbackError | 'callback_error' | OAuth callback processing failed (e.g., state mismatch, authorization denied) |
Handle errors via the error state from useAuth() or the onTokenRefreshError config callback:
const { error } = useAuth();
if (error) {
switch (error.code) {
case 'callback_error':
// User denied authorization or state mismatch
break;
case 'token_refresh_error':
// Session expired, prompt re-login
break;
}
}Exported Types
import type {
AuthConfig,
AuthState,
TokenUser,
LoginOptions,
UseAuthReturn,
AuthProviderProps,
AuthCallbackHandlerProps,
ProtectedRouteProps,
} from '@goto/auth';TokenUser
Decoded JWT claims for the authenticated user.
interface TokenUser {
sub: string; // User ID
email?: string;
name?: string;
scopes?: string[];
[key: string]: unknown; // Additional claims
}AuthState
interface AuthState {
isAuthenticated: boolean;
isLoading: boolean;
user: TokenUser | null;
error: AuthError | null;
}LoginOptions
interface LoginOptions {
scopes?: string[];
state?: string;
prompt?: 'login' | 'consent';
loginHint?: string;
}How It Works
The library implements the OAuth 2.0 Authorization Code flow with PKCE:
1. login()
├── Generate PKCE code_verifier + code_challenge (SHA-256)
├── Generate random state (CSRF protection)
├── Store verifier + state in sessionStorage
└── Redirect to authorization server
2. User authenticates at authorization server
3. Server redirects back to redirectUri with ?code=...&state=...
4. AuthCallbackHandler
├── Parse code and state from URL
├── Retrieve stored verifier + state from sessionStorage
├── Validate state matches (CSRF check)
└── POST to token endpoint with code + code_verifier
5. Token received
├── Store access_token + refresh_token
├── Decode JWT for user info
└── Schedule automatic refresh (expiresAt - refreshBufferSeconds)
6. Automatic background refresh
├── Fires before token expiry
├── Single-flight: concurrent requests share one refresh
└── On failure: clears session, calls onTokenRefreshErrorPKCE parameters are stored in sessionStorage (not the configurable token storage) because they must survive the OAuth redirect but should not persist beyond the current browser tab.
Callback errors are persisted to sessionStorage to prevent ProtectedRoute from re-triggering login in an infinite redirect loop.
