@gg-software/auth
Auth / session / permission logic. The main entry is pure — no React; the React
helpers live in the optional @gg-software/auth/react entry.
pnpm add @gg-software/authimport { decodeJwt, getSession, can } from '@gg-software/auth';
import { AuthProvider, useAuth } from '@gg-software/auth/react'; // optional, needs reactToken
parseBearerToken
Extract the token from an HTTP Authorization header value. Matching is case-insensitive
on the Bearer prefix and tolerant of surrounding whitespace.
parseBearerToken('Bearer abc123'); // "abc123"
parseBearerToken('Basic abc123'); // null
parseBearerToken(undefined); // null| Parameter | Type | Description |
|---|---|---|
header | string | null | undefined | the raw header value |
| returns | string | null | the token, or null if missing / not a Bearer token |
decodeJwt
Decode a JWT’s payload without verifying the signature. Use it to read claims client-side (expiry, user id) — never to trust a token; verification belongs on the server.
decodeJwt(token); // { sub: "42", exp: 1767225600, ... }
decodeJwt('not-a-jwt'); // null
decodeJwt<{ sub: string }>(token)?.sub; // typed access| Parameter | Type | Description |
|---|---|---|
token | string | null | undefined | the raw JWT (header.payload.signature) |
| returns | T | null | decoded payload (default type JwtPayload), or null when malformed |
type JwtPayload = {
iss?: string;
sub?: string;
aud?: string | string[];
exp?: number; // seconds since epoch
nbf?: number;
iat?: number;
jti?: string;
[claim: string]: unknown;
};isTokenExpired
Check the exp claim against the current time. A missing/malformed token counts as
expired (unusable); a valid token without exp counts as not expired.
isTokenExpired(freshToken); // false
isTokenExpired(oldToken); // true
isTokenExpired(oldToken, 60); // false within 60s of expiry (clock-skew leeway)
isTokenExpired(null); // true| Parameter | Type | Default | Description |
|---|---|---|---|
token | string | null | undefined | — | the raw JWT |
leewaySeconds | number | 0 | token stays valid this many seconds past exp |
| returns | boolean |
getTokenExpiry
Read the exp claim as a Date.
getTokenExpiry(token); // Date | null| Parameter | Type | Description |
|---|---|---|
token | string | null | undefined | the raw JWT |
| returns | Date | null | expiry, or null when malformed / no exp |
Session
Sessions are stored as JSON in localStorage under the key "gg-session" by default.
Every function takes optional SessionOptions to change that; all of them are SSR-safe
(no-ops / null when window is unavailable).
type AuthSession = {
token?: string;
user?: AuthUser;
[key: string]: unknown; // extend freely
};
type AuthUser = {
id?: string | number;
name?: string;
email?: string;
roles?: string[];
permissions?: string[];
[key: string]: unknown;
};
type SessionOptions = {
key?: string; // storage key (default "gg-session")
storage?: Storage; // storage backend (default window.localStorage)
};getSession
const session = getSession();
if (session?.token) fetchWithAuth(session.token);
// custom key / sessionStorage
getSession({ key: 'admin-session', storage: sessionStorage });| Parameter | Type | Description |
|---|---|---|
options | SessionOptions? | key / storage overrides |
| returns | T | null | the stored session (default type AuthSession), or null |
setSession
Persist the session and notify onSessionChange subscribers.
setSession({ token, user: { id: 42, name: 'Ada', roles: ['admin'] } });| Parameter | Type | Description |
|---|---|---|
session | T extends AuthSession | the session to persist |
options | SessionOptions? | key / storage overrides |
clearSession
Wipe the session (log out) and notify subscribers.
clearSession();onSessionChange
Subscribe to session changes — fires on setSession / clearSession in this tab and on
changes from other browser tabs (via the storage event). Returns an unsubscribe
function.
const stop = onSessionChange((session) => {
if (!session) redirectToLogin();
});
// later
stop();| Parameter | Type | Description |
|---|---|---|
callback | (session: T | null) => void | called with the new session (or null) |
options | SessionOptions? | key / storage overrides |
| returns | () => void | unsubscribe |
RBAC
Role / permission checks. They accept anything with optional roles / permissions
string arrays (RbacUser — the session’s AuthUser fits), and treat null /
undefined users as having nothing.
Permission format: "action:subject" strings, e.g. "read:orders". Granted
permissions may use wildcards: "*" grants everything, "read:*" any subject,
"*:orders" any action. Non-namespaced permissions (no :) only match exactly.
hasRole
hasRole(user, 'admin'); // user.roles includes "admin"
hasRole(null, 'admin'); // falsehasAnyRole
hasAnyRole(user, ['admin', 'editor']); // at least one of themhasPermission
// user.permissions = ['read:*', 'write:orders']
hasPermission(user, 'read:customers'); // true (read:*)
hasPermission(user, 'write:orders'); // true (exact)
hasPermission(user, 'delete:orders'); // falsecan
Policy-style shorthand: can(user, action, subject) ≡
hasPermission(user, `${action}:${subject}`).
can(user, 'delete', 'orders'); // checks "delete:orders"| Function | Signature |
|---|---|
hasRole | (user: RbacUser | null | undefined, role: string) => boolean |
hasAnyRole | (user: RbacUser | null | undefined, roles: string[]) => boolean |
hasPermission | (user: RbacUser | null | undefined, permission: string) => boolean |
can | (user: RbacUser | null | undefined, action: string, subject: string) => boolean |
React helpers (@gg-software/auth/react)
Optional entry — requires react (optional peer dependency; ships with 'use client', so
it works under the Next.js App Router).
AuthProvider
Provides the current session/user to the tree. Reads the persisted session after mount
(SSR-safe) and stays in sync with setSession / clearSession calls anywhere in the app —
including other tabs.
import { AuthProvider } from '@gg-software/auth/react';
<AuthProvider>
<App />
</AuthProvider>;| Prop | Type | Default | Description |
|---|---|---|---|
children (required) | ReactNode | — | content rendered inside the component |
storageKey | string | "gg-session" | storage key for the persisted session |
useAuth
Access the session plus login / logout. Throws outside an <AuthProvider>.
import { useAuth } from '@gg-software/auth/react';
function UserMenu() {
const { user, isAuthenticated, login, logout } = useAuth();
if (!isAuthenticated) return <LoginButton onDone={(session) => login(session)} />;
return <button onClick={logout}>Sign out {user?.name}</button>;
}Returned AuthContextValue:
| Field | Type | Description |
|---|---|---|
session | AuthSession | null | the persisted session |
user | AuthUser | null | shortcut for session.user |
isAuthenticated | boolean | session != null |
login | (session) => void | persist a session (storage + context) |
logout | () => void | wipe the session (storage + context) |
RequireAuth
Gate an area behind authentication.
import { RequireAuth } from '@gg-software/auth/react';
<RequireAuth fallback={<LoginPage />}>
<AdminArea />
</RequireAuth>;| Prop | Type | Default | Description |
|---|---|---|---|
children (required) | ReactNode | — | rendered when authenticated |
fallback | ReactNode | null | rendered when there is no session |
usePermission
Boolean permission check against the current user (wildcards respected, see
hasPermission).
import { usePermission } from '@gg-software/auth/react';
function DeleteButton() {
const canDelete = usePermission('delete:orders');
return canDelete ? <Button danger>Delete</Button> : null;
}