Skip to main content

Overview

Memos supports three authentication methods for API access:
  1. JWT Access Tokens (V2) - Short-lived tokens (15 minutes) for active sessions
  2. Personal Access Tokens (PAT) - Long-lived tokens for scripts and automation
  3. Refresh Tokens - Long-lived tokens (30 days) for session renewal
All authentication methods use the Authorization header with Bearer token format. See: server/auth/authenticator.go:17-166

Authentication Methods

JWT Access Tokens

Access tokens are short-lived JWT tokens used for authenticated API requests. They are stateless and validated by signature only. Characteristics:
  • Lifetime: 15 minutes
  • Validation: Stateless (signature verification only)
  • Storage: Store in memory only (never in localStorage)
  • Format: Standard JWT with HS256 signing
Token Structure: Header:
{
  "alg": "HS256",
  "kid": "v1",
  "typ": "JWT"
}
Claims:
{
  "type": "access",
  "iss": "memos",
  "aud": ["user.access-token"],
  "sub": "123",
  "iat": 1709251200,
  "exp": 1709252100,
  "username": "demo",
  "role": "USER",
  "status": "ACTIVE"
}
type
string
required
Token type, always "access"
iss
string
required
Issuer, always "memos"
aud
array
required
Audience, always ["user.access-token"]
sub
string
required
Subject - user ID as string
iat
number
required
Issued at timestamp (Unix epoch)
exp
number
required
Expiration timestamp (iat + 15 minutes)
username
string
required
Username for display purposes
role
string
required
User role: "USER", "ADMIN"
status
string
required
User status: "ACTIVE", "ARCHIVED"
Signature: HS256 signature using server secret. See: server/auth/token.go:133-160

Personal Access Tokens (PAT)

PATs are long-lived tokens designed for programmatic access, scripts, and third-party integrations. Characteristics:
  • Lifetime: Configurable (30 days to never expires)
  • Validation: Stateful (database lookup required)
  • Storage: Store securely (environment variables, secrets manager)
  • Format: memos_pat_ prefix + 32 random characters
  • Hash: SHA-256 hash stored in database
Token Format:
memos_pat_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6
Token Prefix:
PersonalAccessTokenPrefix = "memos_pat_"
Generation:
// Random 32-character string
randomStr := util.RandomString(32)
token := "memos_pat_" + randomStr

// SHA-256 hash for database storage
hash := sha256.Sum256([]byte(token))
tokenHash := hex.EncodeToString(hash[:])
See: server/auth/token.go:189-203

Refresh Tokens

Refresh tokens are long-lived tokens stored in HttpOnly cookies for session renewal. Characteristics:
  • Lifetime: 30 days
  • Validation: Stateful (database lookup for revocation)
  • Storage: HttpOnly cookie (memos_refresh)
  • Rotation: New refresh token generated on each refresh
  • Format: Standard JWT with HS256 signing
Token Claims:
{
  "type": "refresh",
  "tid": "550e8400-e29b-41d4-a716-446655440000",
  "iss": "memos",
  "aud": ["user.refresh-token"],
  "sub": "123",
  "iat": 1709251200,
  "exp": 1711843200
}
type
string
required
Token type, always "refresh"
tid
string
required
Token ID (UUID) for database lookup and revocation
Cookie Attributes:
memos_refresh=<token>; Path=/; HttpOnly; SameSite=Lax; Secure (HTTPS only); Expires=<30 days>
See: server/auth/token.go:162-187

Authentication Flow

Password Authentication

1. Sign In Request:
curl -X POST http://localhost:8081/api/v1/auth/signin \
  -H "Content-Type: application/json" \
  -d '{
    "passwordCredentials": {
      "username": "demo",
      "password": "secret"
    }
  }'
