In most frameworks, adding authentication involves:
In Alepha, authentication is built on two low-level primitives: $issuer and $auth. You can use them directly for full control, or use the higher-level presets for common scenarios.
$issuer: Token ManagementAn Issuer is a JWT token provider. It handles JWT token creation, verification, and role management. It doesn't care how users authenticate—it just issues and validates tokens.
1import { $issuer } from "alepha/security"; 2import { $env, t } from "alepha"; 3 4class AppSecurity { 5 env = $env(t.object({ 6 ISSUER_SECRET: t.string({ default: "***********" }), 7 })) 8 9 // Internal issuer: you control the secret, you can forge tokens10 internal = $issuer({11 name: "app",12 secret: this.env.ISSUER_SECRET,13 roles: [{14 name: "user",15 permissions: [{ name: "*" }],16 }],17 settings: {18 accessToken: { expiration: [15, "minutes"] },19 refreshToken: { expiration: [30, "days"] },20 }21 });22 23 // External issuer (delegation): validate tokens from Keycloak, Auth0, etc. You can't forge tokens here.24 external = $issuer({25 name: "keycloak",26 jwks: () => "https://auth.example.com/realms/myrealm/protocol/openid-connect/certs",27 roles: [{28 // Map role "user" to all permissions29 name: "user",30 permissions: [{ name: "*" }],31 }],32 });33}
$issuer in your app automatically enables security check.module:action).$issuer is considered low-level. You usually want to use high-level $realm for full user management.Use $issuer directly when:
$auth: Login FlowsThe $auth primitive handles how users authenticate. It supports multiple strategies:
1import { $auth } from "alepha/server/auth"; 2 3class AuthProviders { 4 env = $env(t.object({ 5 GITHUB_CLIENT_ID: t.string(), 6 GITHUB_CLIENT_SECRET: t.string(), 7 KEYCLOAK_URL: t.string(), 8 })); 9 10 // 1. Credentials: username/password11 credentials = $auth({12 issuer: this.security.internal,13 credentials: {14 account: async ({ username, password }) => {15 // Your validation logic here16 return await this.validateUser(username, password);17 }18 }19 });20 21 // 2. OAuth2: external provider (manual config)22 github = $auth({23 issuer: this.security.internal,24 oauth: {25 clientId: this.env.GITHUB_CLIENT_ID,26 clientSecret: this.env.GITHUB_CLIENT_SECRET,27 authorization: "https://github.com/login/oauth/authorize",28 token: "https://github.com/login/oauth/access_token",29 scope: "user:email",30 userinfo: async (tokens) => {31 // Fetch user profile from GitHub API32 return await fetchGitHubUser(tokens.access_token);33 },34 }35 });36 37 // 3. OIDC: OpenID Connect (Keycloak, Auth0, Okta, etc.)38 keycloak = $auth({39 oidc: {40 issuer: `${this.env.KEYCLOAK_URL}/realms/customers`,41 clientId: "my-app",42 },43 fallback: () => generateAnonymousToken(), // Optional: return anonymous token if not authenticated44 });45}
The oidc strategy auto-discovers endpoints from the issuer's .well-known/openid-configuration.
No manual URL wiring needed—just point it at your identity provider and go.
For most SaaS applications, you don't want to wire all this yourself. Alepha provides $realm—an extension of $issuer that includes:
1import { $realm } from "alepha/api/users"; 2 3class AppSecurity { 4 realm = $realm({ 5 settings: { 6 registrationAllowed: true, 7 emailRequired: true, 8 }, 9 identities: {10 google: true,11 }12 });13}
Similarly, instead of configuring OAuth2 manually, use the presets:
1import { $authGoogle, $authGithub, $authCredentials } from "alepha/server/auth"; 2 3class AuthProviders { 4 // Username/password with your $realm 5 credentials = $authCredentials(this.realm); 6 7 // Google OAuth2 (auto-configured) 8 google = $authGoogle(this.realm, { 9 clientId: "...",10 clientSecret: "...",11 });12 13 // GitHub OAuth2 (auto-configured)14 github = $authGithub(this.realm, {15 clientId: "...",16 clientSecret: "...",17 });18}
To protect an endpoint, tell the $action who is allowed in.
secure: true to require authentication. 1class UserController { 2 // Only logged-in users can see this 3 getProfile = $action({ 4 path: "/me", 5 handler: async ({ user }) => { 6 return user; 7 } 8 }); 9 10 noAuthNeeded = $action({11 secure: false,12 handler: () => "This is public!",13 });14}
On the client (React), you don't need to manage tokens manually. Alepha handles the cookies for you.
1import { useAuth } from "@alepha/react/auth"; 2import type { AuthProviders } from "./AuthProviders"; 3 4const LoginPage = () => { 5 // Type parameter gives you autocomplete for provider names 6 const auth = useAuth<AuthProviders>(); 7 8 if (auth.user) { 9 return (10 <div>11 <p>Welcome, {auth.user.name}</p>12 <button onClick={() => auth.logout()}>Logout</button>13 </div>14 );15 }16 17 return (18 <div>19 {/* OAuth/OIDC: redirects to provider */}20 <button onClick={() => auth.login("keycloak")}>21 Sign in with Keycloak22 </button>23 <button onClick={() => auth.login("github")}>24 Sign in with GitHub25 </button>26 27 {/* Credentials: pass username/password directly */}28 <form onSubmit={(e) => {29 e.preventDefault();30 auth.login("credentials", {31 username: email,32 password: password,33 });34 }}>35 <input type="email" placeholder="Email" />36 <input type="password" placeholder="Password" />37 <button type="submit">Sign in</button>38 </form>39 </div>40 );41};
The generic type useAuth<AuthProviders>() gives you autocomplete for provider names—no more typos.
OAuth providers redirect to the identity provider; credentials submit directly.
The can() helper checks if the current user has permission to call an action:
1const AdminPanel = () => { 2 const auth = useAuth<AuthProviders>(); 3 4 // Check permission before showing UI 5 if (!auth.can("deleteUser")) { 6 return <p>Access denied</p>; 7 } 8 9 return <button>Delete User</button>;10};
That's it. No complex contexts, no interceptors. It just works.