https://github.com/ricardoqmd/auth
Reusable authentication primitives for Next.js + Keycloak. XState-powered, framework-agnostic core.
https://github.com/ricardoqmd/auth
authentication keycloak nextjs oidc rbac react sso typescript xstate
Last synced: 3 days ago
JSON representation
Reusable authentication primitives for Next.js + Keycloak. XState-powered, framework-agnostic core.
- Host: GitHub
- URL: https://github.com/ricardoqmd/auth
- Owner: ricardoqmd
- License: mit
- Created: 2026-05-02T20:57:00.000Z (about 2 months ago)
- Default Branch: main
- Last Pushed: 2026-06-23T05:55:06.000Z (8 days ago)
- Last Synced: 2026-06-23T07:24:23.050Z (8 days ago)
- Topics: authentication, keycloak, nextjs, oidc, rbac, react, sso, typescript, xstate
- Language: TypeScript
- Homepage:
- Size: 250 KB
- Stars: 1
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- Contributing: CONTRIBUTING.md
- License: LICENSE
- Security: SECURITY.md
- Roadmap: ROADMAP.md
Awesome Lists containing this project
README
# @ricardoqmd/auth
> Reusable authentication primitives for Next.js apps using Keycloak — designed for SPAs, with a clean state machine at the core and pluggable adapters.
[](https://sonarcloud.io/summary/new_code?id=ricardoqmd_auth)
[](https://sonarcloud.io/summary/new_code?id=ricardoqmd_auth)
[](./LICENSE)
[](https://pnpm.io/)
[](https://www.typescriptlang.org/)
## Why?
Spinning up a new Next.js app with Keycloak shouldn't take a sprint. This monorepo packages the OIDC dance once — token init, refresh, logout, RBAC helpers — so every new project just runs `npm install` and gets a working `` and `useAuth()` hook with redirect-on-boot behavior out of the box.
## Architecture
```
┌─────────────────────────────────────────────────────────────┐
│ Your Next.js app │
│ │
│ useAuth() { token, user, hasRole, ... }│
└──────────────────────────┬──────────────────────────────────┘
│
┌──────────▼──────────┐
│ @ricardoqmd/ │
│ auth-nextjs │ ← React/Next.js bindings
└──────────┬──────────┘
│
┌──────────▼──────────┐
│ @ricardoqmd/ │
│ auth-core │ ← XState machine (framework-agnostic)
└──────────┬──────────┘
│ AuthProvider interface
┌──────────▼──────────┐
│ @ricardoqmd/ │
│ auth-keycloak │ ← keycloak-js adapter
└─────────────────────┘
```
The split lets you swap the IDP adapter (Keycloak today, Auth0/Cognito tomorrow) without touching your Next.js app.
## Packages
| Package | Description | Version |
| -------------------------------- | -------------------------------------------------------- | ------- |
| `@ricardoqmd/auth-core` | Framework-agnostic XState machine + types | [](https://www.npmjs.com/package/@ricardoqmd/auth-core) |
| `@ricardoqmd/auth-keycloak` | Keycloak adapter using `keycloak-js` | [](https://www.npmjs.com/package/@ricardoqmd/auth-keycloak) |
| `@ricardoqmd/auth-nextjs` | Next.js client-side bindings (`AuthProvider`, `useAuth`) | [](https://www.npmjs.com/package/@ricardoqmd/auth-nextjs) |
| `@ricardoqmd/auth-nextjs-ssr` | Middleware + JWT validation (server-side) | 🔜 planned |
## Installation
```bash
npm install @ricardoqmd/auth-nextjs @ricardoqmd/auth-keycloak @ricardoqmd/auth-core
# peer dependencies
npm install keycloak-js react react-dom next
```
Quick start:
```tsx
// providers.tsx (Client Component)
"use client";
import { AuthProvider } from "@ricardoqmd/auth-nextjs";
import { createKeycloakProvider } from "@ricardoqmd/auth-keycloak";
const provider = createKeycloakProvider({
config: { url: "https://kc.example.com", realm: "my-realm", clientId: "my-app" },
});
export function Providers({ children }: { children: React.ReactNode }) {
return {children};
}
```
```tsx
// Any Client Component inside
import { useAuth } from "@ricardoqmd/auth-nextjs";
import { hasResourceRole } from "@ricardoqmd/auth-keycloak";
import type { KeycloakIdpClaims } from "@ricardoqmd/auth-keycloak";
export function Dashboard() {
const { user, logout, hasRole, idpClaims } = useAuth();
return (
<>
Welcome, {user?.preferred_username}
{hasRole("admin") && }
{hasResourceRole(idpClaims, "my-app", "editor") && }
Sign out
>
);
}
```
> Sign-in on demand (`login()`), structured error handling (`AuthError.code`), and
> public/protected route patterns are documented in
> [`@ricardoqmd/auth-nextjs`](./packages/auth-nextjs/README.md).
## Local development
### Prerequisites
- Node.js >= 18.18
- pnpm >= 9 (`npm install -g pnpm`)
- Docker + Docker Compose
### Setup
```bash
# 1. Install dependencies
pnpm install
# 2. Start Keycloak (ports 8080)
pnpm kc:up
# 3. Wait ~30s for Keycloak to import the realm, then visit:
# http://localhost:8080/admin (admin / admin)
# Realm: demo
# 4. Configure the demo app
cp apps/demo/.env.example apps/demo/.env.local
# 5. Run packages in watch mode + the demo app
pnpm dev # builds packages on change
pnpm demo # in another terminal — http://localhost:3000
```
### Demo users
| Username | Password | Realm roles | Client roles (`demo-app`) |
| -------- | ---------- | ------------ | ------------------------- |
| ricardo | password | admin, user | editor, viewer |
| viewer | password | user | viewer |
### Useful scripts
```bash
pnpm kc:up # start Keycloak
pnpm kc:down # stop Keycloak
pnpm kc:logs # tail Keycloak logs
pnpm kc:reset # full reset (drops volume → re-imports realm)
pnpm build # build all packages
pnpm test # run tests
pnpm typecheck # type-check all packages
pnpm changeset # record a release-worthy change
```
## Roadmap
All three packages are published and **stable (1.0)** — the public API is frozen
and follows SemVer ([ADR-009](./docs/decisions/009-freeze-public-api-for-1.0.md)),
and was validated end-to-end on real infrastructure
([ADR-010](./docs/decisions/010-refine-1.0-consumer-gate.md)). SSR, additional IDP
adapters, and Vue bindings are post-1.0 and demand-driven.
Full plan: [`ROADMAP.md`](./ROADMAP.md) · Rationale: [ADR-006](./docs/decisions/006-harden-before-expand.md).
## License
MIT © ricardoqmd