Response:
{
  "user": {
    "name": "users/123",
    "username": "demo",
    "role": "USER",
    "email": "[email protected]"
  },
  "accessToken": "eyJhbGciOiJIUzI1NiIsImtpZCI6InYxIiwidHlwIjoiSldUIn0.eyJ0eXBlIjoiYWNjZXNzIiwiaXNzIjoibWVtb3MiLCJhdWQiOlsidXNlci5hY2Nlc3MtdG9rZW4iXSwic3ViIjoiMTIzIiwiaWF0IjoxNzA5MjUxMjAwLCJleHAiOjE3MDkyNTIxMDAsInVzZXJuYW1lIjoiZGVtbyIsInJvbGUiOiJVU0VSIiwic3RhdHVzIjoiQUNUSVZFIn0.signature",
  "accessTokenExpiresAt": "2026-03-01T10:15:00Z"
}
Set-Cookie Header:
Set-Cookie: memos_refresh=<refresh_token>; Path=/; HttpOnly; SameSite=Lax; Expires=Sun, 30 Mar 2026 10:00:00 GMT
See: server/router/api/v1/auth_service.go:64-189 2. Authenticated Request:
curl http://localhost:8081/api/v1/memos \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsImtpZCI6InYxIiwidHlwIjoiSldUIn0..."
Authorization Header Format:
Authorization: Bearer <access_token>
See: server/router/api/v1/connect_interceptors.go:177-227

Token Refresh Flow

When the access token expires (after 15 minutes), refresh it without re-authenticating: Refresh Request:
curl -X POST http://localhost:8081/api/v1/auth/refresh \
  -H "Cookie: memos_refresh=<refresh_token>" \
  -c cookies.txt \
  -b cookies.txt
Response:
{
  "accessToken": "eyJhbGciOiJIUzI1NiIsImtpZCI6InYxIiwidHlwIjoiSldUIn0...",
  "expiresAt": "2026-03-01T10:30:00Z"
}
Token Rotation: Memos implements refresh token rotation for enhanced security:
  1. Client sends old refresh token
  2. Server validates old token
  3. Server generates new refresh token (fresh 30-day expiry)
  4. Server stores new token and removes old token
  5. Client receives new access token and new refresh token cookie
This provides:
  • Sliding window sessions - Active users stay logged in indefinitely
  • Better security - Stolen refresh tokens become invalid after legitimate refresh
See: server/router/api/v1/auth_service.go:272-357

Personal Access Token Flow

1. Create Personal Access Token:
curl -X POST http://localhost:8081/api/v1/users/123/personalAccessTokens \
  -H "Authorization: Bearer <access_token>" \
  -H "Content-Type: application/json" \
  -d '{
    "description": "GitHub Actions CI/CD",
    "expiresInDays": 90
  }'
Response:
{
  "personalAccessToken": {
    "name": "users/123/personalAccessTokens/550e8400-e29b-41d4-a716-446655440000",
    "description": "GitHub Actions CI/CD",
    "createdAt": "2026-03-01T10:00:00Z",
    "expiresAt": "2026-05-30T10:00:00Z"
  },
  "token": "memos_pat_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6"
}
The token value is only returned once upon creation. Store it securely immediately. It cannot be retrieved later.
See: server/router/api/v1/user_service.go (CreatePersonalAccessToken) 2. Use PAT for Authentication:
curl http://localhost:8081/api/v1/memos \
  -H "Authorization: Bearer memos_pat_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6"
Authentication Logic:
// 1. Extract token from Authorization header
token := ExtractBearerToken(authHeader)

// 2. Detect PAT by prefix
if strings.HasPrefix(token, "memos_pat_") {
    // 3. Hash token for database lookup
    tokenHash := sha256.Sum256([]byte(token))
    
    // 4. Query database by hash
    result := store.GetUserByPATHash(ctx, tokenHash)
    
    // 5. Check expiration
    if result.PAT.ExpiresAt != nil && result.PAT.ExpiresAt.Before(time.Now()) {
        return errors.New("PAT expired")
    }
    
    // 6. Update last_used_at (async)
    go store.UpdatePATLastUsed(ctx, user.ID, result.PAT.TokenId)
    
    return result.User
}
See: server/auth/authenticator.go:101-124 3. List Personal Access Tokens:
curl http://localhost:8081/api/v1/users/123/personalAccessTokens \
  -H "Authorization: Bearer <access_token>"
