npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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/auth

Peer 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 onTokenRefreshError

PKCE 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.