Automated summary
Provides guidance on integrating Hanko authentication into a web application using Docker Compose, Next.js, and Phoenix.
Strengths
- Self-hosted solution with full control over user data
- Supports WebAuthn passkeys, passwords, and passcodes
- JWT-based sessions with JWKS verification
- Clear architecture for frontend and backend integration
Limitations
- Requires careful production configuration (HTTPS, secure cookies)
- Limited to the specific technology stack (Next.js, Phoenix)
- Hanko Cloud may be needed for third-party OAuth and custom domains
When you need a self-hosted, passkey-capable authentication system for a web application with a JavaScript frontend and a backend API.
When you need out-of-the-box OAuth providers or prefer a fully managed auth service like Auth0 or Firebase.
Security analysis
SafeThe skill is a static reference document with no executable commands or destructive actions. It contains no secrets, no obfuscated payloads, and no instructions that could compromise a system if followed.
No concerns found
How this analysis is madeExamples
I want to self-host Hanko authentication using Docker Compose. Show me the configuration for public and admin APIs, WebAuthn, session, and secrets.How do I add Hanko login and registration to my Next.js app using @teamhanko/hanko-elements? Include setup for web components and events.Write a Next.js middleware that verifies the Hanko JWT using JWKS from the Hanko API.Hanko Auth — Project Skill Reference
Self-hosted Hanko (ghcr.io/teamhanko/hanko:latest) via Docker Compose.
Public API: :8000, Admin API: :8001.
Frontend env var: NEXT_PUBLIC_HANKO_API_URL=http://localhost:8000.
Backend env var: HANKO_API_URL=http://hanko:8000 (internal Docker network).
Architecture
Browser
├── <hanko-auth /> web component → Hanko :8000 (login / register)
├── Cookie "hanko" (JWT, httpOnly, SameSite=Lax)
├── Next.js middleware → verifies JWT via JWKS
└── API calls to Phoenix :4000 → Bearer <jwt>
Phoenix
├── AuthPlug → verify JWT with JwksCache (ETS, 5 min TTL)
└── LoadCurrentUserPlug → find_or_create user by hanko_id (sub claim)
Hanko Config (hanko/config.yaml)
server:
public: { address: ":8000", cors: { allow_origins: ["http://localhost:3000"], allow_credentials: true } }
admin: { address: ":8001" }
webauthn:
relying_party: { id: "localhost", display_name: "Repousse", origins: ["http://localhost:3000"] }
session:
lifespan: "1h"
enable_auth_token_header: true # also sends token as X-Auth-Token header
cookie: { http_only: true, same_site: "lax", secure: false } # secure: true in prod
password: { enabled: true, min_length: 8 }
passcode: { ttl: 120 }
secrets:
keys: ["<32-char-secret>"] # rotate via array; first key used to sign
Production checklist:
secure: trueon cookieallow_origins→ real domainrelying_party.id+origins→ real domainwebauthn.relying_party.originsmust include protocol+host (no trailing slash)- Custom domain in Hanko Cloud (if Cloud) fixes 3rd-party OAuth + cookie scoping
Frontend (@teamhanko/hanko-elements)
Installation
npm install @teamhanko/hanko-elements @teamhanko/hanko-frontend-sdk
Register components (client component)
"use client";
import { register } from "@teamhanko/hanko-elements";
const HANKO_API_URL = process.env.NEXT_PUBLIC_HANKO_API_URL!;
useEffect(() => { register(HANKO_API_URL).catch(console.error); }, []);
Web components
<hanko-auth /> {/* combined login + registration */}
<hanko-login /> {/* login only */}
<hanko-registration /> {/* registration only */}
<hanko-profile /> {/* credential management */}
TypeScript — add to src/custom.d.ts:
declare module "react" {
namespace JSX {
interface IntrinsicElements {
"hanko-auth": HankoAuthElementProps;
"hanko-profile": HankoProfileElementProps;
"hanko-events": HankoEventsElementProps;
}
}
}
Events
// DOM event (used in this project)
document.addEventListener("hankoAuthSuccess", handler);
// SDK event
import { Hanko } from "@teamhanko/hanko-frontend-sdk";
const hanko = new Hanko(HANKO_API_URL);
hanko.onSessionCreated(() => router.push("/dashboard"));
Register options
register(url, {
shadow: true, // default true — shadow DOM
injectStyles: true, // default true
enablePasskeys: true, // default true
sessionCheckInterval: 3000, // ms minimum
sessionTokenLocation: "cookie", // "cookie" | "sessionStorage"
cookieDomain: "example.com",
cookieSameSite: "lax",
fallbackLanguage: "fr",
});
Frontend SDK (@teamhanko/hanko-frontend-sdk)
import { Hanko } from "@teamhanko/hanko-frontend-sdk";
const hanko = new Hanko(HANKO_API_URL);
// Current user
const user = await hanko.user.getCurrent();
// { id, email, ... }
// Logout
await hanko.user.logout();
Logout pattern (this project)
// lib/auth.ts
export async function logout() {
const hanko = getHanko();
await hanko.user.logout();
}
Next.js Middleware (JWT via JWKS)
Cookie name: hanko — contains a signed JWT (RS256).
// middleware.ts
import { jwtVerify, createRemoteJWKSet } from "jose";
const JWKS = createRemoteJWKSet(
new URL(`${HANKO_API_URL}/.well-known/jwks.json`)
);
const token = request.cookies.get("hanko")?.value;
await jwtVerify(token, JWKS); // throws on invalid/expired
JWKS endpoint: GET {HANKO_API_URL}/.well-known/jwks.json
Public paths in this project: ["/", "/auth/login", "/auth/register", "/api/webhooks"]
Matcher (excludes Next.js internals):
export const config = {
matcher: ["/((?!_next/static|_next/image|favicon.ico|.*\\.svg).*)"],
};
Phoenix/Elixir — JWT Validation
Flow
- Frontend sends
Authorization: Bearer <jwt>on all API calls AuthPlugextracts token, callsJwksCache.get_keys/0, verifies viaJoken- On success:
conn.assigns.hanko_claims=%{"sub" => hanko_user_id, "exp" => ..., "email" => ...} LoadCurrentUserPlugfinds or createsRepousse.Accounts.Userbyhanko_idconn.assigns.current_useravailable in controllers
JWT claims
%{
"sub" => "uuid-string", # Hanko user ID — used as hanko_id FK
"email" => "user@ex.com", # present on first login
"exp" => 1234567890, # unix timestamp
"iss" => "http://localhost:8000"
}
JwksCache (Repousse.Auth.JwksCache)
- GenServer, ETS table
:hanko_jwks - Refreshes every 5 min
- Config:
Application.get_env(:repousse, :hanko)[:jwks_url]→ should behttp://hanko:8000/.well-known/jwks.json(internal Docker URL) - Start in
application.exsupervision tree
Router pipeline
pipeline :authenticated do
plug RepousseWeb.Plugs.AuthPlug
plug RepousseWeb.Plugs.LoadCurrentUserPlug
end
pipeline :admin do
plug RepousseWeb.Plugs.RequireRolePlug, role: :admin
end
Alternative: POST /sessions/validate (simpler, no JWT lib)
# Not used in this project but available:
Req.post("#{hanko_url}/sessions/validate",
json: %{session_token: token}
)
# Response: %{"is_valid" => true, "user_id" => "..."}
Hanko Admin API (:8001)
No auth required from same network (internal only — never expose to internet).
Key endpoints
GET /users # list users (supports ?email=, ?page=, ?per_page=)
POST /users # create user
GET /users/:id # get user by Hanko ID
DELETE /users/:id # delete user
PATCH /users/:id # update user
GET /users/:id/credentials # list credentials (passkeys, passwords, OTPs)
DELETE /users/:id/credentials/:cred_id
GET /users/:id/emails # list emails
POST /users/:id/emails # add email
DELETE /users/:id/emails/:email_id
POST /users/:id/emails/:email_id/set_primary
GET /users/:id/sessions # active sessions
DELETE /users/:id/sessions/:session_id # revoke session
GET /audit_logs # audit trail
GET /webhooks # list configured webhooks
POST /webhooks # create webhook
Usage from Elixir (internal Docker network):
base = Application.get_env(:repousse, :hanko)[:admin_url]
# e.g. http://hanko:8001
Req.get("#{base}/users/#{hanko_id}")
Req.delete("#{base}/users/#{hanko_id}")
Webhooks
Requires Pro/Enterprise plan on Hanko Cloud. Self-hosted: configure in config.yaml.
Payload structure
{
"event": "user.login",
"token": "<JWT — decode to get data>"
}
JWT payload (after decode + JWKS verify):
{
"sub": "hanko webhooks",
"evt": "user.login",
"data": { /* full user object */ },
"iat": 1234567890,
"exp": 1234567890,
"aud": ["..."]
}
Event types
user.create # new registration
user.delete # account deleted
user.login # authentication event
user.update.email.create # email added
user.update.email.delete # email removed
user.update.email.primary # primary email changed
user.update.password.update
user.update.username.create / .delete / .update
Verification (Phoenix)
# 1. Fetch JWKS from {HANKO_API_URL}/.well-known/jwks.json
# 2. Verify token with JWKS (same JwksCache can be reused)
# 3. Decode payload → extract data.evt + data.data
# 4. Return 200 within 30 seconds
Passkey API (separate product — not used in this project)
Use only if adding passkeys to an existing auth system (not replacing it). This project uses full Hanko (auth + user management), so Passkey API is not relevant.
If ever needed:
- Hanko Cloud → create "Passkey API" project type
- Two endpoint tiers: frontend (no secret) for login, backend (secret key) for registration
- JS SDK:
@teamhanko/passkeys-sdk
Common Gotchas
| Problem | Cause | Fix |
|---------|-------|-----|
| Cookie not sent to Phoenix | Authorization: Bearer not set | Frontend must read cookie and send as header |
| JWKS fetch fails | Wrong URL in JwksCache config | Use Docker-internal URL http://hanko:8000 not localhost |
| WebAuthn fails locally | relying_party.id mismatch | Must match origin hostname exactly |
| Session not created event | Using wrong event name | DOM: hankoAuthSuccess, SDK: onSessionCreated |
| CORS error on login | Missing allow_credentials: true | Set in hanko/config.yaml + Phoenix CORSPlug |
| Token expired (1h) | Default lifespan | Extend in config or implement refresh |
| Admin API 404 | Wrong port | Admin is :8001, Public is :8000 |
| Passkey not offered | enablePasskeys: false | Remove option or set to true |
Relevant Files (this project)
| File | Purpose |
|------|---------|
| hanko/config.yaml | Hanko server config |
| docker-compose.yml | Hanko service + env vars |
| frontend/src/middleware.ts | Next.js route protection (JWKS verify) |
| frontend/src/lib/auth.ts | Hanko SDK singleton + logout |
| frontend/src/app/(main)/auth/login/page.tsx | <hanko-auth /> component + post-login redirect |
| backend/lib/repousse/auth/jwks_cache.ex | ETS-cached JWKS keys, 5 min refresh |
| backend/lib/repousse_web/plugs/auth_plug.ex | JWT verification via Joken |
| backend/lib/repousse_web/plugs/load_current_user_plug.ex | Hanko ID → DB user |
| backend/lib/repousse_web/router.ex | Pipeline composition |
From the file to a first result
The download contains exactly the content displayed on this page. This copy may differ from the latest version in the source repository.
View the source and supporting filesThis download contains SKILL.md only. Any scripts, references, templates and dependencies are not included. Get the required files from the source, preserving their directory structure, before continuing.
1. Download the file
Download SKILL.mdFrom the root of a test project, save the file as .skill-downloads/hanko-auth-integration-reference-mryiqibf/SKILL.md, or use the command below. This folder is for inspecting the skill before installation.
Command for macOS/Linux or WSL: requires a POSIX shell and curl. On Windows without WSL, use the download button and create the folder manually.
mkdir -p '.skill-downloads/hanko-auth-integration-reference-mryiqibf' && curl --fail --show-error --location 'https://skills-guides.com/api/skills/hanko-auth-integration-reference-mryiqibf/download' --output '.skill-downloads/hanko-auth-integration-reference-mryiqibf/SKILL.md.part' && mv '.skill-downloads/hanko-auth-integration-reference-mryiqibf/SKILL.md.part' '.skill-downloads/hanko-auth-integration-reference-mryiqibf/SKILL.md'2. Check prerequisites
Read SKILL.md before using it. If it references scripts or external services, inspect them and prepare the required access in your test environment.
Original excerpt: Installation
npm install @teamhanko/hanko-elements @teamhanko/hanko-frontend-sdk
The platforms on this page are catalog indications. The paths below follow tool documentation; they do not prove this skill has been run in those tools. No minimum tool version is confirmed for this listing.
3. Load the skill
This file does not provide a usable name/description header for this native procedure. It remains available to download and inspect; use the manual method below.
Manual method or another tool: attach SKILL.md to a conversation, or open it in an agent that can read files. Include any required supporting resources. No universal folder is recognized by every tool.
4. Try a first task
Use a short, synthetic input: an API endpoint, a small document or an interface brief. In a new conversation, attach the file or give the agent its path, then adapt this request:
Read .skill-downloads/hanko-auth-integration-reference-mryiqibf/SKILL.md and apply its instructions to this example: [describe a test input]. Identify any missing files, dependencies or access. Produce the result requested by the skill without inventing data or running unreviewed scripts.Expected result: output matching the skill instructions, or a precise list of missing prerequisites. A read confirmation alone does not validate a first use. If an external resource is unavailable, that path remains untested.
DeFi AMM Security
Security
Security checklist for Solidity AMM contracts, liquidity pools, and swap flows. Covers reentrancy, donation attacks, oracle manipulation, slippage, and integer math.
Semgrep Rule Creator
Security
Creates custom Semgrep rules for detecting security vulnerabilities, bug patterns, and code patterns. Includes test-first methodology, taint mode support, and validation steps. Helps when building targeted static analysis detections.
Dev Toolbar Review Guide
Security
Safety rules for the dev toolbar, PostHog client, and feature flags. Use when reviewing changes to dev-tools and related files, covering environment guards, override cookies, and SSE stream safety.