Response:
{
  "personalAccessTokens": [
    {
      "name": "users/123/personalAccessTokens/550e8400-e29b-41d4-a716-446655440000",
      "description": "GitHub Actions CI/CD",
      "createdAt": "2026-03-01T10:00:00Z",
      "expiresAt": "2026-05-30T10:00:00Z",
      "lastUsedAt": "2026-03-15T14:30:00Z"
    },
    {
      "name": "users/123/personalAccessTokens/660e8400-e29b-41d4-a716-446655440001",
      "description": "Mobile App",
      "createdAt": "2026-02-15T09:00:00Z",
      "lastUsedAt": "2026-03-01T08:45:00Z"
    }
  ],
  "totalSize": 2
}
4. Revoke Personal Access Token:
curl -X DELETE http://localhost:8081/api/v1/users/123/personalAccessTokens/550e8400-e29b-41d4-a716-446655440000 \
  -H "Authorization: Bearer <access_token>"
Response:
{}
The token is immediately revoked and subsequent API requests with it will fail with 401 Unauthenticated.

Authentication Priority

When multiple authentication methods are present, Memos authenticates in this order:
  1. Access Token (V2) - Check Authorization: Bearer <token> header
    • If prefix is NOT memos_pat_, validate as JWT access token
    • Stateless validation (signature check only)
  2. Personal Access Token - Check Authorization: Bearer memos_pat_* header
    • If prefix is memos_pat_, hash and lookup in database
    • Stateful validation (database query + expiration check)
  3. Refresh Token - Check Cookie: memos_refresh=<token>
    • Used only for /auth/refresh endpoint
    • Not used for general API authentication
See: server/auth/authenticator.go:136-165

Security Best Practices

Token Storage

DO:
  • Store in memory only (JavaScript variable)
  • Clear on logout
  • Implement automatic refresh before expiry
DON’T:
  • Never store in localStorage or sessionStorage
  • Never store in cookies (use refresh token cookie instead)
  • Never log tokens in console or analytics
DO:
  • Store in environment variables for scripts
  • Use secrets manager for production (AWS Secrets Manager, HashiCorp Vault)
  • Set appropriate expiration (30-90 days)
  • Rotate regularly
  • Use descriptive names (“GitHub Actions CI”, “Mobile App”)
DON’T:
  • Never commit to version control
  • Never share tokens between services
  • Never use the same token for multiple purposes
  • Never set “never expires” for automated systems
DO:
  • Rely on HttpOnly cookie (automatic handling)
  • Implement token rotation (built-in)
  • Clear on explicit logout
DON’T:
  • Never access cookie from JavaScript
  • Never transmit in URL parameters
  • Never store client-side beyond cookie

HTTPS Configuration

Always use HTTPS in production to protect tokens in transit. HTTP exposes tokens to network interception.
Secure Cookie (HTTPS):
Set-Cookie: memos_refresh=<token>; Path=/; HttpOnly; SameSite=Lax; Secure; Expires=...
Non-Secure Cookie (HTTP):
Set-Cookie: memos_refresh=<token>; Path=/; HttpOnly; SameSite=Lax; Expires=...
The Secure attribute is automatically added when the request origin is https://. See: server/router/api/v1/auth_service.go:369-401

Token Revocation

Refresh Tokens:
  • Revoked on logout via /api/v1/auth/signout
  • Automatically rotated on refresh
  • Stored in user_setting table with expiration
Personal Access Tokens:
  • Manually revoked via DELETE endpoint
  • Immediately invalidated (database removal)
  • Cannot be restored after revocation
Access Tokens:
  • Cannot be revoked (stateless)
  • Short 15-minute lifetime minimizes risk
  • To “revoke” access, sign out to remove refresh token

Examples

Complete Authentication Example (curl)

#!/bin/bash
BASE_URL="http://localhost:8081"
USERNAME="demo"
PASSWORD="secret"

# 1. Sign in and get access token
RESPONSE=$(curl -s -c cookies.txt -X POST "$BASE_URL/api/v1/auth/signin" \
  -H "Content-Type: application/json" \
  -d "{
    \"passwordCredentials\": {
      \"username\": \"$USERNAME\",
      \"password\": \"$PASSWORD\"
    }
  }")

ACCESS_TOKEN=$(echo $RESPONSE | jq -r '.accessToken')
echo "Access Token: $ACCESS_TOKEN"

# 2. Make authenticated request
curl -s "$BASE_URL/api/v1/memos" \
  -H "Authorization: Bearer $ACCESS_TOKEN" | jq

# 3. Refresh token (after 15 minutes)
curl -s -c cookies.txt -b cookies.txt -X POST "$BASE_URL/api/v1/auth/refresh" | jq

