Add authentication to any app in minutes. Users log in through a hosted, branded page — your app just handles the callback.
Create App
Register in Admin Panel, get Client ID + Secret
Redirect
Send user to /login/{appSlug}
Exchange Code
POST /api/auth/token with the auth code
Get Profile
Call /api/auth/userinfo with access token
Go to the Admin Panel → Applications → New Application. Fill in your app name, redirect URI, and choose which login methods to allow.
After creating, configure per-app settings:
Each application gets its own login page at /login/{appSlug} with full branding support.
to continue to App
to continue to My SaaS
import { NextResponse } from 'next/server'
import { cookies } from 'next/headers'
export async function GET() {
const state = crypto.randomUUID()
const cookieStore = await cookies()
cookieStore.set('auth_state', state, {
httpOnly: true, secure: true, sameSite: 'lax', maxAge: 600,
})
const params = new URLSearchParams({
client_id: process.env.AUTH_CLIENT_ID!,
redirect_uri: process.env.AUTH_REDIRECT_URI!,
state,
response_type: 'code',
scope: 'openid profile email',
})
return NextResponse.redirect(
`https://auth.svc.jxs.se/login/${process.env.AUTH_APP_SLUG}?${params}`
)
}Exchange the code for tokens and store the refresh_token — use it to silently renew access tokens without re-login.
const tokenRes = await fetch('https://auth.svc.jxs.se/api/auth/token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
grant_type: 'authorization_code',
code,
redirect_uri: process.env.AUTH_REDIRECT_URI,
client_id: process.env.AUTH_CLIENT_ID,
client_secret: process.env.AUTH_CLIENT_SECRET,
}),
})
const { access_token, id_token, refresh_token } = await tokenRes.json()
// Get user profile (includes role if configured)
const profile = await fetch('https://auth.svc.jxs.se/api/auth/userinfo', {
headers: { Authorization: `Bearer ${access_token}` },
}).then(r => r.json())
// profile = { sub, email, name, email_verified, role? }Access tokens expire after 1 hour. Use the refresh_token to get a new one silently. Tokens are rotated on each use (old one is revoked).
const res = await fetch('https://auth.svc.jxs.se/api/auth/token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
grant_type: 'refresh_token',
refresh_token: storedRefreshToken,
client_id: process.env.AUTH_CLIENT_ID,
client_secret: process.env.AUTH_CLIENT_SECRET,
}),
})
const { access_token, refresh_token } = await res.json()
// Store the new refresh_token — the old one is now revokedIf you configure roles for your app (Admin → App → Roles tab), the user's role is included in tokens automatically.
// From id_token / access_token JWT payload:
{ sub: "...", email: "...", role: "admin" }
// From userinfo endpoint:
{ sub: "...", email: "...", name: "...", role: "admin", email_verified: true }
// Read role directly from JWT (no API call needed):
const payload = JSON.parse(atob(id_token.split('.')[1]))
console.log(payload.role) // "guest" | "user" | "admin" | nullLink your users to a hosted change-password page — no UI to build. Pass the user's current access_token and a redirect_uri to return to after success.
// Build the change-password URL in your app:
const changePasswordUrl = new URL('https://auth.svc.jxs.se/account/change-password')
changePasswordUrl.searchParams.set('token', session.access_token)
changePasswordUrl.searchParams.set('redirect_uri', 'https://yourapp.com/settings')
changePasswordUrl.searchParams.set('client_id', process.env.AUTH_CLIENT_ID)
// Link or redirect the user:
<a href={changePasswordUrl.toString()}>Change password</a>access_token in your session alongside the refresh_token.import { redirect } from 'next/navigation'
import { getSession } from '@/lib/session'
export default async function Dashboard() {
const session = await getSession()
if (!session) redirect('/auth/login')
return (
<div>
<p>Welcome, {session.name}!</p>
<p>Role: {session.role ?? 'none'}</p>
</div>
)
}# .env AUTH_SERVICE_URL=https://auth.svc.jxs.se AUTH_CLIENT_ID=your-client-id # from admin panel AUTH_CLIENT_SECRET=your-secret # from admin panel (keep private!) AUTH_REDIRECT_URI=https://yourapp.com/auth/callback AUTH_APP_SLUG=your-app-slug # your app's slug SESSION_SECRET=generate-a-random-32-char-secret
✓ Rate limiting
Login endpoint blocks after 10 failed attempts per email per 15 min (Redis sliding window).
✓ Password policy
Min 8 chars, one uppercase, one number — enforced on signup, invite, and change password.
✓ Email verification
New signups receive a verification email. email_verified claim in tokens reflects real state.
✓ Refresh token rotation
Each refresh_token is single-use. Revoked on use; replaces itself with a new one.
✓ Account suspension
Admins can suspend a user per-app. Suspended users are blocked at login even with correct credentials.
✓ Auth logs
Every login, failure, registration and token refresh is logged. Viewable in Admin per app or globally.
/login/{appSlug}Hosted login/register page. Query params: redirect_uri, client_id, state, scope, response_type.
/account/change-passwordHosted change-password page. Query params: token (access_token), redirect_uri, client_id.
/api/auth/app-loginLog in with email + password. Body: { email, password, applicationId, redirect_uri?, state? }. Rate limited to 10 attempts / 15 min.
/api/auth/app-registerRegister new user. Body: { email, password, name, applicationId, redirect_uri?, state? }. Blocked if app.allowSignup=false.
/api/auth/tokenExchange auth code or refresh token. grant_type: "authorization_code" or "refresh_token". Returns: { access_token, id_token, refresh_token }.
/api/auth/userinfoGet user profile. Header: Authorization: Bearer {access_token}. Returns: { sub, email, name, email_verified, role? }
/api/auth/account/change-passwordChange password via access token. Body: { token, currentPassword, newPassword }. Used by the hosted change-password page.
/api/auth/verify-emailVerify email address from link. Query param: token (email verification JWT). Sets emailVerified on success.
/.well-known/openid-configurationOIDC discovery document. Standard endpoint for auto-discovery of all auth endpoints.
testapp.app.jxs.se is a minimal Next.js app that authenticates via this identity service.
Source at ~/workspace/test-app/ — use it as a starter template.