Skip to content

API Document

Samuel Cheston edited this page Jun 28, 2026 · 1 revision

Project Overview

HRPAuth is a Minecraft authentication backend service built on Go + the Gin framework, implementing the Yggdrasil API specification with support for official login authentication.

Tech Stack:

  • Web Framework: Gin
  • Database: MySQL (GORM)
  • Cache: Redis
  • Authentication: Yggdrasil API (Authlib-Injector)

Token System Overview

This project involves multiple tokens with similar names but entirely different purposes. The table below provides a unified explanation.

Token Name Field Name (Request/Response) Length Purpose Issuing Endpoint (Response Field) Consuming Endpoint (Request Field)
Remember Token remember_token / remtoken / rt / login response token 32-byte random string Login session token for this site's business system, used to access the site's custom APIs POST /login (response token) GET /logout, POST /user, POST /change-username, POST /change-profile-name, POST /totp/setup, POST /totp/hasbeenenabled; after a successful POST /totp/verify, the response returns this token (rt field)
Yggdrasil Access Token accessToken Random string (utils.GenerateAccessToken) Access token for the Yggdrasil API, carried by the Minecraft client when joining a server POST /authserver/authenticate (response accessToken), POST /authserver/refresh (response new accessToken) POST /authserver/refresh, POST /authserver/validate, POST /authserver/invalidate, POST /sessionserver/session/minecraft/join, PUT/DELETE /api/user/profile/:uuid/:textureType (passed via Authorization: Bearer <accessToken> header)
Yggdrasil Client Token clientToken Random string (utils.GenerateClientToken, can be client-supplied) Client identifier for the Yggdrasil API, must be paired with the AccessToken POST /authserver/authenticate (optional in request / echoed in response) POST /authserver/authenticate, POST /authserver/refresh, POST /authserver/validate, POST /authserver/invalidate
Email Verification Code code 6-digit number Verifies user email ownership, stored in Redis, valid for 10 minutes POST /email-verification (action=send-verification-code, sent via email) POST /email-verification (action=verify-code)
TOTP Secret totpkey (response) / secret (/totpgen query parameter) 32-byte Base32 string TOTP seed key shared between the user and server, used to generate a 6-digit dynamic passcode POST /totp/setup (response totpkey) GET /totpgen?secret=<totpkey> (for testing, generates a dynamic passcode)
TOTP Passcode passcode 6-digit number One-time 6-digit dynamic passcode GET /totpgen?secret=<totpkey> (response in plaintext) POST /totp/verify
User Verification Token (DB field) verificationToken (only written to users.verification_token) 16-byte random string Generated during registration and stored in the database; not used as an input parameter in any endpoint in the current version (reserved field) POST /register (only stored in DB, not returned in response) No corresponding validation endpoint in current code

Important Distinctions:

  • Remember Token (site token) ≠ Yggdrasil Access Token (Minecraft client token). They are completely independent, managing user login states in different systems respectively.
  • remember_token / remtoken / rt are different field names for the same type of token with identical meaning.
  • The email verification code (code) and the TOTP passcode (passcode) are both 6-digit numbers but serve different purposes: the former verifies email ownership, the latter verifies two-step authentication.

Token State Machine & Lifecycle

The tokens.state field corresponds to the models.Token.State enum, with three possible values.

State Meaning Accepted By
valid Fully valid All endpoints
temporarily_invalid Kicked by another client; can only reclaim via /refresh; /validate and /join are all rejected Only /authserver/refresh
invalid Permanently expired (set by /invalidate, /signout, expiry check, or cleanup) None

State Transition Diagram

                                /authenticate (same clientToken)
   ┌─────────────────────────── valid ◄────────────────────────────┐
   │                                 ▲                               │
   │                                 │                               │
   │ /authenticate (different        │ /refresh succeeds             │
   │   clientToken)                  │                               │
   │ /refresh kicks other clients    │                               │
   ▼                                 │                               │
 temporarily_invalid ─────────── /refresh reclaim ───► new row valid │
                                                                         │
   ▼   ▼   ▼                                                             │
 invalid (set by /invalidate /signout /expiry check, cleanup physically  │
   deletes) ─────────────────────────────────────────────────────────────┘
                                                                         │
                                            (cleanup) ────► DELETE FROM tokens

