@permok/auth-nextjs
v0.6.0
Published
Add Peekay Auth sign-in, sessions and the Backend API to a Next.js app.
Maintainers
Readme
@permok/auth-nextjs
Add Peekay Auth sign-in and sessions to a Next.js 16 app. Your app gets no user table and no password handling: users sign in on Peekay Auth and come back with verified tokens, scoped to your app only (no cross-app single sign-on).
apps/demo is a complete working example.
1. Register your app
From the auth repo:
pnpm idp:client:create --name "My App" \
--redirect https://myapp.example.com/api/auth/callback \
--logout https://myapp.example.comIt prints a client ID and a secret. The secret is shown once.
Add --third-party if the app isn't built by you. Its users will then see an "Unverified app" notice at sign-up.
2. Environment
PEEKAY_AUTH_ISSUER=https://auth.example.com
PEEKAY_AUTH_CLIENT_ID=app_...
PEEKAY_AUTH_CLIENT_SECRET=sk_...
PEEKAY_AUTH_APP_URL=https://myapp.example.com
# 32 random bytes: node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"
PEEKAY_AUTH_COOKIE_SECRET=...3. Wire it up (five files)
proxy.ts protects routes and keeps tokens fresh:
import { peekayProxy } from "@permok/auth-nextjs/proxy";
export default peekayProxy({ publicRoutes: ["/", "/pricing", "/blog/*"] });
export const config = {
matcher: ["/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp|ico)$).*)"],
};app/api/auth/[...peekay]/route.ts handles sign-in, the callback and sign-out:
import { handleAuth } from "@permok/auth-nextjs/server";
export const { GET, POST } = handleAuth();app/layout.tsx wraps the app and adds the components:
import "@permok/auth-nextjs/styles.css";
import { SignedIn, SignedOut, SignInButton } from "@permok/auth-nextjs/client";
import { PeekayProvider, UserButton } from "@permok/auth-nextjs/components";
export default function RootLayout({ children }) {
return (
<html lang="en">
<body>
<PeekayProvider>
<header>
<SignedIn><UserButton accountHref="/account" /></SignedIn>
<SignedOut><SignInButton /></SignedOut>
</header>
{children}
</PeekayProvider>
</body>
</html>
);
}app/account/page.tsx renders account management inside your app (see "Keep users in your
app" below) — this is the default setup, not an optional extra:
import { UserProfile } from "@permok/auth-nextjs/components";
export default function Page() { return <UserProfile />; }next.config.ts (needed only while the SDK is used from this monorepo as TypeScript source):
export default { transpilePackages: ["@permok/auth-nextjs"] };4. Use it
Server Components, Route Handlers and Server Actions:
import { getAuth, requireAuth, currentUser } from "@permok/auth-nextjs/server";
const { isSignedIn, userId, user } = await getAuth();
const auth = await requireAuth(); // redirects to sign-in if needed
const live = await currentUser(); // calls the IdP: also confirms the session wasn't revokedClient Components:
"use client";
import { useAuth } from "@permok/auth-nextjs/client";
const { isSignedIn, user } = useAuth();Reference
| Export | From | What it does |
| --- | --- | --- |
| peekayProxy({ publicRoutes }) | /proxy | Refreshes expiring tokens. Sends signed-out page visits to sign-in and returns 401 JSON to APIs. |
| handleAuth() | /server | Handles GET sign-in, GET callback and POST sign-out. |
| getAuth() | /server | The current user. Verified locally from the token, with no network call. |
| requireAuth() | /server | Same as getAuth(), but redirects to sign-in when signed out. |
| currentUser() | /server | Fresh profile from the IdP's userinfo endpoint. Returns null if the session ended. |
| getAccessToken() | /server | The raw access token, for calling your own APIs. |
| PeekayProvider, UserButton, UserProfile | /components | Server Components. UserProfile is an in-app account page (name, password). |
| SignIn, SignUp, ForgotPassword | /components | Embedded forms, no redirect (verified apps only). SignIn has a built-in "Forgot password?" link. |
| useAuth, SignedIn, SignedOut, SignInButton, SignOutButton | /client | Client Components and hooks. |
| peekayBackend(), createBackendClient(), PeekayApiError | /backend | Server-only Backend API client (needs an API key, see below). |
Sign-in links: /api/auth/sign-in?return_to=/somewhere.
Keep users in your app
<UserButton accountHref="/account" /> plus a page that renders <UserProfile /> (shown above) is
the default setup: users change their name and password without ever leaving your app. The forms
call /api/auth/account/* on your own server, which relays to the auth server with the user's
token (it never reaches browser code). This needs a verified app. Without accountHref,
"Manage account" falls back to a slim hosted account page with a "Back to your app" link, for apps
that haven't wired up an in-app route yet.
Backend API
Your server can list the people who use your app and sign them out. Create an API key in your
app's settings under Backend API keys and set PEEKAY_AUTH_API_KEY=pak_…. A key only sees
users who have signed in to its own app.
import { peekayBackend } from "@permok/auth-nextjs/backend";
const api = peekayBackend();
const { data: users } = await api.users.list({ limit: 50 });
await api.users.revokeSessions(users[0].id); // sessions:writeThe key acts for the app, not for the signed-in user. Full reference: <issuer>/docs/backend-api.
How it works
- Tokens. Sign-in uses the authorization code flow with PKCE. The access token (10 minutes), the refresh token and the ID token are kept in one cookie, encrypted with AES-256-GCM, httpOnly and SameSite=Lax. Browser JavaScript never sees a token.
- No cross-app SSO, and no shared accounts. The IdP session created by signing in is scoped to
your app's client ID, and so is the account itself: the same email can have a completely
separate Peekay Auth account on a different app. Being signed into another app (or the IdP's own
dashboard) never signs a user into yours, and
user.idis only stable within your own app. - Refresh. The proxy refreshes an access token within 60 seconds of expiry. It passes the new
cookie to the current request, so
getAuth()in that same render sees fresh claims. - Refresh tokens rotate. Each refresh token works once. Reusing an old one more than 30 seconds after it was replaced revokes the whole session at the IdP, because it means the token was copied. Within those 30 seconds the reuse is treated as a race between servers. On Vercel and other serverless hosts, two instances can refresh at the same moment, and both get valid tokens. The SDK also shares one refresh call between concurrent requests in the same process.
- Sign-out revokes the refresh token and ends the IdP session for this app.
- CSRF.
sign-outaccepts only POSTs from your app's own origin. The OAuthstatevalue and the RFC 9207isscheck protect the callback.
Changelog
0.4.0: Removed organizations end-to-end (
OrgSwitcher,getOrganizations(),orgId/orgRole/permissions/has(), the Backend API'sorganizationclient,switch-org) and closed cross-app SSO: a session is now scoped to the app that created it, so signing into one app or the IdP dashboard no longer silently signs a user into another app.<UserButton>no longer takesshowOrganizations.0.3.0:
<UserProfile />: an in-app account page (change name and password) for verified apps, backed by new/api/auth/account/*routes inhandleAuth().<UserButton>takesaccountHref. The default "Manage account" link now opens a slim hosted account page for the app's users. NewgetAccount()server helper.0.2.0: Backend API client,
@permok/auth-nextjs/backend: users (list, get, sign out of the app), authenticated with an app API key.0.1.1: Security: post-login
return_topaths containing control characters (for example a tab, which browsers strip, turning/\t/evil.cominto//evil.com) are now rejected. Upgrade recommended.0.1.0: First release.