# 4. Sign out
curl -s -b cookies.txt -X POST "$BASE_URL/api/v1/auth/signout"

Complete PAT Example (curl)

#!/bin/bash
BASE_URL="http://localhost:8081"
USER_ID="123"

# 1. Create PAT (requires existing access token)
RESPONSE=$(curl -s -X POST "$BASE_URL/api/v1/users/$USER_ID/personalAccessTokens" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "description": "CI/CD Pipeline",
    "expiresInDays": 90
  }')

PAT=$(echo $RESPONSE | jq -r '.token')
echo "PAT: $PAT"
echo "Save this token securely - it won't be shown again!"

# 2. Use PAT for API requests (no expiry refresh needed)
curl -s "$BASE_URL/api/v1/memos" \
  -H "Authorization: Bearer $PAT" | jq

# 3. List all PATs
curl -s "$BASE_URL/api/v1/users/$USER_ID/personalAccessTokens" \
  -H "Authorization: Bearer $PAT" | jq

# 4. Revoke PAT when no longer needed
PAT_ID="550e8400-e29b-41d4-a716-446655440000"
curl -s -X DELETE "$BASE_URL/api/v1/users/$USER_ID/personalAccessTokens/$PAT_ID" \
  -H "Authorization: Bearer $PAT"

SSO Authentication Example

#!/bin/bash
BASE_URL="http://localhost:8081"
IDP_ID="1"
AUTH_CODE="<authorization_code_from_oauth_provider>"
REDIRECT_URI="http://localhost:3000/auth/callback"

# Sign in with SSO
curl -s -c cookies.txt -X POST "$BASE_URL/api/v1/auth/signin" \
  -H "Content-Type: application/json" \
  -d "{
    \"ssoCredentials\": {
      \"idpId\": $IDP_ID,
      \"code\": \"$AUTH_CODE\",
      \"redirectUri\": \"$REDIRECT_URI\"
    }
  }" | jq
See: server/router/api/v1/auth_service.go:91-170

Troubleshooting

”authentication required” (401)

Cause: Missing or invalid authentication token Solution:
# Verify Authorization header format
curl -v http://localhost:8081/api/v1/memos \
  -H "Authorization: Bearer <token>"

# Should see:
# > Authorization: Bearer eyJhbGci...

“invalid access token” (401)

Cause: Expired, malformed, or revoked token Solution:
# Decode JWT to check expiration
echo "<token>" | cut -d. -f2 | base64 -d | jq

# Check exp (expiration) timestamp
# If expired, refresh token:
curl -X POST http://localhost:8081/api/v1/auth/refresh \
  -b cookies.txt -c cookies.txt

“refresh token not found” (401)

Cause: Refresh token cookie not sent or expired Solution:
# Verify cookie exists
curl -v http://localhost:8081/api/v1/auth/refresh \
  -b cookies.txt -c cookies.txt

# Should see:
# > Cookie: memos_refresh=...

# If missing, sign in again

“PAT expired” (401)

Cause: Personal Access Token past expiration date Solution:
# List PATs to check expiration
curl http://localhost:8081/api/v1/users/123/personalAccessTokens \
  -H "Authorization: Bearer <access_token>" | jq

# Create new PAT if expired
curl -X POST http://localhost:8081/api/v1/users/123/personalAccessTokens \
  -H "Authorization: Bearer <access_token>" \
  -H "Content-Type: application/json" \
  -d '{"description": "New Token", "expiresInDays": 90}'

“permission denied” (403)

Cause: Authenticated but insufficient permissions Solution:
# Check user role
curl http://localhost:8081/api/v1/auth/me \
  -H "Authorization: Bearer <token>" | jq '.user.role'

# Admin operations require role: "ADMIN"
# User operations require matching user ID

Get Current User

Returns the authenticated user’s information. Similar to OIDC’s /userinfo endpoint.
GET /api/v1/auth/me
curl https://your-memos-instance.com/api/v1/auth/me \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

Request

No parameters needed. The access token in the Authorization header identifies the user.

Response

user
object
The authenticated user’s information

Example Response

