This document is based on the current implementation and covers the full behavior, request flows, and data structures of all 27 routes defined in app/http/router/internal/admin/auth/auth.go.
app/http/router/handler.go: registers/{apiPrefix}app/http/router/internal/handler.go: registers/internalapp/http/router/internal/admin/handler.go: registers/admin(SaveOperationRecordis mounted on/admin/system)app/http/router/internal/admin/auth/handler.go: registers/auth
Here apiPrefix refers to system.route_prefix; the default value is dudu-admin-api.
The full business route prefix for the auth module is /{apiPrefix}/internal/admin/auth.
| Function | Method | Path | Protected by CheckAdminAuth |
|---|---|---|---|
| Get OAuth login URL | GET | /oauth/url |
No |
| Exchange login token | POST | /token |
No |
| Get Passkey login options | POST | /passkey/login/options |
No |
| Finish Passkey login | POST | /passkey/login/finish |
No |
| Local account re-authentication | POST | /reauth |
No |
| Get sensitive-operation verification methods | GET | /reauth/methods |
Yes |
| Verify sensitive operation with password | POST | /reauth/password |
Yes |
| Verify sensitive operation with TOTP | POST | /reauth/totp |
Yes |
| Get Passkey verification options for sensitive operations | POST | /reauth/passkey/options |
Yes |
| Finish Passkey verification for sensitive operations | POST | /reauth/passkey/finish |
Yes |
| Confirm third-party binding | POST | /oauth/bind/confirm |
No |
| List bound third-party accounts | GET | /oauth/accounts |
Yes |
| Unbind a third-party account | POST | /oauth/unbind |
Yes |
| Get Passkey registration options | POST | /passkey/register/options |
Yes |
| Finish Passkey registration | POST | /passkey/register/finish |
Yes |
| List current user's Passkeys | GET | /passkeys |
Yes |
| Delete current user's Passkey | DELETE | /passkey |
Yes |
| Get current user profile | GET | /profile |
Yes |
| Reset password with safe code | PUT | /password/reset |
No |
| Change password | PUT | /password |
Yes |
| Update profile | PUT | /profile |
Yes |
| Get user menus | GET | /menus |
Yes |
| Change login identifier | PUT | /identifier |
Yes |
| Enable TFA | PUT | /tfa/enable |
Yes |
| Disable TFA | PUT | /tfa/disable |
Yes |
| Get TOTP key | GET | /tfa/key |
Yes |
| Get TFA status | GET | /tfa/status |
Yes |
{
"code": 0,
"msg": "ok",
"trace": {
"id": "afeade2f5957-tcdtjo-gdmaj",
"desc": ""
},
"data": {}
}sequenceDiagram
participant C as Client
participant RL as AdminAuthRateLimit
participant MA as CheckAdminAuth
participant H as AdminAuth Handler
participant S as AuthService
participant R as Repository/Redis
C->>RL: Request /{apiPrefix}/internal/admin/auth/*
RL->>MA: Enter auth middleware (protected routes only)
MA->>S: VerifyToken + HasRole/HasPermission
MA-->>H: On success, inject user_id/user_name
H->>S: Business method
S->>R: DB/Redis/external OAuth
R-->>S: Return data
S-->>H: errCode + data
H-->>C: Standard JSON response
/oauth/url, /token, /passkey/login/options, /passkey/login/finish, /reauth, /oauth/bind/confirm, and /password/reset skip CheckAdminAuth.
These public routes are still subject to route-level rate limiting where configured.
From app/http/middleware/check_admin_auth.go:
- Read the header first:
Authorization: Bearer <token>. - If the header is missing, read the cookie:
admin-token=<token>. - Call
AuthService.VerifyTokenfor JWT validation. - On success, write
user_idanduser_nameinto the request context.
JWT uses HS256. Expiration comes from config.System.Admin.TokenExpireIn and falls back to the system config if the admin config is empty.
- Call
HasRole(userID, "super_admin")first. - If the user is
super_admin, allow the request immediately. - Otherwise compute the permission hash as
MD5(HTTP_METHOD + RequestPathWithoutQuery). - Call
HasPermission(userID, permissionHash).
| Code | Meaning | Trigger |
|---|---|---|
| 10001 | Not logged in / token missing | Middleware default when no token is found |
| 11005 | User authorization expired | jwt.ErrTokenExpired |
| 11007 | Malformed identifier structure | jwt.ErrTokenMalformed |
| 11009 | Invalid token signature | jwt.ErrTokenSignatureInvalid |
| 11006 | Authorization failed | Other token validation failures |
| 11008 | Insufficient permissions | Not super_admin and no permission for the route |
| Field | Type | Required | Description |
|---|---|---|---|
| identifier | string | No | Email/phone in password mode; safe_code in totp mode |
| grant_type | string | Yes | password / totp / feishu / wechat |
| state | string | No | OAuth callback state, used by feishu / wechat |
| credentials | string | Yes | Must be md5(plaintext password) in password mode; verification code in totp mode; OAuth code in OAuth mode |
4.2 AccessToken (successful data from POST /token, POST /passkey/login/finish, and POST /oauth/bind/confirm)
| Field | Type | Description |
|---|---|---|
| safe_code | string | Returned for NeedTfa(11028) or NeedResetPWD(11015) |
| token | string | JWT returned on successful login |
| expires_in | int64 | Token expiration in seconds |
| bind_ticket | string | Returned for NeedBindOAuth(11042) and used for binding confirmation |
| oauth_profile | object | Preview of the third-party profile returned for NeedBindOAuth(11042); currently supports user_name and avatar |
| syncable_fields | array | List of fields that can be synced for NeedBindOAuth(11042); currently may include user_name and avatar |
| Field | Type | Description |
|---|---|---|
| safe_code | string | Returned when the password step succeeds but TFA is still required, with action=high_risk_reauth |
| reauth_ticket | string | Returned after re-auth completes, used for high-risk operations such as binding, unbinding, or deleting Passkeys |
4.2.2 PasskeyOptionsResult (data from POST /passkey/login/options and POST /passkey/register/options)
| Field | Type | Description |
|---|---|---|
| challenge_id | string | Server-generated verification request identifier that must be sent back unchanged during the finish step |
| options | object | Outer WebAuthn options object; for login it can be passed directly to navigator.credentials.get(options), and for registration to navigator.credentials.create(options) |
Notes:
- The login flow returns a
CredentialRequestOptions-compatible structure whereoptions.publicKeyisPublicKeyCredentialRequestOptions. - The registration flow returns a
CredentialCreationOptions-compatible structure whereoptions.publicKeyisPublicKeyCredentialCreationOptions.
| Field | Type | Description |
|---|---|---|
| id | uint | Passkey primary key |
| display_name | string | Device display name |
| aaguid | string | Authenticator AAGUID as a hex string; may be empty |
| transports | array | Browser-reported transport methods such as internal and hybrid |
| last_used_at | string/null | Time of the most recent successful login |
| created_at | string | Creation time |
| Field | Type | Required | Description |
|---|---|---|---|
| id | string | Yes | Credential ID returned by the browser |
| raw_id | string | No | Original rawId; falls back to id if empty |
| type | string | No | Defaults to public-key |
| response | object | Yes | WebAuthn response structure returned by the browser |
Notes:
responsesupports common browser camelCase keys and also snake_case keys.- The server converts it to the WebAuthn protocol object before validation.
- Redis key:
admin:system:auth:safeCode:{code} - Value (JSON; fields vary by action):
{ "user_id": 1, "action": "tfa" } actioncan betfa,reset_password, orhigh_risk_reauthhigh_risk_reauthis used when the password step of local-accountReauthsucceeds but the user has TFA enabled; the server returns a one-timesafe_codefor the second-steptotp_codesubmission- TTL:
config.System.Admin.SafeCodeExpireIn - One-time consumption:
parseSafeCodedeletes it after reading
bind_ticket- key:
admin:system:auth:bindTicket:{code} - value:
provider/provider_tenant/provider_subject/oauth_profile - purpose: the third-party identity is verified, but not yet bound to a local account
- key:
reauth_ticket- key:
admin:system:auth:reauthTicket:{code} - value:
user_id + action=high_risk_reauth - purpose: confirm ownership of the local account before a high-risk operation
- key:
- Neither ticket type is consumed immediately when read.
bind_ticketis deleted after a successful bind and login issuance.reauth_ticketis deleted after binding, unbinding, or deleting the current user's Passkey succeeds.
- Redis key:
admin:system:auth:oauth:{state} - Value:
feishuorwechat - TTL: 180 seconds
- Deleted after the OAuth branch of
POST /tokenreads it successfully
- Redis key:
admin:system:auth:passkey:challenge:{challenge_id} - Value (JSON):
{ "action": "login", "challenge_id": "RANDOM_CHALLENGE_ID", "session_data": {}, "created_at": 1738838400 } actioncan beloginorregisteruser_idanddisplay_nameare written only during registration; accountless Passkey login does not pre-bind a user- TTL:
config.System.Admin.WebAuthn.ChallengeExpireIn, default180seconds POST /passkey/*/finishattempts to consume the verification request before returning on both success and failure; missing or expired requests return11050, and mismatched content returns11051
From system.Menu:
| Field | Type | Description |
|---|---|---|
| ID | uint | Menu ID (from gorm.Model) |
| name | string | Menu name |
| path | string | Menu path |
| permission_id | uint | Linked permission ID |
| parent_id | uint | Parent menu ID (0 means root) |
| icon | string | Icon |
| sort | int | Sort order |
| children | array | Child menus (recursive same structure) |
Note: the struct embeds gorm.Model, so the serialized payload may also contain CreatedAt, UpdatedAt, and DeletedAt.
GET /tfa/keyreturns:totp_key: a 32-character Base32 random stringqr_code:data:image/png;base64,...
- TOTP validation parameters: 6-digit code, 30-second step,
±1time-slice window.
| Field | Type | Description |
|---|---|---|
| id | uint | User ID |
| user_name | string | User name |
| avatar | string | Avatar URL |
| string | Masked email for display only; plaintext is not returned | |
| phone | string | Masked phone number for display only; plaintext is not returned |
| role_name | string | Role label, actual response values are 超级管理员, 管理员, or 普通用户 |
role_name resolution rules:
- Contains
super_admin:超级管理员 - Contains only
base:普通用户 - Contains additional roles on top of
basewithoutsuper_admin:管理员
| Field | Type | Description |
|---|---|---|
| rp_id | string | Relying Party ID. Required for Passkey support. Usually the frontend domain or its parent domain, without protocol, path, or port |
| rp_display_name | string | Relying Party display name. Defaults to Dudu Admin when empty |
| rp_origins | array | List of frontend origins allowed to initiate WebAuthn. Required for Passkey support. Every item must be a full origin |
| challenge_expire_in | int | Valid lifetime for a Passkey verification request in seconds. Default 180 |
| user_verification | string | User verification policy: required, preferred, or discouraged. Default preferred |
Notes:
- If
rp_idorrp_originsis missing, Passkey registration/login endpoints return500. - The current implementation uses discoverable Passkey login and requires a resident key during registration, so
rp_iddirectly determines the credential scope and later subdomain sharing boundary. challenge_expire_inaffects both the Redis TTL of the verification request and the WebAuthn registration/login timeout.- If
challenge_expire_in <= 0, it falls back to180. Ifuser_verificationis empty or invalid, it is treated aspreferred. - Example configuration can be found in
bin/configs/dev.json,bin/configs/local.json.default, andbin/configs/prod.json.
rp_id defines the Relying Party domain scope for a Passkey and is one of the most important WebAuthn settings.
- Use a domain or host name such as
localhost,127.0.0.1,admin.example.com, orexample.com. - Do not use values with protocol, port, or path such as
https://admin.example.com,admin.example.com:3000, or/login. - If the current page runs at
https://a.b.com, valid choices are usuallya.b.comorb.com; both may be valid, but they produce different scopes:a.b.com: narrower scope for the current subdomain onlyb.com: broader scope, suitable only when multiple subdomains must share Passkeys
- Once Passkeys have been issued in production, changing
rp_idusually requires users to re-register old credentials.
rp_display_name is the site display name shown by the browser or operating system when requesting a Passkey.
- Use a stable product name such as
Dudu AdminorAcme Admin. - The backend falls back to
Dudu Adminwhen the value is empty. - This field is not the name of the credential storage provider. It does not determine whether users see
iCloud Keychain,Apple Passwords,Google Password Manager, orBitwarden. - Labels such as
iCloud KeychainandBitwardenusually describe the credential manager or storage location chosen by the browser, OS, and password manager, not something inferred by this project.
rp_origins is the list of frontend origins allowed to initiate WebAuthn. These values must be frontend page origins, not backend API addresses.
- Every item must be a full origin including protocol, host, and optional port, for example:
http://localhost:3000http://127.0.0.1:3000https://admin.example.com
- Do not use values such as
/admin,admin.example.com,https://admin.example.com/login, orhttps://api.example.comif they do not match the actual frontend page origin. - If the admin frontend has multiple valid entry points, include all of them in the array.
rp_iddefines credential scope, whilerp_originsdefines which pages may start WebAuthn. Both must be correct.
challenge_expire_in defines how long a Passkey verification request remains valid, in seconds, from generation to expiration.
- Default value:
180 - The current implementation applies this value to:
- the TTL of the Passkey challenge in Redis
- the WebAuthn registration timeout
- the WebAuthn login timeout
- Recommended values:
- local development:
180 - normal production:
120to300
- local development:
- Values that are too small may expire before the user completes the system prompt; values that are too large expand the reuse window.
user_verification controls whether the authenticator requires local user verification, such as fingerprint, face recognition, or a device PIN.
required: local verification is mandatory; strongest security, stricter compatibilitypreferred: local verification is used when available; this is the project default and usually the most balanced choice for admin systemsdiscouraged: avoid requiring local verification; generally not recommended for high-sensitivity admin backends- If the value is empty or invalid, the current implementation falls back to
preferredwithout raising a dedicated config error
Local development:
"webauthn": {
"rp_id": "localhost",
"rp_display_name": "Dudu Admin",
"rp_origins": ["http://localhost:3000"],
"challenge_expire_in": 180,
"user_verification": "preferred"
}Single-domain production:
"webauthn": {
"rp_id": "admin.example.com",
"rp_display_name": "Dudu Admin",
"rp_origins": ["https://admin.example.com"],
"challenge_expire_in": 180,
"user_verification": "preferred"
}Shared Passkeys across multiple subdomains:
"webauthn": {
"rp_id": "example.com",
"rp_display_name": "Dudu Admin",
"rp_origins": [
"https://admin.example.com",
"https://console.example.com"
],
"challenge_expire_in": 180,
"user_verification": "preferred"
}The last setup is appropriate only when you explicitly want the same Passkey system shared across multiple subdomains. For a single admin domain, prefer a narrower rp_id.
/passkey/register/finishand/passkey/login/finishdo not log the raw operation payload.- Request logging redacts sensitive fields such as
challenge_id,credential,attestation,assertion,public_key,signature, anduser_handle.
- Validate that
identifierandcredentialsare not empty, otherwise return11000. - Validate that
identifieris an email or phone number, otherwise return11007. - Query the user by
identifier(email/phone) + status=1; return11002if not found. - Validate the password with bcrypt against the frontend-provided
credentials; return11001on failure. - If the user has TFA enabled, return
11028withsafe_code(action=tfa). - Otherwise generate JWT and return
token + expires_in.
- Parameter mapping:
identifier=safe_code,credentials=totp_code. - Validate
safe_codeand requireaction=tfa; return11030on failure. - Validate the user's TOTP; return
11029on failure. - Return JWT on success.
- Validate
safe_codeandpasswordare not empty (11034/11032). - Parse
safe_codeand requireaction=reset_password; otherwise return11030. - Update the password and store it with bcrypt. Under the current API contract,
passwordis the frontend-providedmd5(plaintext password).
- Generate a 16-character
stateand cache it for 180 seconds. - Build the redirect URL according to
type:feishuwechat(use the WeCom QR-code URL whenlogin_type=qrcode)
- Validate that
statematchesoauthType; otherwise return11041. - Exchange user identity through the corresponding provider API.
- Query
sys_user_identityby(provider, provider_tenant, provider_subject). - If the user is already bound, generate JWT immediately and update
last_login_aton that identity. - If the third-party identity is not bound, return
11042withbind_ticket. NeedBindOAuthalso returns a third-party profile preview and a list of syncable fields, allowing the frontend to let the user choose what to sync.
- Submit local
identifier + password. - Verify ownership of the local account with the password.
- If the user does not have TFA enabled, return a short-lived
reauth_ticketdirectly. - If the user has TFA enabled, return
11028withsafe_code(action=high_risk_reauth).
- Submit the
safe_codereturned in stage 1 and the currenttotp_code. - Validate
safe_codeand requireaction=high_risk_reauth; return11030on failure. - Query the user referenced by that
safe_codeand verify the user is still enabled. - Validate the user's TOTP; return
11029on failure. - Return a short-lived
reauth_ticketon success, used only for high-risk operations such as binding, unbinding, or deleting Passkeys.
- The frontend first calls
GET /reauth/methodsto get the current user's available methods, default method, and thepassword_requires_totpflag. - If the current user has a Passkey configured, the default method is
passkey, but the user may still switch manually to the password flow. - Passkey flow:
- Call
POST /reauth/passkey/optionsto getchallenge_id + options - The browser runs
navigator.credentials.get(options) - Call
POST /reauth/passkey/finishto complete validation and exchangereauth_ticket
- Call
- Password flow:
- Call
POST /reauth/passwordwith the current password - If the user does not have TFA enabled, it returns
reauth_ticketdirectly - If the user has TFA enabled, it returns
11028 + safe_code - Then call
POST /reauth/totpwithsafe_code + totp_codeto exchangereauth_ticket
- Call
- The following sensitive operations always require this validation and a valid
reauth_ticket:- Change password
- Change login identifier
- Manage third-party accounts
- Manage Passkeys
- Manage two-factor authentication
- Submit
bind_ticketandreauth_ticket. - Validate that both tickets are valid and that
reauth_ticket.action=high_risk_reauth. - Inside a transaction, verify that the target third-party identity is not already used by another user.
- Insert a record into
sys_user_identity. - If
sync_fieldsis provided, sync the selected fields intosys_user. - After a successful bind, query the target user and issue JWT immediately, returning
token + expires_in. - Consume both tickets after success.
- The frontend calls
GET /oauth/urlto get the third-party authorization URL, then redirects the user to complete authorization. - After the third-party callback, the frontend calls
POST /tokenwithcode + stateandgrant_type=feishu/wechat. - If the third-party identity is already bound to a local account,
POST /tokenreturns JWT directly and the flow ends. - If it is not bound,
POST /tokenreturns11042withbind_ticket + oauth_profile + syncable_fields. - The frontend asks the user for the local account and password to bind, then starts stage 1 with
POST /reauth. - If the local account does not have TFA enabled,
POST /reauthreturnsreauth_ticketdirectly. - If TFA is enabled,
POST /reauthreturns11028 + safe_code; the frontend then prompts for TOTP and completes stage 2 to exchangereauth_ticket. - The frontend calls
POST /oauth/bind/confirmwithbind_ticket + reauth_ticket; if the user selected profile sync, also submitsync_fields. - After a successful bind, the API returns JWT directly. The frontend should establish the login session immediately without calling another login endpoint.
- Future logins with the same third-party account will directly match the bound user and return JWT.
- The frontend calls
POST /passkey/login/optionsdirectly without submitting an account first. - The server returns a verification request identifier
challenge_idandoptions. - The frontend passes
optionsas the full argument tonavigator.credentials.get(options)and gets the browser assertion. - The frontend submits
challenge_id + credentialtoPOST /passkey/login/finish. - The server locates the user by
userHandleandcredential id, validates the verification request and WebAuthn signature, updatessign_count + last_used_at, and finally returnstoken + expires_in.
Additional notes:
- During login, the
responsefield should be forwarded exactly as returned by the browser. The server supports both camelCase and snake_case keys. POST /passkey/login/optionsandPOST /passkey/login/finishare subject to admin-login rate limiting.- The project does not keep backward compatibility for historical development credentials. After switching to accountless Passkey login, old development Passkeys must be deleted and re-registered.
GET /oauth/accounts: returns the list of third-party accounts bound to the current logged-in user, and each record containsid; the frontend must use this identifier when unbinding.POST /oauth/unbind: the request body must containidentity_id + reauth_ticket; the server unbinds only that specific identity and checks before deletion that the account still retains at least one available login method.
- The current logged-in user must first complete the unified sensitive-operation re-authentication flow, then call
POST /passkey/register/optionswithreauth_ticket. - The server returns a verification request identifier
challenge_idandoptions, and the frontend passesoptionsas the full argument tonavigator.credentials.create(options). - The frontend submits
challenge_id + credentialtoPOST /passkey/register/finish, which returns the createdPasskeyItemon success. GET /passkeyslists all Passkeys bound to the current user.DELETE /passkeyrequiresid + reauth_ticketto delete a single Passkey. If that deletion would leave the account with no available login method, the API returns11049.- If the credential already exists, the registration finish step returns
11053. If credential verification fails, it returns11054.
-
Method: GET
-
Path:
/{apiPrefix}/internal/admin/auth/oauth/url -
Auth: No
-
Query:
Field Type Required Description type string Yes feishu/wechatlogin_type string No wechatmay passqrcode -
Response:
data.url(string) -
Error Codes:
400,11040,500
- Method: POST
- Path:
/{apiPrefix}/internal/admin/auth/token - Auth: No
- Body:
AuthParam - Response:
AccessToken - Example when OAuth is not bound:
{ "code": 11042, "msg": "NeedBindOAuth", "trace": { "id": "afeade2f5957-tcdtjo-gdmaj", "desc": "" }, "data": { "bind_ticket": "BIND_TICKET", "oauth_profile": { "user_name": "张三", "avatar": "https://example.com/avatar.png" }, "syncable_fields": ["user_name", "avatar"] } } - Error codes by branch:
- Common:
400,11010 - password:
11000,11001,11002,11015,11028 - totp:
11029,11030,11002 - OAuth:
11041,500
- Common:
- Method: GET
- Path:
/{apiPrefix}/internal/admin/auth/profile - Auth: Yes
- Response:
{ "id": 1, "user_name": "admin", "avatar": "https://...", "email": "a***n@e*****e.com", "phone": "+86*******0000", "role_name": "管理员" } - Identifier field notes:
emailandphoneare display-only masked values. The response does not expose the plaintext identifiers stored in the database.- If the frontend initializes the "change login identifier" form directly from this response, it may send the same masked value back unchanged. The server treats that field as not modified.
role_namenotes:超级管理员: the user's roles includesuper_admin普通用户: the user's roles include onlybase管理员: the user has roles beyondbasebut notsuper_admin
- Error Codes: auth error codes from section 3
- Business boundary note: according to the service logic, the missing-user case should be
11002; however, the current handler forcescode=0whenerr == nil, so this case may currently returncode=0, data=null.
-
Method: PUT
-
Path:
/{apiPrefix}/internal/admin/auth/password/reset -
Auth: No
-
Body:
Field Type Required Description safe_code string Yes Safe code returned by POST /tokenpassword string Yes New password. The frontend must send md5(plaintext password), which the server stores with bcrypt -
Error Codes:
400,11032,11034,11030,11002,500
-
Method: PUT
-
Path:
/{apiPrefix}/internal/admin/auth/password -
Auth: Yes
-
Body:
Field Type Required Description reauth_ticket string Yes Ticket obtained from the unified sensitive-operation verification flow password string Yes New password. The frontend must send md5(new plaintext password), which the server stores with bcrypt -
Validation rules:
- The unified sensitive-operation verification flow must be completed first.
- When a Passkey exists, Passkey is the default preferred method, but the user may manually switch to the password flow.
- If there is no Passkey and TFA is enabled, the frontend must obtain
reauth_ticketthrough the two-steppassword -> TOTPflow.
-
Error Codes:
400,11032,11046,11047,11002,500+ auth error codes from section 3
-
Method: PUT
-
Path:
/{apiPrefix}/internal/admin/auth/profile -
Auth: Yes
-
Body:
Field Type Required Description user_name string No User name, subject to reserved-word rules avatar string No Avatar URL -
Reserved words:
admin/root/administrator/管理员/超级管理员/seakee/super_admin/superAdmin(any matching prefix or suffix is invalid) -
Error Codes:
400,11007,11002,500+ auth error codes from section 3
- Method: GET
- Path:
/{apiPrefix}/internal/admin/auth/menus - Auth: Yes
- Response:
data.items(see the menu tree structure in 4.6) - Permission logic:
super_admin: returns the full menu tree- Regular users: aggregate menus from role permissions, then automatically include parent menus before returning a tree
- Error Codes: the current controller maps service errors uniformly to
400and includes the error message; auth error codes from section 3 also apply
-
Method: PUT
-
Path:
/{apiPrefix}/internal/admin/auth/identifier -
Auth: Yes
-
Body:
Field Type Required Description reauth_ticket string Yes Ticket obtained from the unified sensitive-operation verification flow email string No New email, must be in a valid format phone string No New phone number, must be in a valid format -
Validation rules:
- The unified sensitive-operation verification flow must be completed first.
- Missing or invalid
reauth_ticketreturns11046 / 11047. - Empty
emailorphonemeans that identifier is not being submitted; if both are empty, return11014. - If the frontend directly sends back the masked value from
GET /profile, the server treats that field as unchanged, restores the original value of the current account, and then continues uniqueness and format validation. - Only newly entered email or phone values must satisfy format and uniqueness validation.
-
Error Codes:
400,11007,11014,11046,11047,11002,11013,500+ auth error codes from section 3
-
Method: POST
-
Path:
/{apiPrefix}/internal/admin/auth/reauth -
Auth: No
-
Body:
Field Type Required Description identifier string Required in stage 1 Local email or phone password string Required in stage 1 md5(current plaintext password)safe_code string Required in stage 2 Safe code returned in stage 1, must have action=high_risk_reauthtotp_code string Required in stage 2 Current user's TOTP code -
Call pattern:
- Stage 1: submit only
identifier + password - Stage 2: submit only
safe_code + totp_code
- Stage 1: submit only
-
Stage 1 request example:
{ "identifier": "admin@example.com", "password": "md5(current plaintext password)" } -
Response example when stage 1 still requires TFA:
{ "safe_code": "SAFE_CODE" } -
Stage 2 request example:
{ "safe_code": "SAFE_CODE", "totp_code": "123456" } -
Successful response:
{ "reauth_ticket": "REAUTH_TICKET" } -
Notes:
- If stage 1 returns
11028, it means the local account password has been verified, but stage 2 TOTP is still required. safe_codeis a one-time credential. It is consumed immediately after validation and cannot be reused.
- If stage 1 returns
-
Error Codes:
400,11000,11001,11002,11028,11030,11033,11029,500
- Method: GET
- Path:
/{apiPrefix}/internal/admin/auth/reauth/methods - Auth: Yes
- Successful response:
default_method:passkey/passwordavailable_methods: list of currently available methodspassword_requires_totp: whether the password flow still requires a TOTP steptotp_enabled: whether the current user has TFA enabledpasskey_count: number of registered Passkeys for the current user
-
Method: POST
-
Path:
/{apiPrefix}/internal/admin/auth/reauth/password -
Auth: Yes
-
Body:
Field Type Required Description password string Yes md5(current plaintext password) -
Behavior:
- If the user does not have TFA enabled, return
reauth_ticketdirectly - If the user has TFA enabled, return
11028 + safe_code
- If the user does not have TFA enabled, return
-
Method: POST
-
Path:
/{apiPrefix}/internal/admin/auth/reauth/totp -
Auth: Yes
-
Body:
Field Type Required Description safe_code string Yes Safe code returned by POST /reauth/passwordtotp_code string Yes Current user's TOTP code -
Successful response:
{ "reauth_ticket": "REAUTH_TICKET" }
- Method: POST
- Path:
/{apiPrefix}/internal/admin/auth/reauth/passkey/options - Auth: Yes
- Response:
PasskeyOptionsResult
-
Method: POST
-
Path:
/{apiPrefix}/internal/admin/auth/reauth/passkey/finish -
Auth: Yes
-
Body:
Field Type Required Description challenge_id string Yes Verification request identifier returned by POST /reauth/passkey/optionscredential object Yes Browser-returned PasskeyCredential -
Successful response:
{ "reauth_ticket": "REAUTH_TICKET" }
-
Method: POST
-
Path:
/{apiPrefix}/internal/admin/auth/oauth/bind/confirm -
Auth: No
-
Body:
Field Type Required Description bind_ticket string Yes Binding ticket returned by POST /tokenreauth_ticket string Yes Re-authentication ticket returned by POST /reauthsync_fields array No List of fields selected by the user for syncing; currently supports user_nameandavatar -
Request example:
{ "bind_ticket": "BIND_TICKET", "reauth_ticket": "REAUTH_TICKET", "sync_fields": ["user_name", "avatar"] } -
Successful response:
AccessToken{ "code": 0, "msg": "ok", "trace": { "id": "afeade2f5957-tcdtjo-gdmaj", "desc": "" }, "data": { "token": "JWT_TOKEN", "expires_in": 7200 } } -
Behavior notes:
- This endpoint is the final step of the "third-party login not yet bound" flow.
- After a successful bind, it issues the login session directly. The frontend does not need to call
POST /tokenagain. - If
sync_fieldsis selected, the returned JWT is generated from the latest synced user profile.
-
Error Codes:
400,11044,11045,11046,11047,11043,11002,500
-
Method: GET
-
Path:
/{apiPrefix}/internal/admin/auth/oauth/accounts -
Auth: Yes
-
Response:
data.list[]Field Type Description id uint Identity primary key, required when unbinding provider string Third-party provider such as feishu/wechatprovider_tenant string Third-party tenant identifier display_name string Display name on the third-party side avatar_url string Avatar URL on the third-party side bound_at string Bind time last_login_at string Most recent login time through that identity -
Error Codes:
11002,500+ auth error codes from section 3
-
Method: POST
-
Path:
/{apiPrefix}/internal/admin/auth/oauth/unbind -
Auth: Yes
-
Body:
Field Type Required Description identity_id uint Yes Identity primary key returned by GET /oauth/accountsreauth_ticket string Yes Re-authentication ticket returned by POST /reauth -
Request example:
{ "identity_id": 12, "reauth_ticket": "REAUTH_TICKET" } -
Behavior notes:
- The server unbinds precisely by
identity_id; it no longer deletes in batch byprovider/provider_tenant. - Unbinding physically deletes the corresponding
sys_user_identityrecord, so the same third-party identity can be bound again later. - If the account would be left with only zero available login methods, the API returns
11049.
- The server unbinds precisely by
-
Error Codes:
400,11046,11047,11048,11049,11002,500+ auth error codes from section 3
- Method: POST
- Path:
/{apiPrefix}/internal/admin/auth/passkey/login/options - Auth: No
- Response:
PasskeyOptionsResult - Notes:
data.optionsis the outer WebAuthn login options object and can be passed directly tonavigator.credentials.get(data.options).- No account identifier is required. The server generates the options needed for discoverable Passkey login.
- This endpoint is protected by admin-login rate limiting.
- Error Codes:
400,11056,500
-
Method: POST
-
Path:
/{apiPrefix}/internal/admin/auth/passkey/login/finish -
Auth: No
-
Body:
Field Type Required Description challenge_id string Yes Verification request identifier returned by POST /passkey/login/optionscredential object Yes Browser-returned PasskeyCredential -
Successful response:
AccessToken -
Error Codes:
400,11002,11050,11051,11052,11054,500
-
Method: POST
-
Path:
/{apiPrefix}/internal/admin/auth/passkey/register/options -
Auth: Yes
-
Body:
Field Type Required Description reauth_ticket string Yes Ticket obtained from the unified sensitive-operation verification flow display_name string No Custom device display name. When empty, the backend generates it from the user profile -
Response:
PasskeyOptionsResult -
Notes:
data.optionsis the outer WebAuthn registration options object and can be passed directly tonavigator.credentials.create(data.options).- This endpoint consumes
reauth_ticketafter the challenge is generated successfully.
-
Error Codes:
400,11002,11046,11047,11055,500+ auth error codes from section 3
-
Method: POST
-
Path:
/{apiPrefix}/internal/admin/auth/passkey/register/finish -
Auth: Yes
-
Body:
Field Type Required Description challenge_id string Yes Verification request identifier returned by POST /passkey/register/optionscredential object Yes Browser-returned PasskeyCredential -
Successful response:
PasskeyItem -
Error Codes:
400,11002,11050,11051,11053,11054,11055,500+ auth error codes from section 3
-
Method: GET
-
Path:
/{apiPrefix}/internal/admin/auth/passkeys -
Auth: Yes
-
Response:
data.list[]Field Type Description id uint Passkey primary key display_name string Device display name aaguid string Authenticator AAGUID, may be empty transports array Browser-reported transport methods last_used_at string/null Last usage time created_at string Creation time -
Error Codes:
11002,500+ auth error codes from section 3
-
Method: DELETE
-
Path:
/{apiPrefix}/internal/admin/auth/passkey -
Auth: Yes
-
Body:
Field Type Required Description id uint Yes Passkey primary key to delete reauth_ticket string Yes Re-authentication ticket returned by POST /reauth -
Behavior notes:
- Deletes precisely by current logged-in user +
id. - A high-risk re-authentication must be completed first and
reauth_ticketmust be submitted. - If the current account would be left with no available login method, the API returns
11049. - Missing or invalid
id/reauth_ticketis rejected directly at the handler layer with400.
- Deletes precisely by current logged-in user +
-
Error Codes:
400,11046,11047,11049,11052,500+ auth error codes from section 3
-
Method: PUT
-
Path:
/{apiPrefix}/internal/admin/auth/tfa/enable -
Auth: Yes
-
Body:
Field Type Required Description reauth_ticket string Yes Ticket obtained from the unified sensitive-operation verification flow totp_code string Yes Verification code generated from totp_keytotp_key string Yes Obtained from /tfa/key -
Error Codes:
400,11035,11033,11029,11046,11047,500+ auth error codes from section 3
-
Method: PUT
-
Path:
/{apiPrefix}/internal/admin/auth/tfa/disable -
Auth: Yes
-
Body:
Field Type Required Description reauth_ticket string Yes Ticket obtained from the unified sensitive-operation verification flow -
Error Codes:
400,11046,11047,11002,500+ auth error codes from section 3
-
Method: GET
-
Path:
/{apiPrefix}/internal/admin/auth/tfa/key -
Auth: Yes
-
Response:
Field Type Description totp_key string Newly generated Base32 secret, length 32 qr_code string Base64 data URL of the corresponding QR code -
Error Codes:
11002(user not found) + auth error codes from section 3
-
Method: GET
-
Path:
/{apiPrefix}/internal/admin/auth/tfa/status -
Auth: Yes
-
Response:
Field Type Description enable bool Whether the current user has TFA enabled -
Error Codes:
11002(user not found) + auth error codes from section 3
In the current implementation, password-related APIs still require the frontend to submit md5(plaintext password) first. The server stores that digest with bcrypt.
- For
POST /tokenwithgrant_type=password,credentialsmust bemd5(plaintext password). - For
PUT /password/reset,passwordmust bemd5(new plaintext password). - Sensitive security operations for logged-in users now all follow the "verify first, then submit
reauth_ticket" pattern:- Call
GET /reauth/methodsto get the default method. - Passkey flow:
POST /reauth/passkey/options->POST /reauth/passkey/finish. - Password flow:
POST /reauth/password, and if necessaryPOST /reauth/totp.
- Call
PUT /password,PUT /identifier,PUT /tfa/enable,PUT /tfa/disable,POST /passkey/register/options, andDELETE /passkey:- must carry
reauth_ticket PUT /passwordstill requirespassword=md5(new plaintext password)
- must carry
POST /reauth:- stage 1 sends
identifier + password=md5(current plaintext password) - if
11028is returned, stage 2 must switch tosafe_code + totp_code - do not resend
identifier/passwordin stage 2; complete the TOTP check throughsafe_code
- stage 1 sends
POST /reauth/password:passwordmust bemd5(current plaintext password)- if
11028is returned, continue withsafe_code + totp_codeonPOST /reauth/totp
POST /oauth/bind/confirm:- must carry
bind_ticket + reauth_ticket - if third-party profile fields should be synced during the first bind, send
sync_fields, currently supportinguser_nameandavatar - on success, the endpoint returns
token + expires_indirectly and the frontend should treat it as a successful login
- must carry
POST /passkey/login/options:- does not require an account identifier
- old development Passkeys must be deleted and re-registered to reliably support accountless login
POST /passkey/register/finish,POST /passkey/login/finish, andPOST /reauth/passkey/finish:credentialmust forward the raw browser WebAuthn resultrawId,clientDataJSON,attestationObject,authenticatorData,signature, anduserHandlesupport both camelCase and snake_case keys
POST /passkey/register/options:
- must be called only after completing the unified sensitive-operation verification flow and with a valid
reauth_ticket display_nameis optional- if omitted, the backend falls back to
user_nameor the current login identifier and auto-generatesxxx Passkey
DELETE /passkey:
- must first complete the unified sensitive-operation verification flow to obtain
reauth_ticket - the request body must contain
id + reauth_ticket
Example: change password after completing sensitive-operation verification through the password flow:
{
"reauth_ticket": "REAUTH_TICKET",
"password": "md5(new plaintext password)"
}Example: update login identifier after completing unified verification:
{
"reauth_ticket": "REAUTH_TICKET",
"email": "new_admin@example.com"
}