Single-Client Idempotency (/authenticate)

/authserver/authenticate reuses the existing row (without inserting a new row) when all three of the following conditions are met simultaneously:

  1. A row exists in the database with state='valid' and issued_at + expires_in_days*86400000 > now()
  2. The user_id of that row matches the current login user
  3. The client_token of that row matches the request's clientToken

Reuse behavior:

  • The response accessToken directly returns the old value
  • The existing row's issued_at is reset to now(), and the validity period is extended by expires_in_days (default 15 days)
  • The selectedProfile uses the profile bound to the existing row

When conditions are not met (different clientToken / no valid row / previous row expired):

  • Transactional UPDATE: all rows for this user with state='valid' AND client_token != ? are set to temporarily_invalid
  • Insert a new row with state='valid'

Cross-Client Reclaim (/refresh is a Soft "Temporary Invalidation")

A client kicked to temporarily_invalid can still call /authserver/refresh to regain control:

  • ValidateTokenForRefresh accepts state IN ('valid', 'temporarily_invalid')
  • Old accessToken → state='invalid'
  • The current user's other clients with state='valid'temporarily_invalid
  • Issue a new accessToken → state='valid'

/authserver/validate and /sessionserver/session/minecraft/join only recognize valid, so the kicked client cannot join new servers but can "reclaim" its own session.

Background Cleanup Task

On startup, main.go triggers runOnce in controllers/token_cleanup_controller.go once and then every 1 hour. The logic is in services/auth_service.go::CleanupExpiredTokens:

  • DELETE rows with state='invalid'
  • DELETE rows where issued_at + expires_in_days*86400000 < now() (covers expired rows of both valid and temporarily_invalid)
  • The number of deleted rows is logged: [TokenCleanup] removed N expired/invalid tokens

Feature Modules

Module Functionality
User Authentication Registration, login, logout
Yggdrasil API Minecraft official authentication compatibility
TOTP Two-step verification
Email Verification Email verification code sending and validation
User Profile Username/character name modification
Texture Management Skin/cape upload and download
Key Generation RSA key pair generation

API Endpoints

1. Service Status

Method Path Description
GET /status Get service status

Response Example:

{
  "status": "online",
  "backend": {
    "name": "HRPAuth",
    "url": "https://auth.example.com",
    "version": "1.0.0",
    "go_version": "go1.26",
    "server_time": "2026-06-27 15:04:05"
  },
  "message": "HRPAuth Backend is running."
}

2. Authentication

Method Path Description
POST /login User login
POST /register User registration
GET /logout User logout

The token required for /logout is the Remember Token (from the token field in the POST /login response; can be passed via request body / form / query parameter as remember_token in any format).

POST /login

Required Token: None (login via email + password; a Remember Token is issued upon successful login)

Request Body:

{
  "email": "user@example.com",
  "password": "password123"
}

Response (note: the token field in the response is the Remember Token):

{
  "success": true,
  "message": "Login successful",
  "token": "<Remember Token, 32-byte random string>",
  "uid": 1,
  "totp": 0
}

POST /register

Required Token: None (no login required for registration)

Request Body:

{
  "email": "user@example.com",
  "username": "PlayerOne",
  "password": "password123"
}

Response (on successful registration, a verificationToken is written to the database, but the current version does not return this value in the response):

{
  "success": true,
  "uid": 1,
  "message": "Register successful"
}

3. User Information

Method Path Description
POST /user Get user information

POST /user

Required Token: Remember Token (from the token field in the POST /login response; can be passed via request body / form / query parameter as remember_token in any format)

Request Body:

{
  "remember_token": "<Remember Token>",
  "uid": "1",
  "email": "user@example.com"
}

Response:

{
  "success": true,
  "message": "User information retrieved successfully",
  "data": {
    "uid": 1,
    "email": "user@example.com",
    "username": "PlayerOne",
    "avatar": "",
    "verified": true
  }
}

4. Email Verification

Method Path Description
POST /email-verification Email verification (supports 3 sub-operations)

POST /email-verification

Token Requirements by Sub-Operation:

