bkend-auth
SkillCommunicationbkend.ai authentication and security expert skill. Covers email signup/login, social login (Google, GitHub), magic link, JWT tokens (Access 1h, Refresh 30d), session management, RBAC (admin/user/self/guest), RLS policies, password management, and account lifecycle.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the bkend-auth skill
What this skill tells your AI
The instructions your AI receives, as published by ww-w-ai/bkit-gemini in skills/bkend-auth/SKILL.md and read by ahel’s review.
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);
}
}
Shortened here. Read the whole file on GitHub.
Signals
- GitHub stars
- 65
- Forks
- 16
- Last commit
- May 2026
Advanced
- Catalog kind
- skill
- Gateway key
bkend-auth- Source
- github.com/ww-w-ai/bkit-gemini