Skip to main content

Authentication Reference

The public security package surface for authentication: principals, JWT, the auth manager, challenge providers and token stores, signature auth, and login events. For the narrative guide — strategies, the built-in auth resource, and the login flow — see Authentication. The wire-level contract of the built-in auth endpoint — every action's request and response fields — is tabulated in RPC Resource: security/auth at the end of this page.

API groupPublic surface
principalsPrincipal, PrincipalType, NewUser, NewExternalApp, PrincipalSystem, PrincipalAnonymous, SetUserDetailsType, SetExternalAppDetailsType, IsReserved
JWTJWT, JWTConfig, JWTClaimsBuilder, JWTClaimsAccessor, NewJWT, GenerateSecret, token type constants, DefaultJWTAudience, DefaultJWTSecret, JWTIssuer
auth managerAuthentication, AuthTokens, Authenticator, AuthManager, TokenGenerator, UserLoader, ExternalAppLoader, ExternalAppConfig, PasswordDecryptor
challenge tokensChallengeProvider, ChallengeState, ChallengeTokenStore, NewMemoryChallengeTokenStore, NewRedisChallengeTokenStore, NewJWTChallengeTokenStore
OTP/challengesOTPEvaluator, OTPCodeSender, OTPCodeVerifier, OTPCodeStore, NewOTPChallengeProvider, NewDeliveredCodeSender, NewDeliveredCodeVerifier, NewDeliveredChallengeProvider, NewSMSChallengeProvider, NewEmailChallengeProvider
TOTP/password/departmentNewTOTPEvaluator, NewTOTPVerifier, NewTOTPChallengeProvider, WithTOTPDestination, NewPasswordChangeChallengeProvider, NewDepartmentSelectionChallengeProvider
signature authSignature, SignatureCredentials, SignatureResult, SignatureAlgorithm, NewSignature, WithAlgorithm, WithTimestampTolerance, WithNonceStore, NonceStore, NewMemoryNonceStore, NewRedisNonceStore
login eventsLoginEvent, LoginEventParams, NewLoginEvent, SubscribeLoginEvent

Bearer constants are AuthSchemeBearer and QueryKeyAccessToken. The token type constants are TokenTypeAccess, TokenTypeRefresh, and TokenTypeChallenge.

JWT and principals

NewJWT expects JWTConfig.Secret to be a hex-encoded key and defaults an empty audience to DefaultJWTAudience. Low-level NewJWT still falls back to the public DefaultJWTSecret when the secret is empty; the framework security module wraps this with a safer boot-time behavior that generates an ephemeral key and warns. Use GenerateSecret() to provision a private production key for vef.security.secret.

The built-in framework token generator issues access tokens with a fixed 30m TTL. vef.security.token_expires configures the refresh-token TTL instead (default 168h), and vef.security.refresh_not_before defaults to 15m. Access and refresh tokens generated together share the same jti.

JWT parsing accepts only HS256, requires issuer JWTIssuer (vef), validates audience, validates iat when present, requires exp, and applies a 10-second leeway. The compact claim keys are:

ClaimKey
JWT IDjti
subjectsub
issueriss
audienceaud
issued atiat
not beforenbf
expires atexp
token typetyp
rolesrls
detailsdet

JWT signing always uses HS256 (HMAC-SHA256 in JWT terminology). The signature authenticator in security/signature.go supports three algorithms:

AlgorithmConstantContext
HMAC-SHA256SignatureAlgHmacSHA256Signature authenticator (default)
HMAC-SHA512SignatureAlgHmacSHA512Signature authenticator
HMAC-SM3SignatureAlgHmacSM3Signature authenticator

The built-in access and refresh token generator stores the subject as id@name. JWTTokenAuthenticator rebuilds a user principal from that subject without a database lookup. JWTRefreshAuthenticator also expects id@name, but then reloads the user with UserLoader.LoadByID(...) using the id part.