action Required Token Description
send-test-email None Send a test email directly, only requires to/subject/message
send-verification-code None Server generates a 6-digit verification code, stores it in Redis (valid for 10 minutes), and emails it to the user
verify-code Email Verification Code (6-digit number) The 6-digit code delivered by send-verification-code via email, submitted via the code field in the request body

Sub-Operations:

action Description
send-test-email Send a test email
send-verification-code Send a verification code
verify-code Verify the code

Send Verification Code Request:

{
  "action": "send-verification-code",
  "email": "user@example.com"
}

Verify Code Request (note: requires the code field carrying the Email Verification Code):

{
  "action": "verify-code",
  "email": "user@example.com",
  "code": "<6-digit verification code from the send-verification-code email>"
}

5. TOTP Two-Step Verification

Method Path Description
GET /totpgen Generate TOTP verification code
POST /totp/setup Set up TOTP
POST /totp/verify Verify TOTP
POST /totp/hasbeenenabled Check if the user has enabled TOTP

TOTP Token Flow: /totp/setup requires a Remember Token (field name remtoken) and returns the TOTP Secret (field name totpkey). Pass this totpkey as the secret parameter to /totpgen to obtain a 6-digit TOTP Passcode. Submit this 6-digit passcode to /totp/verify to complete verification.

GET /totpgen?secret=xxx

Required Token: TOTP Secret (from the totpkey field in the POST /totp/setup response; passed via query parameter secret) Returns a 6-digit TOTP Passcode as text (for testing/frontend display)

POST /totp/setup

Required Token: Remember Token (submitted via the remtoken field in the request body; validated jointly with the email field)

Request Body:

{
  "email": "user@example.com",
  "remtoken": "<Remember Token>"
}

Response (the totpkey field in the response is the TOTP Secret):

{
  "success": true,
  "totpkey": "<TOTP Secret, 32-byte Base32 string>"
}

POST /totp/verify

Required Token: TOTP Passcode (submitted via the passcode field in the request body; 6-digit number)

Request Body:

{
  "email": "user@example.com",
  "passcode": "<6-digit TOTP dynamic passcode>"
}

