digitwhale-auth
v0.7.0
Published
Digitwhale Auth SDK - OAuth 2.0 Authorization Code Flow with PKCE
Maintainers
Readme
Digitwhale Auth — TypeScript SDK
OAuth 2.0 Authorization Code Flow with PKCE for TypeScript/JavaScript applications.
Installation
npm install digitwhale-authQuick Start
import { DigitwhaleAuth, MemoryTokenStore } from "digitwhale-auth";
const auth = new DigitwhaleAuth(new MemoryTokenStore());
const { url, pkce, state } = await auth.buildAuthorizeUrl(
"your_client_id",
"yourapp://callback",
"read write"
);
console.log("Open this URL:", url);
const tokens = await auth.exchangeCode(
"authorization_code_from_callback",
"yourapp://callback",
"your_client_id",
pkce
);
console.log("Logged in:", tokens.user);Import Paths
| What you need | Import from |
|---|---|
| Core SDK (any runtime) | "digitwhale-auth" |
| React components / hooks | "digitwhale-auth/react" |
The digitwhale-auth entry point has zero React dependency — safe for Node
backends, React Native without Expo, Deno, and browsers without React.
React components live in digitwhale-auth/react and require react >= 17 as a
peer dependency.
Extending Types
All types (UserInfo, StoredTokens, TokenResponse) include an index signature
so you can add custom fields from your own system:
import type { UserInfo } from "digitwhale-auth";
// Extend with your own fields
interface MyUser extends UserInfo {
department?: string;
employee_id?: number;
roles?: string[];
}
// The server returns extra fields — they're captured automatically
const user: MyUser = await auth.user();
console.log(user.department); // typed as string | undefined
console.log(user.roles); // typed as string[] | undefinedOr access custom fields directly without extending:
const user = await auth.user();
const department = user.department as string | undefined;Scenarios
1. One-Click Button (Recommended)
A React component that handles everything — popup, redirect, code exchange:
import { DigitwhaleAuth, LocalStorageTokenStore } from "digitwhale-auth";
import { DigitwhaleAuthButton } from "digitwhale-auth/react";
const auth = new DigitwhaleAuth(new LocalStorageTokenStore());
<DigitwhaleAuthButton
auth={auth}
clientId="my_web_app"
redirectUri="https://myapp.com/auth/callback"
scope="read write"
onSuccess={(tokens) => {
console.log("Logged in:", tokens.user?.email);
router.push("/dashboard");
}}
onError={(error) => {
toast.error(error.message);
}}
/>2. React Hook
Full control with useDigitwhaleAuth:
import { DigitwhaleAuth, LocalStorageTokenStore } from "digitwhale-auth";
import { useDigitwhaleAuth } from "digitwhale-auth/react";
const auth = new DigitwhaleAuth(new LocalStorageTokenStore());
function LoginPage() {
const { login, logout, user, isAuthenticated, isLoading, error } =
useDigitwhaleAuth(auth);
if (isAuthenticated) {
return (
<div>
<p>Welcome, {user?.email}</p>
<button onClick={logout}>Sign out</button>
</div>
);
}
return (
<div>
{error && <p className="error">{error}</p>}
<button
onClick={() => login("my_app", "https://myapp.com/auth/callback")}
disabled={isLoading}
>
{isLoading ? "Signing in..." : "Sign in with Digitwhale"}
</button>
</div>
);
}3. Web — Popup Flow
Opens a popup window for the auth flow, closes on success:
const { url, pkce, state } = await auth.buildAuthorizeUrl(
"my_app",
"https://myapp.com/auth/callback"
);
const popup = window.open(url, "auth", "width=600,height=700,popup=yes");
// Poll for redirect
const interval = setInterval(async () => {
if (popup?.closed) {
clearInterval(interval);
await auth.restoreSession();
return;
}
try {
if (popup.location.href.startsWith("https://myapp.com/auth/callback")) {
const params = new URLSearchParams(popup.location.search);
const code = params.get("code");
if (code) {
await auth.exchangeCode(code, redirectUri, clientId, pkce);
popup.close();
clearInterval(interval);
}
}
} catch {
// Cross-origin
}
}, 500);4. Web — Redirect Flow
Redirects the browser to the auth page:
const { url, pkce, state } = await auth.buildAuthorizeUrl(
"my_app",
"https://myapp.com/auth/callback"
);
// Store pkce and state for later
sessionStorage.setItem("pkce", JSON.stringify(pkce));
sessionStorage.setItem("state", state);
// Redirect
window.location.href = url;// On callback page (/auth/callback)
const params = new URLSearchParams(window.location.search);
const code = params.get("code");
const state = params.get("state");
const storedState = sessionStorage.getItem("state");
if (state !== storedState) {
throw new Error("Invalid state");
}
const pkce = JSON.parse(sessionStorage.getItem("pkce")!);
await auth.exchangeCode(code!, redirectUri, clientId, pkce);
router.push("/dashboard");5. React Native / Expo
import * as Linking from "expo-linking";
const { url, pkce, state } = await auth.buildAuthorizeUrl(
"my_app",
"myapp://auth/callback"
);
// Open in browser
Linking.openURL(url);
// Listen for deep link
Linking.addEventListener("url", async ({ url }) => {
const uri = new URL(url);
const code = uri.searchParams.get("code");
const returnedState = uri.searchParams.get("state");
if (returnedState !== state) {
console.error("CSRF detected");
return;
}
if (code) {
const tokens = await auth.exchangeCode(
code,
"myapp://auth/callback",
"my_app",
pkce
);
console.log("Logged in:", tokens.user?.email);
}
});6. Express Backend
app.get("/auth/login", async (req, res) => {
const { url, pkce, state } = await auth.buildAuthorizeUrl(
"server_app",
"https://myapp.com/auth/callback"
);
req.session.pkce = pkce;
req.session.state = state;
res.redirect(url);
});
app.get("/auth/callback", async (req, res) => {
const { code, state } = req.query;
if (state !== req.session.state) return res.status(403).send("Invalid state");
await auth.exchangeCode(
code as string,
"https://myapp.com/auth/callback",
"server_app",
req.session.pkce
);
res.redirect("/dashboard");
});7. Error Handling
import { AuthError, ErrorType } from "digitwhale-auth";
try {
await auth.exchangeCode(code, redirectUri, clientId, pkce);
} catch (err) {
if (err instanceof AuthError) {
switch (err.type) {
case ErrorType.CodeExpired:
showNotification("Code expired, please try again");
break;
case ErrorType.PkceFailed:
showNotification("Security verification failed");
break;
case ErrorType.MfaRequired:
showNotification("MFA code required — re-initiate with MFA");
break;
case ErrorType.RefreshTokenRevoked:
showNotification("Session compromised, logging out");
await auth.signOut();
break;
case ErrorType.TokenFamilyRevoked:
showNotification("All tokens revoked — re-authenticate");
await auth.signOut();
break;
default:
if (err.retryable) {
showNotification("Network error, retrying...");
} else {
showNotification(`Auth error: ${err.message}`);
}
}
}
}8. Confidential Clients (server-side)
// For confidential clients, pass client_secret in the exchangeCode call
const tokens = await auth.exchangeCode(
code,
"https://myapp.com/auth/callback",
"server_app",
pkce,
"sec_xxxxx" // clientSecret
);9. Fetch User Info
const user = await auth.user();
console.log(`Logged in as: ${user.first_name} ${user.last_name}`);10. Update User Profile
const updated = await auth.updateUser({
first_name: "Jane",
last_name: "Doe",
phone_number: "+1234567890",
date_of_birth: "1990-01-01",
nationality: "US",
});11. Change Password
await auth.changePassword("current_pass", "new_pass", "new_pass");
// All existing tokens are revoked — user must re-authenticate12. Refresh Tokens
// Explicitly refresh (normally handled automatically by getAccessToken)
const refreshed = await auth.refresh();
console.log(`New token expires at: ${new Date(refreshed.expiresAt).toISOString()}`);13. Logout
// Revoke token on server + clear local storage
await auth.logout();14. Sign Out (local only)
// Clear local tokens without server revocation
await auth.signOut();Rate Limiting
The auth server enforces rate limits per IP address. If you hit a 429 Too Many Requests, wait and retry. The SDK does not automatically retry — handle this in your application code.
Token Revocation
When a user changes their password or deletes their account, all existing tokens are revoked. The SDK will receive a 401 Unauthorized on the next API call. Handle this by calling auth.signOut() and redirecting to the login page.