JWTClaimsBuilder writes compact token claims with WithID, WithSubject, WithRoles, WithDetails, WithType, and WithClaim. JWTClaimsAccessor reads the same payload back with ID, Subject, Roles, Details, Type, and Claim. Use NewJWTClaimsBuilder() and NewJWTClaimsAccessor(...) to create those helpers directly.

PrincipalTypeUser, PrincipalTypeExternalApp, and PrincipalTypeSystem describe the supported principal kinds. SetUserDetailsType[T]() and SetExternalAppDetailsType[T]() configure process-global detail unmarshalling targets; call them during startup before serving requests.

Principal serializes as JSON type, id, name, roles, and details. SetUserDetailsType[T]() and SetExternalAppDetailsType[T]() require T to be a struct or struct pointer and panic with ErrUserDetailsNotStruct or ErrExternalAppDetailsNotStruct otherwise. They mutate package-level state and should be treated as startup-only configuration. Unknown principal types keep details as map[string]any; system principals deserialize with details set to nil. The built-in special principals are PrincipalSystem (type: "system", id system, name 系统) and PrincipalAnonymous (type: "user", id anonymous, name 匿名).

Principal.IsReserved() reports whether a principal claims a framework-internal identity. It returns true when the principal type is system, or when the principal ID equals orm.OperatorSystem ("system") or orm.OperatorCronJob ("cron_job"). PrincipalAnonymous is deliberately not reserved, because it represents the absence of an identity and is legitimately produced by the public auth strategy. The framework enforces the reserved-identity invariant at the authentication boundary, token issuance, and the challenge flow; see Authentication: Reserved Identities.

Challenge providers

Built-in challenge type constants include:

  • ChallengeTypeTOTP
  • ChallengeTypeSMS
  • ChallengeTypeEmail
  • ChallengeTypePasswordChange
  • ChallengeTypeDepartmentSelection

Their wire values and default orders are:

ConstantWire valueDefault order
ChallengeTypeTOTPtotp100
ChallengeTypeSMSsms_otp200
ChallengeTypeEmailemail_otp300
ChallengeTypePasswordChangepassword_change400
ChallengeTypeDepartmentSelectiondepartment_selection500

ChallengeTokenStore.Generate(ctx, principal, username, pending, resolved) and Parse(ctx, token) carry the state between login and resolve_challenge (username is the original login identifier the applicant supplied at the first step, preserved across challenge steps for audit events). The built-in login resources expose that state field as challengeToken. JWTChallengeTokenStore is stateless; MemoryChallengeTokenStore is suitable for tests or single-instance deployments; RedisChallengeTokenStore is for distributed deployments. Challenge tokens expire after ChallengeTokenExpires. The JWT-backed store uses ClaimChallengePrincipalType, ClaimChallengePrincipalName, ClaimChallengeUsername, ClaimChallengePending, and ClaimChallengeResolved as compact claim keys.

Challenge token stores have different wire/storage shapes:

StoreToken/state contract
JWTChallengeTokenStoreJWT token, typ: "challenge", 5-minute ChallengeTokenExpires, subject is principal ID only
MemoryChallengeTokenStoreUUID token stored in process memory for ChallengeTokenExpires
RedisChallengeTokenStoreUUID token stored under vef:security:challenge:<token> for ChallengeTokenExpires