{
  "user": {
    "name": "users/123",
    "username": "demo",
    "role": "USER",
    "email": "[email protected]",
    "displayName": "Demo User",
    "avatarUrl": "https://example.com/avatar.jpg",
    "description": "Just testing Memos",
    "state": "NORMAL",
    "createTime": "2024-01-01T00:00:00Z",
    "updateTime": "2026-03-01T10:00:00Z"
  }
}

Error Responses

401
error
Unauthenticated - Invalid or expired access token

Sign Out

Terminates the user’s authentication session by revoking the refresh token and clearing the authentication cookie.
POST /api/v1/auth/signout
curl -X POST https://your-memos-instance.com/api/v1/auth/signout \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -b cookies.txt

Request

No parameters needed. The endpoint will:
  1. Revoke the refresh token (from memos_refresh cookie)
  2. Clear the authentication cookie
  3. Invalidate the current session
The access token in the Authorization header is still technically valid until expiry (15 minutes), but the refresh token is immediately revoked, preventing session renewal.

Response

Returns an empty response on success.
{}
The memos_refresh cookie is cleared:
Set-Cookie: memos_refresh=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0

Example

#!/bin/bash
BASE_URL="https://your-memos-instance.com"

# Sign out and clear cookies
curl -X POST "$BASE_URL/api/v1/auth/signout" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -c cookies.txt \
  -b cookies.txt

# Subsequent refresh attempts will fail
curl -X POST "$BASE_URL/api/v1/auth/refresh" \
  -b cookies.txt
# Returns: 401 Unauthenticated - refresh token not found

Error Responses

401
error
Unauthenticated - User not authenticated

Refresh Access Token

Exchanges a valid refresh token for a new access token. The refresh token is read from the memos_refresh HttpOnly cookie.
POST /api/v1/auth/refresh
curl -X POST https://your-memos-instance.com/api/v1/auth/refresh \
  -b cookies.txt \
  -c cookies.txt

Request

No request body needed. The refresh token is automatically read from the memos_refresh cookie.
Memos implements refresh token rotation for security. Each refresh generates a new refresh token and invalidates the old one.

Response

access_token
string
The new short-lived access token (15-minute expiry)
expires_at
timestamp
When the access token expires. Clients should refresh before this time.

Example Response

{
  "accessToken": "eyJhbGciOiJIUzI1NiIsImtpZCI6InYxIiwidHlwIjoiSldUIn0...",
  "expiresAt": "2026-03-01T10:15:00Z"
}
A new memos_refresh cookie is set with a fresh 30-day expiry:
Set-Cookie: memos_refresh=<new_token>; Path=/; HttpOnly; SameSite=Lax; Expires=Sun, 30 Mar 2026 10:00:00 GMT

Token Rotation Flow

Example Usage

#!/bin/bash
BASE_URL="https://your-memos-instance.com"
COOKIES="cookies.txt"

# Initial sign in
curl -s -c $COOKIES -X POST "$BASE_URL/api/v1/auth/signin" \
  -H "Content-Type: application/json" \
  -d '{
    "passwordCredentials": {
      "username": "demo",
      "password": "secret"
    }
  }' | jq -r '.accessToken' > access_token.txt

ACCESS_TOKEN=$(cat access_token.txt)

# Use access token for 15 minutes...
curl "$BASE_URL/api/v1/memos" \
  -H "Authorization: Bearer $ACCESS_TOKEN"

# After 15 minutes, refresh token
REFRESH_RESPONSE=$(curl -s -b $COOKIES -c $COOKIES -X POST "$BASE_URL/api/v1/auth/refresh")
NEW_ACCESS_TOKEN=$(echo $REFRESH_RESPONSE | jq -r '.accessToken')

# Continue using new access token
curl "$BASE_URL/api/v1/memos" \
  -H "Authorization: Bearer $NEW_ACCESS_TOKEN"

Error Responses

401
error
Unauthenticated - Refresh token missing, expired, or revokedCommon causes:
  • No memos_refresh cookie sent
  • Refresh token expired (older than 30 days)
  • Token was revoked via sign out
  • Token was already used (rotation invalidated it)
If refresh fails with 401, the user must sign in again. There is no way to recover an expired or revoked refresh token.

Next Steps

API Reference

Explore detailed endpoint documentation

SDKs & Libraries

Use official SDKs for your programming language

Build docs developers (and LLMs) love