bkend-auth

SkillCommunication

bkend.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.

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:

TokenTypeLifetimePurpose
Access TokenJWT1 hourAPI request authorization
Refresh TokenOpaque30 daysObtain new access tokens

Supported Authentication Methods

  1. Email/Password -- traditional signup and login
  2. Magic Link -- passwordless email-based login
  3. Social Login (OAuth) -- Google, GitHub
  4. 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 StatusError CodeDescription
400INVALID_EMAILEmail format is invalid
400WEAK_PASSWORDPassword does not meet requirements
409EMAIL_ALREADY_EXISTSAccount with this email already exists
400MISSING_REQUIRED_FIELDRequired 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 StatusError CodeDescription
401INVALID_CREDENTIALSEmail or password is incorrect
403ACCOUNT_DISABLEDAccount has been disabled
403ACCOUNT_LOCKEDToo many failed attempts (locked 30 min)
429TOO_MANY_ATTEMPTSRate 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 StatusError CodeDescription
400INVALID_MAGIC_LINKToken is invalid or malformed
410MAGIC_LINK_EXPIREDToken has expired (10 min lifetime)
400MAGIC_LINK_USEDToken has already been used

4. Social Login (OAuth)

4.1 Google OAuth

Console Configuration:

  1. Go to Console > Project > Settings > Auth > Social Login
  2. Enable Google provider
  3. Enter your Google Client ID and Client Secret
  4. 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:

  1. Go to Console > Project > Settings > Auth > Social Login
  2. Enable GitHub provider
  3. Enter your GitHub Client ID and Client Secret
  4. 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 StatusError CodeDescription
400SOCIAL_AUTH_FAILEDOAuth provider returned an error
400SOCIAL_EMAIL_NOT_FOUNDProvider did not return an email
409EMAIL_ALREADY_EXISTSEmail is linked to another auth method
400SOCIAL_PROVIDER_DISABLEDProvider 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 StatusError CodeDescription
401INVALID_REFRESH_TOKENRefresh token is invalid
401REFRESH_TOKEN_EXPIREDRefresh token has expired (30 day lifetime)
401REFRESH_TOKEN_REVOKEDRefresh 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 StatusError CodeDescription
400INVALID_RESET_TOKENReset token is invalid
410RESET_TOKEN_EXPIREDReset token has expired (1 hour)
401INCORRECT_PASSWORDCurrent password is incorrect
400WEAK_PASSWORDNew password does not meet requirements
400SAME_PASSWORDNew 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 StatusError CodeDescription
400PROVIDER_NOT_LINKEDProvider is not linked to this account
400LAST_AUTH_METHODCannot unlink the only remaining auth method
409PROVIDER_ALREADY_LINKEDProvider 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:

ParameterTypeDefaultDescription
pagenumber1Page number
limitnumber20Items per page (max 100)
sortstring-createdAtSort field (prefix - for descending)
rolestring--Filter by role
searchstring--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