The JWT challenge claim keys are ptp (ClaimChallengePrincipalType), pnm (ClaimChallengePrincipalName), unm (ClaimChallengeUsername), pnd (ClaimChallengePending), and rsd (ClaimChallengeResolved). Under the reserved-identity hardening, challenge parsing accepts only user and external_app principal types — system, empty, and unknown types are all rejected with ErrTokenInvalid (a challenge token carrying the framework's internal identity has no legitimate origin), and a parsed principal that reports IsReserved() is rejected as well.

Challenge Claim Keys

The JWTChallengeTokenStore encodes challenge state as compact JWT claims. The following claim keys appear in challenge tokens and in the standard JWTClaimsBuilder/JWTClaimsAccessor:

Claim KeyConstantHolds
det(standard JWT claim)User details (claimDetails) — application-defined payload, carried in both access tokens and challenge tokens
pndClaimChallengePendingPending challenge types in evaluation order — the remaining types that have not yet been evaluated
pnmClaimChallengePrincipalNamePrincipal display name — stored as a separate claim because the subject (sub) carries only the principal ID
ptpClaimChallengePrincipalTypePrincipal type — user or external_app; system is rejected during challenge token parsing
rls(standard JWT claim)User roles (claimRoles) — carried in access tokens and challenge tokens
rsdClaimChallengeResolvedResolved challenge types in resolution order — the types that have already been satisfied

JWT.Generate sets iss, aud, iat, nbf, and exp. jti, sub, and typ are written by the caller's JWTClaimsBuilder before signing and are validated by JWT.Parse. The parser validates issuer JWTIssuer (vef), audience JWTConfig.Audience (the security module uses the snake-cased vef.app.name and falls back to DefaultJWTAudience (vef-app) only when no audience is configured), 10-second leeway, and HS256 signing.

Challenge tokens carry typ: "challenge" and expire after ChallengeTokenExpires (5m). The sub claim holds only the principal ID; pnm holds the display name. det and rls mirror the standard access-token claims so the challenge flow can reconstruct the principal without a database lookup.

Challenge providers are sorted by Order() in ascending order. The built-in convenience providers use 100 for TOTP, 200 for SMS, 300 for email, 400 for password change, and 500 for department selection. Providers that are not registered, or whose Evaluate(...) returns nil, are skipped. During resolve_challenge, the submitted type must match the first pending challenge type or the framework returns ErrChallengeTypeInvalid.

NewOTPChallengeProvider is the generic constructor. Its OTPChallengeProviderConfig requires ChallengeType, Evaluator, and Verifier; ChallengeOrder controls evaluation order, and Sender is optional and is used by delivered-code flows. OTPChallengeProvider returns OTPChallengeData to the client when a challenge is required. The delivered-code helpers combine OTPCodeStore and OTPCodeDelivery: DeliveredCodeSender, DeliveredCodeVerifier, NewDeliveredCodeSender, NewDeliveredCodeVerifier, NewDeliveredChallengeProvider, NewSMSChallengeProvider, and NewEmailChallengeProvider.

NewTOTPChallengeProvider only needs a TOTPSecretLoader; if LoadSecret(...) returns an empty string, the challenge is skipped. TOTPEvaluator, TOTPVerifier, and TOTPOption are the lower-level pieces behind the convenience provider. TOTP uses TOTPDefaultDestination (Authenticator App) unless WithTOTPDestination(...) overrides it.

NewPasswordChangeChallengeProvider uses PasswordChangeChecker and PasswordChanger, and accepts an optional security.PasswordValidator (pass nil to skip strength validation; the framework's config-backed validator from vef.security.password_policy can be injected); it returns PasswordChangeChallengeData when a password change is required. Common reason constants are PasswordChangeReasonFirstLogin (first_login) and PasswordChangeReasonExpired (expired). The concrete provider type is PasswordChangeChallengeProvider. NewDepartmentSelectionChallengeProvider uses DepartmentLoader and DepartmentSelector; resolve expects a department ID string. DepartmentLoader.LoadDepartments returns *DepartmentSelectionChallengeData so the host controls both the selectable options and the challenge-scoped metadata around them. A nil return, or data carrying no departments, skips the challenge.

DepartmentSelectionChallengeData serializes as departments plus optional meta; each DepartmentOption serializes as id, name, and optional meta. The loader's full payload is forwarded as-is — the framework does not rebuild it, so both per-option Meta (owning organization, parent id for tree rendering) and challenge-level Meta (the organization tree, default selection, grouping definitions) reach the client. Neither level is read, validated, or persisted by the framework.

The challenge constructors are wiring-time APIs. NewOTPChallengeProvider panics when ChallengeType, Evaluator, or Verifier is missing. NewPasswordChangeChallengeProvider panics when PasswordChangeChecker or PasswordChanger is missing. NewDepartmentSelectionChallengeProvider panics when DepartmentLoader or DepartmentSelector is missing.

Signature helpers

NewSignature(secret, ...) requires a non-empty hex-encoded secret and defaults to SignatureAlgHmacSHA256 with a 5-minute timestamp tolerance. The option type is SignatureOption. Other algorithm constants are SignatureAlgHmacSHA512 and SignatureAlgHmacSM3. WithTimestampTolerance changes the accepted timestamp window and WithNonceStore controls replay protection. Low-level NewSignature(...) creates a MemoryNonceStore by default; pass WithNonceStore(nil) only when you intentionally want to disable nonce storage for that helper. The built-in SignatureAuthenticator injects the application NonceStore when one is provided, otherwise each verification uses the low-level helper's process-local memory store. MemoryNonceStore is local to one process; RedisNonceStore is the distributed option. Stored nonces use twice the timestamp tolerance plus a 1-minute buffer as TTL.

The signed payload is exactly:

app_id=<appID>&method=<method>&nonce=<nonce>&path=<path>&timestamp=<timestamp>

The fields are bound in that order. The request body is not part of the signature payload. The method field is the HTTP method observed by the server.

NewIPWhitelistValidator returns an IPWhitelistValidator from a comma-separated list of IPs and CIDR ranges. An empty whitelist allows all IPs; an invalid whitelist is fail-closed and denies all requests. When an ExternalAppConfig.IPWhitelist is non-empty but the request IP cannot be resolved, SignatureAuthenticator also fails closed with ErrIPNotAllowed.

security.NewIPWhitelistValidatorFromEntries(entries) is the slice-based counterpart used by the built-in api.IPAuth(...) strategy. The strategy resolves a named security.IPWhitelist through security.IPWhitelistLoader; the default loader reads vef.security.ip_whitelists, while applications may provide their own loader for database or config-center backed lists. The built-in strategy is fail-closed: a missing, empty, or unparseable whitelist denies every request with security.ErrIPNotAllowed (HTTP 401).

Signature storage keys and defaults:

ContractValue
request headersX-App-ID, X-Timestamp, X-Nonce, X-Signature
algorithmsHMAC-SHA256, HMAC-SHA512, HMAC-SM3
default algorithmHMAC-SHA256
default tolerance5m
nonce TTL2*tolerance + 1m
Redis nonce prefixvef:security:nonce:
disable replay checkingWithNonceStore(nil)

Security-domain API errors expose ErrCode* constants: 10001029 for authentication, 10301039 for challenges, and 1050 for password policy (every policy violation shares that one code — see Login Hardening):

CodeConstantErrorHTTP status
1000ErrCodeUnauthenticatedErrUnauthenticated401
1001ErrCodeUnsupportedAuthenticationTypeunsupported authentication type400
1002ErrCodeTokenExpiredErrTokenExpired401
1003ErrCodeTokenInvalidErrTokenInvalid401
1004ErrCodeTokenNotValidYetErrTokenNotValidYet401
1005ErrCodeTokenInvalidIssuerErrTokenInvalidIssuer401
1006ErrCodeTokenInvalidAudienceErrTokenInvalidAudience401
1007ErrCodePrincipalInvalidErrPrincipalInvalid(message), ErrReservedPrincipal401
1008ErrCodeCredentialsInvalidErrCredentialsInvalid(message)401
1009ErrCodeAppIDRequiredErrAppIDRequired401
1010ErrCodeTimestampRequiredErrTimestampRequired401
1011ErrCodeSignatureRequiredErrSignatureRequired401
1012ErrCodeTimestampInvalidErrTimestampInvalid401
1013ErrCodeSignatureExpiredErrSignatureExpired401
1014ErrCodeExternalAppNotFoundErrExternalAppNotFound401
1015ErrCodeExternalAppDisabledErrExternalAppDisabled401
1016ErrCodeIPNotAllowedErrIPNotAllowed401
1017ErrCodeSignatureInvalidErrSignatureInvalid401
1018ErrCodeNonceRequiredErrNonceRequired401
1019ErrCodeNonceInvalidErrNonceInvalid401
1020ErrCodeNonceAlreadyUsedErrNonceAlreadyUsed401
1021ErrCodeAuthHeaderMissingErrAuthHeaderMissing401
1022ErrCodeAuthHeaderInvalidErrAuthHeaderInvalid401
1023ErrCodeAccountLockeddynamic account-locked error (see Login Hardening)429
1024ErrCodeTooManyConcurrentSessionsErrTooManyConcurrentSessions403
1025ErrCodeAPIKeyInvalidErrAPIKeyInvalid401
1026ErrCodeBasicCredentialsInvalidErrBasicCredentialsInvalid401
1031ErrCodeChallengeTokenInvalidErrChallengeTokenInvalid401
1033ErrCodeChallengeTypeInvalidErrChallengeTypeInvalid400
1034ErrCodeChallengeResolveFailedErrChallengeResolveFailed401
1035ErrCodeOTPCodeRequiredErrOTPCodeRequired400
1036ErrCodeOTPCodeInvalidErrOTPCodeInvalid401
1037ErrCodeNewPasswordRequiredErrNewPasswordRequired400
1038ErrCodeDepartmentRequiredErrDepartmentRequired400

Authentication-related sentinels include ErrUnauthenticated, ErrTokenExpired, ErrTokenInvalid, ErrTokenNotValidYet, ErrTokenInvalidIssuer, ErrTokenInvalidAudience, ErrAppIDRequired, ErrTimestampRequired, ErrSignatureRequired, ErrTimestampInvalid, ErrSignatureExpired, ErrSignatureInvalid, ErrExternalAppNotFound, ErrExternalAppDisabled, ErrIPNotAllowed, ErrNonceRequired, ErrNonceInvalid, ErrNonceAlreadyUsed, ErrAuthHeaderMissing, ErrAuthHeaderInvalid, ErrChallengeTokenInvalid, ErrChallengeTypeInvalid, ErrChallengeResolveFailed, ErrOTPCodeRequired, ErrOTPCodeInvalid, ErrNewPasswordRequired, ErrDepartmentRequired, ErrTooManyConcurrentSessions, ErrAPIKeyInvalid, ErrBasicCredentialsInvalid, and ErrReservedPrincipal (rejects a framework-internal identity at every entry point; it rides ErrCodePrincipalInvalid/1007 with HTTP 401), plus the factory helpers ErrCredentialsInvalid(message) and ErrPrincipalInvalid(message). ErrChallengeResolveFailed is not a reserved placeholder: resolve_challenge normalizes bare errors returned by a ChallengeProvider into it.

Low-level secret parsing errors use ErrDecodeJWTSecretFailed, ErrGenerateJWTSecretFailed, ErrDecodeSignatureSecretFailed, and ErrSignatureSecretRequired. ErrUserDetailsNotStruct and ErrExternalAppDetailsNotStruct are raised when detail-type registration is not given a struct or struct pointer. Public i18n message ID constants include ErrMessageChallengeResolveFailed, ErrMessageCredentialsFormatInvalid, ErrMessageExternalAppLoaderNotImplemented, ErrMessageUnauthenticated, ErrMessageUnsupportedAuthenticationType, ErrMessageUserInfoLoaderNotImplemented, and ErrMessageUserLoaderNotImplemented.

Error Message Constants

The security package exposes i18n message ID constants for callers that construct errors directly (template arguments, factory-based result.Err, Fiber error mapping):

ConstantMessage IDTriggered by
ErrMessageUnauthenticatedsecurity_unauthenticatedErrUnauthenticated — message ID behind the generic 1000 unauthenticated response; a missing bearer token surfaces as HTTP 401 mapped to this code/message by the global error handler
ErrMessageExternalAppLoaderNotImplementedsecurity_external_app_loader_not_implementedSignatureAuthenticator.Authenticate when ExternalAppLoader is nil
ErrMessageCredentialsFormatInvalidsecurity_credentials_format_invalidSignatureAuthenticator.Authenticate when credentials are not *SignatureCredentials
ErrMessageUnsupportedAuthenticationTypesecurity_unsupported_authentication_typeAuthManager.Authenticate when no authenticator supports the given type
ErrMessageUserLoaderNotImplementedsecurity_user_loader_not_implementedPasswordAuthenticator.Authenticate and JWTRefreshAuthenticator.Authenticate when UserLoader is nil
ErrMessageUserInfoLoaderNotImplementedsecurity_user_info_loader_not_implementedAuthResource.GetUserInfo when UserInfoLoader is nil
ErrMessageChallengeResolveFailedsecurity_challenge_resolve_failedresolve_challenge normalizes bare errors from ChallengeProvider.Resolve

RPC Resource: security/auth

The security module mounts the built-in authentication resource as an RPC resource under /api, using the standard envelope (resource, action, version, params). Responses ride the standard result envelope — code (0 on success), message, data — and the shapes below describe the data payload. Request parameter tables also appear in Built-in Resources; this section is the complete wire contract, including every response field.

ActionAccessRate limit (max)InputOutput (data)
loginPublicvef.security.login_rate_limit (default 6)LoginParamsLoginResult — tokens or a challenge envelope
refreshPublic; mounted only under token_type = "jwt_token"vef.security.refresh_rate_limit (default 1)RefreshParamsAuthTokens (no tokens wrapper)
logoutBearer authAPI engine defaultnoneempty (data: null)
resolve_challengePublicvef.security.login_rate_limit (default 6)ResolveChallengeParamsLoginResult — next challenge or final tokens
get_user_infoBearer authAPI engine defaultraw params mapUserInfo

The custom limits set only max; the window falls back to the API engine's default rate-limit period (vef.api.rate_limit.period, default 5m). Operations without a custom limit inherit the engine default entirely (stock 100 requests per 5m). Under token_type = "opaque_token" the refresh operation is not mounted at all — calling it fails with the operation-not-found error (HTTP 404), since opaque sessions renew themselves on use.

login

LoginParams:

FieldTypeRequiredDescription
typestringYescredential type. password is the only type the framework ships for this endpoint; custom security.Authenticator registrations extend the vocabulary. The framework-issued token types (jwt_token, opaque_token, refresh) are refused with code 1001 so an issued token can never be laundered into a fresh token pair
principalstringYeslogin identifier, typically the username. The built-in password flow rejects the reserved identifiers (system, cron_job, anonymous) with code 1007
credentialsanyYescredential payload. For type = "password" this is the password string — transport-encrypted when a security.PasswordDecryptor is configured, plaintext otherwise

The response is a LoginResult and takes exactly one of two shapes. tokens, challengeToken, and challenge are all omitempty: whichever half does not apply is absent, never null.

Shape 1 — tokens. No challenge provider is registered, or none applies to this account. data.tokens is an AuthTokens:

FieldTypeDescription
tokens.accessTokenstringthe bearer token for subsequent requests. Under jwt_token a JWT with a fixed 30m TTL; under opaque_token a random session reference valid per the session policy (idle_ttl / max_lifetime)
tokens.refreshTokenstringJWT refresh token, TTL vef.security.token_expires (default 168h). Omitted under opaque_token — sessions renew themselves, so no refresh token exists
{
"code": 0,
"message": "成功",
"data": {
"tokens": {
"accessToken": "eyJhbGciOiJIUzI1NiIs...",
"refreshToken": "eyJhbGciOiJIUzI1NiIs..."
}
}
}

The JSON response carries no expiry fields — token lifetimes are deployment configuration, communicated out of band. (The JWTs themselves carry the standard exp/iat/nbf claims.)

Shape 2 — challenge envelope. The credential verified, but at least one challenge provider requires a second step. No auth tokens are issued yet:

FieldTypeDescription
challengeTokenstringstate token carrying the challenge progress (principal, the original login identifier, pending and resolved types) — clients treat it as an opaque value and pass it to resolve_challenge. Each token expires after ChallengeTokenExpires (5m); every successful step issues a fresh one
challengeLoginChallengethe first pending challenge to resolve (below)

LoginChallenge:

FieldTypeDescription
typestringchallenge type wire value, e.g. totp, sms_otp, password_change (see the wire-value table)
dataanyprovider-specific presentation data; omitted when the provider supplies none. The OTP providers return {destination, meta?} (OTPChallengeData); department selection returns {departments, meta?} where each department entry carries {id, name, meta?} and the top-level meta carries challenge-scoped display data such as the organization tree
requiredboolwhether the challenge must be resolved to finish the login
{
"code": 0,
"message": "成功",
"data": {
"challengeToken": "eyJhbGciOiJIUzI1NiIs...",
"challenge": {
"type": "totp",
"data": { "destination": "Authenticator App" },
"required": true
}
}
}

Behavior notes:

  • Providers are evaluated strictly in Order() sequence; providers whose Evaluate(...) returns nil are skipped, so the envelope always carries the first challenge that actually applies.
  • The brute-force guard clears the failure counter as soon as the credential verifies — before any second factor. A rejected credential publishes a failure LoginEvent; the success event is published only when tokens are actually issued — immediately when no challenge applies, otherwise at the end of the challenge chain — always carrying the submitted identifier.
  • Typical failures, all from the error-code table above: 1001 (unsupported/refused type, HTTP 400), 1008 (invalid credentials — deliberately the same uniform response for an unknown user, a nil principal or empty stored hash, and a wrong password, HTTP 401), 1007 (reserved or invalid principal, HTTP 401), 1023 (account locked by the brute-force guard, HTTP 429), and the generic 1400 validation error for missing required fields (HTTP 400). A reserved-principal rejection is audited but not counted toward lockout, because the fault lies with the authenticator, not the caller.

refresh

Mounted only under the stateless JWT mechanism (token_type = "jwt_token", the default).

RefreshParams:

FieldTypeRequiredDescription
refreshTokenstringYesthe refresh token issued by login, a previous refresh, or a token-issuing resolve_challenge

The response data is the new AuthTokens pair directly — without the tokens wrapper login uses:

FieldTypeDescription
accessTokenstringfresh access token (30m TTL)
refreshTokenstringfresh refresh token (vef.security.token_expires TTL)
{
"code": 0,
"message": "成功",
"data": {
"accessToken": "eyJhbGciOiJIUzI1NiIs...",
"refreshToken": "eyJhbGciOiJIUzI1NiIs..."
}
}

Behavior notes:

  • Internally the token is authenticated as type refresh: the JWT must parse, carry typ: "refresh" (an access token is refused), and its subject must be the id@name form the built-in generator writes.
  • A refresh token is not usable before vef.security.refresh_not_before (default 15m) has elapsed since issue — an early exchange fails with 1004 (ErrCodeTokenNotValidYet).
  • The user is reloaded through UserLoader.LoadByID(...) so deactivated accounts stop refreshing; a loader error is returned to the caller as-is.
  • Each exchange returns a fresh pair. The presented refresh token is not revoked server-side — the mechanism is stateless — it simply ages out.
  • Typical failures: 1003 (malformed token, wrong typ, wrong subject shape, HTTP 401), 1002 (expired, HTTP 401), 1004 (not valid yet, HTTP 401), 1400 (missing/empty refreshToken, HTTP 400).

logout

No parameters. Always returns success with empty data — logout is deliberately not failable from the client's perspective, and clients must drop their stored tokens either way.

  • Under opaque_token, the session backing the presented bearer token is revoked best-effort: the token is read exactly like bearer auth reads it (Authorization: Bearer header, case-insensitive scheme, then the __accessToken query parameter), hashed, looked up, and revoked. A successful revocation notifies the registered security.SessionRevocationListeners, so coupled grants — e.g. WebSocket push connections — are torn down immediately. A missing session or a store failure is only logged and never fails the call.
  • Under jwt_token, there is no server-side session: logout is effectively a no-op and token invalidation is the client discarding its copy.

resolve_challenge

ResolveChallengeParams:

FieldTypeRequiredDescription
challengeTokenstringYesthe state token from the previous login or resolve_challenge response — always the latest one; each step re-issues it
typestringYesthe challenge type being resolved. Must equal the first pending type (the challenge.type just returned); anything else fails with 1033
responseanyYesprovider-specific answer, e.g. the OTP code string, the new password payload, or the selected department ID

The response is a LoginResult with the same two shapes as login:

  • Another challenge pending — a fresh challengeToken plus the next challenge. The chain is strictly sequential in provider order; providers whose Evaluate(...) returns nil for this principal are skipped. The new token carries the updated pending/resolved lists and the original login identifier (for audit continuity), and restarts the 5m expiry window.
  • All challenges resolveddata.tokens with the final AuthTokens, exactly as in login shape 1. Only at this point are auth tokens issued, and the success LoginEvent is published with the original login identifier.

Behavior notes:

  • Any challenge-token parse failure — expired, tampered, wrong typ, or the reserved-identity rejections (system/empty/unknown principal types, reserved IDs) described in Challenge providers — surfaces uniformly as 1031 (ErrChallengeTokenInvalid, HTTP 401) on this endpoint.
  • A rejected response is treated like a failed login: it counts toward the brute-force lockout for the original identifier and is audited. Providers that return a typed result.Error keep their code (1035 ErrOTPCodeRequired, 1036 ErrOTPCodeInvalid, 1037 ErrNewPasswordRequired, 1038 ErrDepartmentRequired); a bare error is normalized to 1034 (ErrChallengeResolveFailed, HTTP 401).
  • A provider that resolves to a nil or framework-reserved principal is refused with 1007 (ErrReservedPrincipal); the rejection is audited but not counted toward lockout — the second factor was correct, the fault is the provider's.
  • Wrong-type and invalid-token protocol errors (1031, 1033) are not guarded or audited; the lockout check (1023, HTTP 429) applies before the provider verifies the response.

get_user_info

Requires Bearer auth. The params object is not interpreted by the framework: it is forwarded verbatim to the application's security.UserInfoLoader.LoadUserInfo(ctx, principal, params). When no loader is registered, the action fails with the generic not-implemented error (code 1500, HTTP 501, message security_user_info_loader_not_implemented); a loader error is returned as-is.

The response data is the loader's security.UserInfo:

FieldTypeDescription
idstringuser identifier
namestringdisplay name
genderstringone of male, female, unknown (security.Gender)
avatarstring | nullavatar URL; null when unset (the field is always present)
permissionTokensstring[]permission tokens granted to the user, typically consumed by the frontend to gate UI affordances
menusUserMenu[]navigation menu tree (below)
detailsanyapplication-defined extension payload; omitted when absent (omitempty)

UserMenu (recursive):

FieldTypeDescription
typestringone of directory, menu, view, dashboard, report (security.UserMenuType)
pathstringroute path
namestringdisplay name
iconstring | nullicon identifier; null when unset (always present)
metaobjectoptional extension map; omitted when absent
childrenUserMenu[]child entries; omitted when absent
{
"code": 0,
"message": "成功",
"data": {
"id": "user001",
"name": "Alice",
"gender": "female",
"avatar": null,
"permissionTokens": ["user.read", "order.read"],
"menus": [
{
"type": "directory",
"path": "/system",
"name": "System Management",
"icon": "setting",
"children": [
{ "type": "menu", "path": "/system/users", "name": "User Management", "icon": null }
]
}
]
}
}

permissionTokens and menus carry no omitempty: return empty slices (not nil) from your loader so clients see [] rather than null.

Next Step