Response (on successful verification, the rt field in the response is the user's Remember Token; if the user did not previously have a token, the server will issue a new one and store it in the database):

{
  "success": true,
  "email": "user@example.com",
  "rt": "<Remember Token>"
}

POST /totp/hasbeenenabled

Required Token: Remember Token (submitted via the rt field in the request body; validated jointly with the uid field against users.remember_token)

Checks whether the specified user has enabled TOTP two-step verification.

Request Body:

{
  "uid": "1",
  "rt": "<Remember Token>"
}

Parameter Description:

Parameter Type Required Description
uid string Yes User UID
rt string Yes Remember Token (compared against the users.remember_token field)

Success Response:

{
  "success": true,
  "enabled": 1
}

Response Field Description:

Field Type Description
success bool Whether the request was successful
enabled int 1 = TOTP enabled (users.totp column is not null); 0 = TOTP not enabled (users.totp column is null)

Failure Response:

{
  "success": false,
  "message": "Invalid uid or rt"
}

6. User Profile Management

Method Path Description
POST /change-username Change username
POST /change-profile-name Change Minecraft character name

Both endpoints require the Remember Token (passed via remember_token in request body / form / query parameter).

POST /change-username

Required Token: Remember Token

Request Body:

{
  "remember_token": "<Remember Token>",
  "username": "NewName"
}

POST /change-profile-name

Required Token: Remember Token

Request Body:

{
  "remember_token": "<Remember Token>",
  "profile_id": "uuid-xxx",
  "name": "NewPlayerName"
}

7. Key Generation

Method Path Description
POST /generate-key Generate RSA key pair

POST /generate-key

Generates a 2048-bit RSA key pair and saves it to the ./keys/ directory.


8. HRPAuth Texture Management

Texture management endpoints for this site's business system, independent of the Yggdrasil API, using Remember Token authentication.

Method Path Description
POST /texture/upload Upload skin/cape
POST /texture/delete Delete skin/cape
POST /texture/get Get user texture information

All three endpoints require the Remember Token (passed via remember_token in request body / form / query parameter).

POST /texture/upload

Required Token: Remember Token

Request Body (multipart/form-data):

Parameter Type Required Description
remember_token string Yes User login token
profile_id string No Character ID; defaults to the user's first character
texture_type string Yes skin (skin) or cape (cape)
model string No Skin model: default (default) or slim (slim); only applicable for skins
file file Yes PNG format texture file

Request Example (curl):

curl -X POST http://localhost:8080/texture/upload \
  -F "remember_token=<Remember Token>" \
  -F "texture_type=skin" \
  -F "model=slim" \
  -F "file=@skin.png"

Success Response:

{
  "success": true,
  "message": "Texture uploaded successfully",
  "data": {
    "profile_id": "uuid-xxx",
    "texture_type": "skin"
  }
}

Failure Response:

{
  "success": false,
  "message": "Invalid texture type, must be skin or cape"
}

POST /texture/delete

Required Token: Remember Token

Request Body:

{
  "remember_token": "<Remember Token>",
  "profile_id": "uuid-xxx",
  "texture_type": "skin"
}

Parameter Description:

Parameter Type Required Description
remember_token string Yes User login token
profile_id string No Character ID; defaults to the user's first character
texture_type string Yes skin (skin) or cape (cape)

Success Response:

{
  "success": true,
  "message": "Texture deleted successfully",
  "data": {
    "profile_id": "uuid-xxx",
    "texture_type": "skin"
  }
}

POST /texture/get

Required Token: Remember Token

Request Body:

{
  "remember_token": "<Remember Token>",
  "profile_id": "uuid-xxx"
}

Parameter Description:

Parameter Type Required Description
remember_token string Yes User login token
profile_id string No Character ID; defaults to the user's first character

Success Response:

{
  "success": true,
  "message": "Texture information retrieved successfully",
  "data": {
    "profile_id": "uuid-xxx",
    "textures": [
      {
        "texture_type": "skin",
        "url": "https://auth.example.com/textures/abc123...",
        "model": "slim"
      },
      {
        "texture_type": "cape",
        "url": "https://auth.example.com/textures/def456..."
      }
    ]
  }
}

Response Field Description:

Field Type Description
profile_id string Character ID
textures array Texture list
textures[].texture_type string skin or cape
textures[].url string Texture file download URL
textures[].model string Skin model (only present for skins)

9. Yggdrasil API

The Yggdrasil API is the core interface for Minecraft official authentication. Completely independent from this site's business system (Remember Token), it uses the Yggdrasil Access Token + Yggdrasil Client Token system.

Meta Information

Method Path Description Required Token
GET / Get server meta information None

Authentication Server (authserver)

Method Path Description Required Token
POST /authserver/authenticate User authentication (issues AccessToken + ClientToken). Idempotent for the same clientToken: reuses the existing row and refreshes issued_at; Mutual kick for different clientToken: other clients' valid tokens → temporarily_invalid None (login via username + password; on success, both a Yggdrasil Access Token and Yggdrasil Client Token are issued)
POST /authserver/refresh Refresh token. Accepts state IN ('valid', 'temporarily_invalid'), allowing a kicked client to reclaim the session via /refresh; old row → invalid, new row → valid, and kicks other clients' valid rows to temporarily_invalid Yggdrasil Access Token (accessToken) + Yggdrasil Client Token (clientToken); returns a new AccessToken
POST /authserver/validate Validate token Yggdrasil Access Token (accessToken) + Yggdrasil Client Token (clientToken)
POST /authserver/invalidate Invalidate token Yggdrasil Access Token (accessToken) + Yggdrasil Client Token (clientToken)
POST /authserver/signout Account logout (revokes all tokens for the user) None (via username + password)

Session Server (sessionserver)

Method Path Description Required Token
POST /sessionserver/session/minecraft/join Join server session (called by the client carrying the AccessToken) Yggdrasil Access Token (request body accessToken field)
GET /sessionserver/session/minecraft/hasJoined Check if a player is on the server None (query parameters username / serverId / ip only)
GET /sessionserver/session/minecraft/profile/:uuid Query player profile None (public endpoint)

Profile/Texture API

Method Path Description Required Token
POST /api/profiles/minecraft Batch query player profiles None (public endpoint)
PUT /api/user/profile/:uuid/:textureType Upload texture Yggdrasil Access Token (passed via Authorization: Bearer <accessToken> header)
DELETE /api/user/profile/:uuid/:textureType Delete texture Yggdrasil Access Token (passed via Authorization: Bearer <accessToken> header)
GET /textures/:hash Download texture file None (public endpoint)

/authserver/authenticate Idempotency & Kicking Details

When logging in with the same clientToken a second time, no database write occurs:

T0  Client A logs in with clientToken=C-A → inserts row#1 {access=tok1, client=C-A, state=valid}
T1  Client A restarts and logs in again with clientToken=C-A
    → GetValidTokenByClientToken(U, C-A) hits row#1
    → Response accessToken=tok1 (not newly generated)
    → UPDATE row#1 SET issued_at=now() WHERE access_token=tok1
    → No new rows inserted in the database

When logging in with a different clientToken, mutual kicking occurs:

T2  Client B logs in with clientToken=C-B (row#1 is still valid)
    → GetValidTokenByClientToken(U, C-B) returns nil
    → UPDATE tokens SET state='temporarily_invalid'
        WHERE user_id=U AND client_token != C-B AND state='valid'   ← row#1 gets kicked
    → INSERT row#2 {access=tok2, client=C-B, state=valid}
    → Response accessToken=tok2

After this, A will be rejected by /validate and /join (temporarily_invalid), but A can call /refresh to reclaim — see the next section.

/authserver/refresh Reclaim & Kicking Details

Continuing the example above, A calls /authserver/refresh with the old accessToken=tok1:

T3  POST /authserver/refresh {accessToken: tok1, clientToken: C-A}
    → ValidateTokenForRefresh(tok1, C-A) hits row#1 (state=temporarily_invalid still passes)
    → InvalidateToken(tok1)                  → row#1 state=invalid
    → MarkOtherClientTokensTemporarilyInvalid(U, C-A)
        → row#2 (client=C-B, state=valid)   → state=temporarily_invalid
    → INSERT row#3 {access=tok3, client=C-A, state=valid}
    → Response accessToken=tok3

B will now also be rejected by /validate and /join, but retains the ability to reclaim via /refresh.

/authserver/validate & /join Hard Rejection of temporarily_invalid

Endpoint Accepted state Behavior for temporarily_invalid
POST /authserver/validate valid 403 ForbiddenOperationException
POST /sessionserver/session/minecraft/join valid 403 ForbiddenOperationException
POST /authserver/refresh valid, temporarily_invalid Success, triggers the "reclaim" process
POST /authserver/invalidate valid 403 (a kicked token cannot be invalidated)
POST /authserver/signout Via username/password, independent of token state Revokes all tokens for the user (including valid / temporarily_invalid / invalid)

Data Models

Model Table Name Description
User users User information
Profile profiles Minecraft character profiles
ProfileProperty profile_properties Character properties (e.g., textures)
UserProperty user_properties User properties
Token tokens Authentication tokens
Session sessions Server sessions

Feature Flags

Configuration Description
non_email_login Allow non-email login (username login)
legacy_skin_api Enable legacy skin API
no_mojang_namespace Do not use Mojang namespace
enable_mojang_anti_features Enable Mojang anti-cheat features
enable_profile_key Enable profile key
username_check Enable username check

Configuration Structure

version: "2"
site:
  name: "HRPAuth"
  implementation: "HRPAuth"
  version: "1.0.0"
server:
  port: ":8080"
  cors_origin: "*"
database:
  host: "localhost"
  db_name: "hrpauth"
  user: "root"
  password: ""
  charset: "utf8mb4"
redis:
  host: "localhost"
  port: 6379
  password: ""
  db: 0
smtp:
  host: "smtp.example.com"
  port: 587
  username: ""
  password: ""
  encryption: "tls"
  from_email: "noreply@example.com"
  from_name: "HRPAuth"
yggdrasil:
  server:
    name: "My Server"
    implementation: "HRPAuth"
    version: "1.0.0"
    skin_domains:
      - "example.com"
  security:
    token_expiry_days: 15
    session_expiry_seconds: 3600
    password_cost: 10
    rate_limit_max_attempts: 10
    rate_limit_window_sec: 600
  feature_flags:
    non_email_login: false
    legacy_skin_api: false
    no_mojang_namespace: false
    enable_mojang_anti_features: false
    enable_profile_key: false
    username_check: true