bkend-auth
bkend.ai authentication and security expert skill
1. Auth Overview
bkend.ai uses JWT-based authentication with a dual-token strategy:
| Token | Type | Lifetime | Purpose |
|---|---|---|---|
| Access Token | JWT | 1 hour | API request authorization |
| Refresh Token | Opaque | 30 days | Obtain new access tokens |
Supported Authentication Methods
- Email/Password -- traditional signup and login
- Magic Link -- passwordless email-based login
- Social Login (OAuth) -- Google, GitHub
- API Key -- server-to-server (tenant-level, not user-level)
Required Headers
All auth endpoints require these headers:
X-Project-Id: <your-project-id>
X-Environment: <dev|staging|prod>
Content-Type: application/json
Authenticated endpoints additionally require:
Authorization: Bearer <access-token>
Auth Response Structure
All successful auth responses follow this pattern:
{
"success": true,
"data": {
"user": {
"id": "usr_abc123",
"email": "user@example.com",
"name": "Alice",
"role": "user",
"emailVerified": true,
"createdAt": "2025-01-15T09:00:00.000Z",
"updatedAt": "2025-01-15T09:00:00.000Z"
},
"tokens": {
"accessToken": "eyJhbGciOiJIUzI1NiIs...",
"refreshToken": "rt_a1b2c3d4e5f6...",
"expiresIn": 3600
}
}
}
2. Email Authentication
2.1 Signup
Endpoint: POST /auth/email/signup
Request:
{
"email": "user@example.com",
"password": "SecureP@ss123",
"name": "Alice Kim"
}
Response (201 Created):
{
"success": true,
"data": {
"user": {
"id": "usr_abc123",
"email": "user@example.com",
"name": "Alice Kim",
"role": "user",
"emailVerified": false,
"createdAt": "2025-01-15T09:00:00.000Z"
},
"tokens": {
"accessToken": "eyJhbGciOiJIUzI1NiIs...",
"refreshToken": "rt_a1b2c3d4e5f6...",
"expiresIn": 3600
}
}
}
Password Requirements:
- Minimum 8 characters
- At least one uppercase letter
- At least one lowercase letter
- At least one number
- At least one special character
Error Responses:
| HTTP Status | Error Code | Description |
|---|---|---|
| 400 | INVALID_EMAIL |
Email format is invalid |
| 400 | WEAK_PASSWORD |
Password does not meet requirements |
| 409 | EMAIL_ALREADY_EXISTS |
Account with this email already exists |
| 400 | MISSING_REQUIRED_FIELD |
Required field (email, password) is missing |
bkendFetch Example:
const result = await bkendFetch("/auth/email/signup", {
method: "POST",
body: JSON.stringify({
email: "user@example.com",
password: "SecureP@ss123",
name: "Alice Kim",
}),
});
// Store tokens
const { accessToken, refreshToken } = result.data.tokens;
2.2 Login
Endpoint: POST /auth/email/signin
Request:
{
"email": "user@example.com",
"password": "SecureP@ss123"
}
Response (200 OK):
{
"success": true,
"data": {
"user": {
"id": "usr_abc123",
"email": "user@example.com",
"name": "Alice Kim",
"role": "user",
"emailVerified": true,
"lastLoginAt": "2025-01-20T14:30:00.000Z"
},
"tokens": {
"accessToken": "eyJhbGciOiJIUzI1NiIs...",
"refreshToken": "rt_x9y8z7w6v5u4...",
"expiresIn": 3600
}
}
}
Error Responses:
| HTTP Status | Error Code | Description |
|---|---|---|
| 401 | INVALID_CREDENTIALS |
Email or password is incorrect |
| 403 | ACCOUNT_DISABLED |
Account has been disabled |
| 403 | ACCOUNT_LOCKED |
Too many failed attempts (locked 30 min) |
| 429 | TOO_MANY_ATTEMPTS |
Rate limit exceeded |
2.3 Email Verification
Send verification email:
POST /auth/email/verify/resend
{
"email": "user@example.com"
}
Verify email with token:
POST /auth/email/verify
{
"token": "ev_abc123def456..."
}
Response (200 OK):
{
"success": true,
"data": {
"message": "Email verified successfully",
"emailVerified": true
}
}
3. Magic Link Authentication
Magic link provides passwordless authentication via email.
3.1 Send Magic Link
Endpoint: POST /auth/magiclink/send
Request:
{
"email": "user@example.com",
"redirectUri": "https://myapp.com/auth/callback"
}
Response (200 OK):
{
"success": true,
"data": {
"message": "Magic link sent to user@example.com",
"expiresIn": 600
}
}
The user receives an email with a link like:
https://api-client.bkend.ai/auth/magiclink/verify?token=ml_abc123...&redirectUri=https://myapp.com/auth/callback
3.2 Verify Magic Link
Endpoint: GET /auth/magiclink/verify?token=<token>&redirectUri=<uri>
The server verifies the token and redirects to redirectUri with tokens as query parameters:
https://myapp.com/auth/callback?accessToken=eyJ...&refreshToken=rt_...&expiresIn=3600
Client-side handling:
// app/auth/callback/page.tsx
"use client";
import { useSearchParams, useRouter } from "next/navigation";
import { useEffect } from "react";
export default function AuthCallback() {
const searchParams = useSearchParams();
const router = useRouter();
useEffect(() => {
const accessToken = searchParams.get("accessToken");
const refreshToken = searchParams.get("refreshToken");
if (accessToken && refreshToken) {
// Store tokens securely
document.cookie = `bkend_access_token=${accessToken}; path=/; secure; samesite=lax; max-age=3600`;
document.cookie = `bkend_refresh_token=${refreshToken}; path=/; secure; samesite=lax; max-age=2592000`;
router.push("/dashboard");
} else {
router.push("/login?error=invalid_magic_link");
}
}, [searchParams, router]);
return <div>Authenticating...</div>;
}
Error Responses:
| HTTP Status | Error Code | Description |
|---|---|---|
| 400 | INVALID_MAGIC_LINK |
Token is invalid or malformed |
| 410 | MAGIC_LINK_EXPIRED |
Token has expired (10 min lifetime) |
| 400 | MAGIC_LINK_USED |
Token has already been used |
4. Social Login (OAuth)
4.1 Google OAuth
Console Configuration:
- Go to Console > Project > Settings > Auth > Social Login
- Enable Google provider
- Enter your Google Client ID and Client Secret
- Set authorized redirect URI:
https://api-client.bkend.ai/auth/social/google/callback
Initiate Google Login:
GET /auth/social/google?redirectUri=https://myapp.com/auth/callback
The server redirects the user to Google's OAuth consent screen. After authorization, the user is redirected back to your redirectUri with tokens:
https://myapp.com/auth/callback?accessToken=eyJ...&refreshToken=rt_...&expiresIn=3600
bkendFetch Example (redirect):
function handleGoogleLogin() {
const projectId = process.env.NEXT_PUBLIC_BKEND_PROJECT_ID;
const env = process.env.NEXT_PUBLIC_BKEND_ENVIRONMENT;
const redirectUri = encodeURIComponent(`${window.location.origin}/auth/callback`);
window.location.href =
`${process.env.NEXT_PUBLIC_BKEND_API_URL}/auth/social/google` +
`?redirectUri=${redirectUri}` +
`&projectId=${projectId}` +
`&environment=${env}`;
}
4.2 GitHub OAuth
Console Configuration:
- Go to Console > Project > Settings > Auth > Social Login
- Enable GitHub provider
- Enter your GitHub Client ID and Client Secret
- Set authorization callback URL:
https://api-client.bkend.ai/auth/social/github/callback
Initiate GitHub Login:
GET /auth/social/github?redirectUri=https://myapp.com/auth/callback
The flow is identical to Google. The user is redirected to GitHub for authorization, then back to your app with tokens.
Error Responses (Social Login):
| HTTP Status | Error Code | Description |
|---|---|---|
| 400 | SOCIAL_AUTH_FAILED |
OAuth provider returned an error |
| 400 | SOCIAL_EMAIL_NOT_FOUND |
Provider did not return an email |
| 409 | EMAIL_ALREADY_EXISTS |
Email is linked to another auth method |
| 400 | SOCIAL_PROVIDER_DISABLED |
Provider not enabled in project settings |
5. Token Management
5.1 Refresh Token
Endpoint: POST /auth/token/refresh
Request:
{
"refreshToken": "rt_a1b2c3d4e5f6..."
}
Response (200 OK):
{
"success": true,
"data": {
"accessToken": "eyJhbGciOiJIUzI1NiIs...",
"refreshToken": "rt_newtoken123...",
"expiresIn": 3600
}
}
Error Responses:
| HTTP Status | Error Code | Description |
|---|---|---|
| 401 | INVALID_REFRESH_TOKEN |
Refresh token is invalid |
| 401 | REFRESH_TOKEN_EXPIRED |
Refresh token has expired (30 day lifetime) |
| 401 | REFRESH_TOKEN_REVOKED |
Refresh token has been revoked |
5.2 Token Storage Patterns
Recommended: httpOnly Cookie (Server-rendered apps)
// API route: app/api/auth/login/route.ts
import { NextRequest, NextResponse } from "next/server";
import { bkendFetch } from "@/lib/bkend";
export async function POST(request: NextRequest) {
const body = await request.json();
const result = await bkendFetch("/auth/email/signin", {
method: "POST",
body: JSON.stringify(body),
});
const response = NextResponse.json({ user: result.data.user });
response.cookies.set("bkend_access_token", result.data.tokens.accessToken, {
httpOnly: true,
secure: true,
sameSite: "lax",
path: "/",
maxAge: 3600, // 1 hour
});
response.cookies.set("bkend_refresh_token", result.data.tokens.refreshToken, {
httpOnly: true,
secure: true,
sameSite: "lax",
path: "/",
maxAge: 2592000, // 30 days
});
return response;
}
Alternative: Memory + localStorage (SPA)
// lib/auth-store.ts
class AuthStore {
private accessToken: string | null = null;
setTokens(accessToken: string, refreshToken: string) {
this.accessToken = accessToken;
localStorage.setItem("bkend_refresh_token", refreshToken);
}
getAccessToken(): string | null {
return this.accessToken;
}
getRefreshToken(): string | null {
return localStorage.getItem("bkend_refresh_token");
}
clearTokens() {
this.accessToken = null;
localStorage.removeItem("bkend_refresh_token");
}
}
export const authStore = new AuthStore();
5.3 Auto-refresh Pattern (Next.js Middleware)
// middleware.ts
import { NextRequest, NextResponse } from "next/server";
const PUBLIC_PATHS = ["/login", "/signup", "/", "/auth/callback"];
export async function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
if (PUBLIC_PATHS.some((p) => pathname.startsWith(p))) {
return NextResponse.next();
}
const accessToken = request.cookies.get("bkend_access_token")?.value;
const refreshToken = request.cookies.get("bkend_refresh_token")?.value;
// No tokens at all -- redirect to login
if (!accessToken && !refreshToken) {
return NextResponse.redirect(new URL("/login", request.url));
}
// Access token exists -- proceed
if (accessToken) {
return NextResponse.next();
}
// Access token expired, refresh token exists -- auto-refresh
try {
const res = await fetch(
`${process.env.NEXT_PUBLIC_BKEND_API_URL}/auth/token/refresh`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Project-Id": process.env.NEXT_PUBLIC_BKEND_PROJECT_ID!,
"X-Environment": process.env.NEXT_PUBLIC_BKEND_ENVIRONMENT!,
},
body: JSON.stringify({ refreshToken }),
}
);
if (!res.ok) {
throw new Error("Refresh failed");
}
const data = await res.json();
const response = NextResponse.next();
response.cookies.set("bkend_access_token", data.data.accessToken, {
httpOnly: true,
secure: true,
sameSite: "lax",
path: "/",
maxAge: 3600,
});
if (data.data.refreshToken) {
response.cookies.set("bkend_refresh_token", data.data.refreshToken, {
httpOnly: true,
secure: true,
sameSite: "lax",
path: "/",
maxAge: 2592000,
});
}
return response;
} catch {
const response = NextResponse.redirect(new URL("/login", request.url));
response.cookies.delete("bkend_access_token");
response.cookies.delete("bkend_refresh_token");
return response;
}
}
export const config = {
matcher: ["/((?!_next/static|_next/image|favicon.ico|api).*)"],
};
6. Session Management
6.1 Revoke Current Session
Endpoint: POST /auth/session/revoke
Headers:
Authorization: Bearer <access-token>
Response (200 OK):
{
"success": true,
"data": {
"message": "Session revoked successfully"
}
}
6.2 Revoke All Sessions
Endpoint: POST /auth/session/revoke-all
Headers:
Authorization: Bearer <access-token>
Response (200 OK):
{
"success": true,
"data": {
"message": "All sessions revoked",
"revokedCount": 5
}
}
6.3 List Active Sessions
Endpoint: GET /auth/session/list
Headers:
Authorization: Bearer <access-token>
Response (200 OK):
{
"success": true,
"data": {
"sessions": [
{
"id": "ses_abc123",
"device": "Chrome on macOS",
"ip": "192.168.1.1",
"lastActiveAt": "2025-01-20T14:30:00.000Z",
"createdAt": "2025-01-15T09:00:00.000Z",
"current": true
},
{
"id": "ses_def456",
"device": "Safari on iPhone",
"ip": "10.0.0.1",
"lastActiveAt": "2025-01-19T10:00:00.000Z",
"createdAt": "2025-01-18T08:00:00.000Z",
"current": false
}
]
}
}
bkendFetch Example:
// Logout from current device
async function logout(token: string) {
await bkendFetch("/auth/session/revoke", {
method: "POST",
token,
});
// Clear local tokens
document.cookie = "bkend_access_token=; max-age=0; path=/";
document.cookie = "bkend_refresh_token=; max-age=0; path=/";
window.location.href = "/login";
}
// Logout from all devices
async function logoutAll(token: string) {
await bkendFetch("/auth/session/revoke-all", {
method: "POST",
token,
});
}
7. Password Management
7.1 Forgot Password
Endpoint: POST /auth/password/forgot
Request:
{
"email": "user@example.com",
"redirectUri": "https://myapp.com/reset-password"
}
Response (200 OK):
{
"success": true,
"data": {
"message": "Password reset email sent",
"expiresIn": 3600
}
}
The user receives an email with a link:
https://myapp.com/reset-password?token=pr_abc123...
7.2 Reset Password
Endpoint: POST /auth/password/reset
Request:
{
"token": "pr_abc123...",
"newPassword": "NewSecureP@ss456"
}
Response (200 OK):
{
"success": true,
"data": {
"message": "Password reset successfully"
}
}
7.3 Change Password (Authenticated)
Endpoint: PUT /auth/password/change
Headers:
Authorization: Bearer <access-token>
Request:
{
"currentPassword": "SecureP@ss123",
"newPassword": "NewSecureP@ss456"
}
Response (200 OK):
{
"success": true,
"data": {
"message": "Password changed successfully"
}
}
Error Responses:
| HTTP Status | Error Code | Description |
|---|---|---|
| 400 | INVALID_RESET_TOKEN |
Reset token is invalid |
| 410 | RESET_TOKEN_EXPIRED |
Reset token has expired (1 hour) |
| 401 | INCORRECT_PASSWORD |
Current password is incorrect |
| 400 | WEAK_PASSWORD |
New password does not meet requirements |
| 400 | SAME_PASSWORD |
New password must differ from current |
8. Multi-Factor Authentication (MFA)
8.1 Setup MFA
Endpoint: POST /auth/mfa/setup
Headers:
Authorization: Bearer <access-token>
Response (200 OK):
{
"success": true,
"data": {
"secret": "JBSWY3DPEHPK3PXP",
"qrCodeUrl": "otpauth://totp/bkend:user@example.com?secret=JBSWY3DPEHPK3PXP&issuer=bkend",
"backupCodes": [
"abc123def456",
"ghi789jkl012",
"mno345pqr678",
"stu901vwx234",
"yza567bcd890"
]
}
}
8.2 Verify MFA
Endpoint: POST /auth/mfa/verify
Request:
{
"code": "123456"
}
This endpoint is used both to complete MFA setup (first verification) and during login when MFA is enabled.
Response (200 OK):
{
"success": true,
"data": {
"message": "MFA verified successfully",
"mfaEnabled": true
}
}
When MFA is enabled, login responses include an mfaRequired flag:
{
"success": true,
"data": {
"mfaRequired": true,
"mfaToken": "mfa_temp_abc123..."
}
}
The client must then call /auth/mfa/verify with the mfaToken header and the TOTP code.
8.3 Disable MFA
Endpoint: POST /auth/mfa/disable
Headers:
Authorization: Bearer <access-token>
Request:
{
"code": "123456"
}
Response (200 OK):
{
"success": true,
"data": {
"message": "MFA disabled successfully",
"mfaEnabled": false
}
}
9. Account Linking
Link multiple auth methods to a single user account.
9.1 Link Provider
Endpoint: POST /auth/link/{provider}
Supported providers: google, github
Headers:
Authorization: Bearer <access-token>
The server redirects to the OAuth provider. After authorization, the provider is linked to the current user account.
Response (200 OK):
{
"success": true,
"data": {
"message": "Google account linked successfully",
"linkedProviders": ["email", "google"]
}
}
9.2 Unlink Provider
Endpoint: DELETE /auth/link/{provider}
Headers:
Authorization: Bearer <access-token>
Response (200 OK):
{
"success": true,
"data": {
"message": "Google account unlinked",
"linkedProviders": ["email"]
}
}
Error Responses:
| HTTP Status | Error Code | Description |
|---|---|---|
| 400 | PROVIDER_NOT_LINKED |
Provider is not linked to this account |
| 400 | LAST_AUTH_METHOD |
Cannot unlink the only remaining auth method |
| 409 | PROVIDER_ALREADY_LINKED |
Provider is already linked to another account |
10. Invitation System
Invite users to join your application with a predefined role.
10.1 Send Invitation
Endpoint: POST /auth/invite
Headers:
Authorization: Bearer <access-token>
Request:
{
"email": "newuser@example.com",
"role": "user",
"redirectUri": "https://myapp.com/invite/accept",
"metadata": {
"teamId": "team_abc123",
"welcomeMessage": "Welcome to our platform!"
}
}
Response (201 Created):
{
"success": true,
"data": {
"inviteId": "inv_abc123",
"email": "newuser@example.com",
"role": "user",
"status": "pending",
"expiresAt": "2025-01-22T09:00:00.000Z"
}
}
10.2 Accept Invitation
Endpoint: POST /auth/invite/accept
Request:
{
"token": "inv_token_abc123...",
"name": "New User",
"password": "SecureP@ss123"
}
Response (200 OK):
{
"success": true,
"data": {
"user": {
"id": "usr_xyz789",
"email": "newuser@example.com",
"name": "New User",
"role": "user"
},
"tokens": {
"accessToken": "eyJhbGciOiJIUzI1NiIs...",
"refreshToken": "rt_newuser123...",
"expiresIn": 3600
}
}
}
11. User Management
11.1 List Users (Admin)
Endpoint: GET /users
Query Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
page |
number | 1 | Page number |
limit |
number | 20 | Items per page (max 100) |
sort |
string | -createdAt |
Sort field (prefix - for descending) |
role |
string | -- | Filter by role |
search |
string | -- | Search by name or email |
Headers:
Authorization: Bearer <admin-access-token>
Response (200 OK):
{
"success": true,
"data": [
{
"id": "usr_abc123",
"email": "user@example.com",
"name": "Alice Kim",
"role": "user",
"emailVerified": true,
"createdAt": "2025-01-15T09:00:00.000Z"
}
],
"meta": {
"page": 1,
"limit": 20,
"total": 45
}
}
11.2 Get User by ID (Admin)
Endpoint: GET /users/:id
Response (200 OK):
{
"success": true,
"data": {
"id": "usr_abc123",
"email": "user@example.com",
"name": "Alice Kim",
"role": "user",
"emailVerified": true,
"linkedProviders": ["email", "google"],
"mfaEnabled": false,
"lastLoginAt": "2025-01-20T14:30:00.000Z",
"createdAt": "2025-01-15T09:00:00.000Z",
"updatedAt": "2025-01-20T14:30:00.000Z"
}
}
11.3 Update User (Admin)
Endpoint: PUT /users/:id
Request:
{
"name": "Alice Kim (Updated)",
"role": "admin",
"metadata": {
"department": "Engineering"
}
}
Response (200 OK):
{
"success": true,
"data": {
"id": "usr_abc123",
"name": "Alice Kim (Updated)",
"role": "admin",
"updatedAt": "2025-01-21T10:00:00.000Z"
}
}
11.4 Delete User (Admin)
Endpoint: DELETE /users/:id
Response (200 OK):
{
"success": true,
"data": {
"message": "User deleted successfully",
"deletedId": "usr_abc123"
}
}
11.5 Get Current User Profile
Endpoint: GET /users/me
Headers:
Authorization: Bearer <access-token>
Response (200 OK):
{
"success": true,
"data": {
"id": "usr_abc123",
"email": "user@example.com",
"name": "Alice Kim",
"role": "user",
"emailVerified": true,
"linkedProviders": ["email", "google"],
"mfaEnabled": true,
"metadata": {},
"createdAt": "2025-01-15T09:00:00.000Z",
"updatedAt": "2025-01-20T14:30:00.000Z"
}
}
11.6 Update Current User Profile
Endpoint: PUT /users/me
Headers:
Authorization: Bearer <access-token>
Request:
{
"name": "Alice K.",
"metadata": {
"avatar": "https://example.com/avatar.jpg",
"bio": "Full-stack developer"
}
}
Response (200 OK):
{
"success": true,
"data": {
"id": "usr_abc123",
"name": "Alice K.",
"metadata": {
"avatar": "https://example.com/avatar.jpg",
"bio": "Full-stack developer"
},
"updatedAt": "2025-01-21T11:00:00.000Z"
}
}
12. Auth Form Patterns (React / Next.js)
12.1 LoginForm Component
// components/auth/LoginForm.tsx
"use client";
import { useState, FormEvent } from "react";
import { useRouter } from "next/navigation";
import { useAuth } from "@/hooks/useAuth";
export function LoginForm() {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
const router = useRouter();
const { login } = useAuth();
async function handleSubmit(e: FormEvent) {
e.preventDefault();
setError(null);
setLoading(true);
try {
await login(email, password);
router.push("/dashboard");
} catch (err: any) {
setError(err.message || "Login failed");
} finally {
setLoading(false);
}
}
function handleSocialLogin(provider: "google" | "github") {
const baseUrl = process.env.NEXT_PUBLIC_BKEND_API_URL;
const projectId = process.env.NEXT_PUBLIC_BKEND_PROJECT_ID;
const env = process.env.NEXT_PUBLIC_BKEND_ENVIRONMENT;
const redirectUri = encodeURIComponent(
`${window.location.origin}/auth/callback`
);
window.location.href =
`${baseUrl}/auth/social/${provider}` +
`?redirectUri=${redirectUri}` +
`&projectId=${projectId}` +
`&environment=${env}`;
}
return (
<form
{error && <div className="error">{error}</div>}
<div>
<label htmlFor="email">Email</label>
<input
id="email"
type="email"
value={email}
=> setEmail(e.target.value)}
required
/>
</div>
<div>
<label htmlFor="password">Password</label>
<input
id="password"
type="password"
value={password}
=> setPassword(e.target.value)}
required
/>
</div>
<button type="submit" disabled={loading}>
{loading ? "Signing in..." : "Sign In"}
</button>
<div className="social-login">
<button type="button" => handleSocialLogin("google")}>
Continue with Google
</button>
<button type="button" => handleSocialLogin("github")}>
Continue with GitHub
</button>
</div>
<div className="links">
<a href="/forgot-password">Forgot password?</a>
<a href="/signup">Create account</a>
</div>
</form>
);
}
12.2 SignupForm Component
// components/auth/SignupForm.tsx
"use client";
import { useState, FormEvent } from "react";
import { useRouter } from "next/navigation";
import { useAuth } from "@/hooks/useAuth";
export function SignupForm() {
const [name, setName] = useState("");
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [confirmPassword, setConfirmPassword] = useState("");
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
const router = useRouter();
const { signup } = useAuth();
async function handleSubmit(e: FormEvent) {
e.preventDefault();
setError(null);
if (password !== confirmPassword) {
setError("Passwords do not match");
return;
}
setLoading(true);
try {
await signup(email, password, name);
router.push("/verify-email");
} catch (err: any) {
setError(err.message || "Signup failed");
} finally {
setLoading(false);
}
}
return (
<form
{error && <div className="error">{error}</div>}
<div>
<label htmlFor="name">Name</label>
<input
id="name"
type="text"
value={name}
=> setName(e.target.value)}
required
/>
</div>
<div>
<label htmlFor="email">Email</label>
<input
id="email"
type="email"
value={email}
=> setEmail(e.target.value)}
required
/>
</div>
<div>
<label htmlFor="password">Password</label>
<input
id="password"
type="password"
value={password}
=> setPassword(e.target.value)}
required
minLength={8}
/>
</div>
<div>
<label htmlFor="confirmPassword">Confirm Password</label>
<input
id="confirmPassword"
type="password"
value={confirmPassword}
=> setConfirmPassword(e.target.value)}
required
/>
</div>
<button type="submit" disabled={loading}>
{loading ? "Creating account..." : "Create Account"}
</button>
<div className="links">
<a href="/login">Already have an account? Sign in</a>
</div>
</form>
);
}
12.3 AuthProvider Context
// providers/AuthProvider.tsx
"use client";
import {
createContext,
useContext,
useState,
useEffect,
ReactNode,
} from "react";
import { bkendFetch } from "@/lib/bkend";
interface User {
id: string;
email: string;
name: string;
role: string;
emailVerified: boolean;
}
interface AuthContextType {
user: User | null;
loading: boolean;
login: (email: string, password: string) => Promise<void>;
signup: (email: string, password: string, name: string) => Promise<void>;
logout: () => Promise<void>;
refreshUser: () => Promise<void>;
}
const AuthContext = createContext<AuthContextType | undefined>(undefined);
export function AuthProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
refreshUser().finally(() => setLoading(false));
}, []);
async function refreshUser() {
try {
const result = await fetch("/api/auth/me");
if (result.ok) {
const data = await result.json();
setUser(data.user);
} else {
setUser(null);
}
} catch {
setUser(null);
}
}
async function login(email: string, password: string) {
const result = await fetch("/api/auth/login", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email, password }),
});
if (!result.ok) {
const error = await result.json();
throw new Error(error.error?.message || "Login failed");
}
const data = await result.json();
setUser(data.user);
}
async function signup(email: string, password: string, name: string) {
const result = await fetch("/api/auth/signup", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email, password, name }),
});
if (!result.ok) {
const error = await result.json();
throw new Error(error.error?.message || "Signup failed");
}
const data = await result.json();
setUser(data.user);
}
async function logout() {
await fetch("/api/auth/logout", { method: "POST" });
setUser(null);
window.location.href = "/login";
}
return (
<AuthContext.Provider
value={{ user, loading, login, signup, logout, refreshUser }}
>
{children}
</AuthContext.Provider>
);
}
export function useAuthContext() {
const context = useContext(AuthContext);
if (!context) {
throw new Error("useAuthContext must be used within an AuthProvider");
}
return context;
}
12.4 useAuth Hook
// hooks/useAuth.ts
"use client";
import { useAuthContext } from "@/providers/AuthProvider";
export function useAuth() {
return useAuthContext();
}
12.5 Protected Route HOC
// components/auth/ProtectedRoute.tsx
"use client";
import { useEffect } from "react";
import { useRouter } from "next/navigation";
import { useAuth } from "@/hooks/useAuth";
interface ProtectedRouteProps {
children: React.ReactNode;
requiredRole?: string;
fallback?: React.ReactNode;
}
export function ProtectedRoute({
children,
requiredRole,
fallback,
}: ProtectedRouteProps) {
const { user, loading } = useAuth();
const router = useRouter();
useEffect(() => {
if (!loading && !user) {
router.push("/login");
}
}, [user, loading, router]);
if (loading) {
return fallback || <div>Loading...</div>;
}
if (!user) {
return null;
}
if (requiredRole && user.role !== requiredRole && user.role !== "admin") {
return <div>Access denied. Required role: {requiredRole}</div>;
}
return <>{children}</>;
}
// Usage:
// <ProtectedRoute requiredRole="admin">
// <AdminDashboard />
// </ProtectedRoute>
12.6 RBAC Reference
bkend.ai supports four built-in roles:
| Role | Level | Description | Permissions |
|---|---|---|---|
admin |
4 | Full access | All CRUD, user management, settings |
user |
3 | Standard user | CRUD own data, read shared data |
self |
2 | Self-only | Read/write only own records |
guest |
1 | Read-only | Read public data only |
RLS (Row-Level Security) Policies:
Configure in Console > Project > Tables > [Table] > Security:
{
"read": {
"admin": "*",
"user": "*",
"self": { "createdBy": "{{userId}}" },
"guest": { "isPublic": true }
},
"create": {
"admin": "*",
"user": "*",
"self": "*",
"guest": false
},
"update": {
"admin": "*",
"user": { "createdBy": "{{userId}}" },
"self": { "createdBy": "{{userId}}" },
"guest": false
},
"delete": {
"admin": "*",
"user": false,
"self": { "createdBy": "{{userId}}" },
"guest": false
}
}
13. Error Codes Reference
Complete list of auth-related error codes:
| Error Code | HTTP Status | Description |
|---|---|---|
INVALID_EMAIL |
400 | Email format is invalid |
WEAK_PASSWORD |
400 | Password does not meet requirements |
MISSING_REQUIRED_FIELD |
400 | A required field is missing |
INVALID_REQUEST_BODY |
400 | Request body is malformed |
EMAIL_ALREADY_EXISTS |
409 | An account with this email already exists |
INVALID_CREDENTIALS |
401 | Email or password is incorrect |
ACCOUNT_DISABLED |
403 | User account has been disabled by admin |
ACCOUNT_LOCKED |
403 | Account locked due to too many failed attempts |
EMAIL_NOT_VERIFIED |
403 | Email address has not been verified |
INVALID_VERIFICATION_TOKEN |
400 | Email verification token is invalid |
VERIFICATION_TOKEN_EXPIRED |
410 | Email verification token has expired |
INVALID_MAGIC_LINK |
400 | Magic link token is invalid |
MAGIC_LINK_EXPIRED |
410 | Magic link token has expired (10 min) |
MAGIC_LINK_USED |
400 | Magic link has already been used |
SOCIAL_AUTH_FAILED |
400 | OAuth provider returned an error |
SOCIAL_EMAIL_NOT_FOUND |
400 | OAuth provider did not return an email |
SOCIAL_PROVIDER_DISABLED |
400 | Social login provider not enabled |
INVALID_ACCESS_TOKEN |
401 | Access token is invalid or malformed |
ACCESS_TOKEN_EXPIRED |
401 | Access token has expired |
INVALID_REFRESH_TOKEN |
401 | Refresh token is invalid |
REFRESH_TOKEN_EXPIRED |
401 | Refresh token has expired (30 days) |
REFRESH_TOKEN_REVOKED |
401 | Refresh token has been revoked |
INVALID_RESET_TOKEN |
400 | Password reset token is invalid |
RESET_TOKEN_EXPIRED |
410 | Password reset token has expired (1 hour) |
INCORRECT_PASSWORD |
401 | Current password is incorrect |
SAME_PASSWORD |
400 | New password must differ from current |
MFA_REQUIRED |
403 | Multi-factor authentication is required |
INVALID_MFA_CODE |
400 | MFA TOTP code is invalid |
MFA_ALREADY_ENABLED |
409 | MFA is already enabled for this account |
PROVIDER_NOT_LINKED |
400 | Auth provider is not linked to account |
PROVIDER_ALREADY_LINKED |
409 | Provider already linked to another account |
LAST_AUTH_METHOD |
400 | Cannot remove the last auth method |
INVITE_NOT_FOUND |
404 | Invitation not found |
INVITE_EXPIRED |
410 | Invitation has expired (7 days) |
INVITE_ALREADY_ACCEPTED |
400 | Invitation has already been accepted |
INSUFFICIENT_PERMISSIONS |
403 | User role lacks required permissions |
USER_NOT_FOUND |
404 | User with given ID does not exist |
TOO_MANY_ATTEMPTS |
429 | Rate limit exceeded, try again later |
SESSION_NOT_FOUND |
404 | Session does not exist |
SESSION_EXPIRED |
401 | Session has expired |
MISSING_PROJECT_HEADER |
400 | X-Project-Id header is missing |
MISSING_ENVIRONMENT_HEADER |
400 | X-Environment header is missing |
INVALID_PROJECT_ID |
400 | Project ID is invalid or not found |