-
Notifications
You must be signed in to change notification settings - Fork 0
API Document
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)
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/rtare 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.
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 |
/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
/authserver/authenticate reuses the existing row (without inserting a new row) when all three of the following conditions are met simultaneously:
- A row exists in the database with
state='valid'andissued_at + expires_in_days*86400000 > now() - The
user_idof that row matches the current login user - The
client_tokenof that row matches the request'sclientToken
Reuse behavior:
- The response
accessTokendirectly returns the old value - The existing row's
issued_atis reset tonow(), and the validity period is extended byexpires_in_days(default 15 days) - The
selectedProfileuses 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 totemporarily_invalid - Insert a new row with
state='valid'
A client kicked to temporarily_invalid can still call /authserver/refresh to regain control:
-
ValidateTokenForRefreshacceptsstate 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.
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
| 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 |
| 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."
}| Method | Path | Description |
|---|---|---|
| POST | /login |
User login |
| POST | /register |
User registration |
| GET | /logout |
User logout |
The token required for
/logoutis the Remember Token (from thetokenfield in thePOST /loginresponse; can be passed via request body / form / query parameter asremember_tokenin any format).
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
}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"
}| Method | Path | Description |
|---|---|---|
| POST | /user |
Get user information |
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
}
}| Method | Path | Description |
|---|---|---|
| POST | /email-verification |
Email verification (supports 3 sub-operations) |
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>"
}| 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/setuprequires a Remember Token (field nameremtoken) and returns the TOTP Secret (field nametotpkey). Pass thistotpkeyas thesecretparameter to/totpgento obtain a 6-digit TOTP Passcode. Submit this 6-digit passcode to/totp/verifyto complete verification.
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)
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>"
}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>"
}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"
}| 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_tokenin request body / form / query parameter).
Required Token: Remember Token
Request Body:
{
"remember_token": "<Remember Token>",
"username": "NewName"
}Required Token: Remember Token
Request Body:
{
"remember_token": "<Remember Token>",
"profile_id": "uuid-xxx",
"name": "NewPlayerName"
}| Method | Path | Description |
|---|---|---|
| POST | /generate-key |
Generate RSA key pair |
Generates a 2048-bit RSA key pair and saves it to the ./keys/ directory.
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_tokenin request body / form / query parameter).
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"
}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"
}
}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) |
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.
| Method | Path | Description | Required Token |
|---|---|---|---|
| GET | / |
Get server meta information | None |
| 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) |
| 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) |
| 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) |
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.
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.
| 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) |
| 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 |
| 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 |
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