Appearance
GrydAuth Auth API Reference (V1)
Referencia oficial dos endpoints de autenticacao e configuracoes do usuario no modulo Auth.
Escopo
Este arquivo cobre:
AuthController(/api/v1/auth/*)UserPreferencesController(/api/v1/auth/me/preferences/*)UserMenuFavoritesController(/api/v1/auth/me/menu-favorites/*)
De/Para de contratos (frontend)
| Endpoint | Antes | Agora | Observacao |
|---|---|---|---|
POST /api/v1/auth/login | LoginCommand | LoginRequest | requestedAppId suportado no body; X-App-Id no header e priorizado |
POST /api/v1/auth/social-login | SocialLoginCommand | SocialLoginRequest | requestedAppId suportado no body; X-App-Id no header e priorizado |
POST /api/v1/auth/refresh | RefreshTokenCommand | RefreshTokenRequest | tenantId opcional |
POST /api/v1/auth/switch-tenant | SwitchTenantCommand | SwitchTenantRequest | sem mudanca de payload |
POST /api/v1/auth/request-password-reset | RequestPasswordResetCommand | RequestPasswordResetRequest | ipAddress e userAgent continuam server-side |
POST /api/v1/auth/reset-password | ResetPasswordCommand | ResetPasswordRequest | ipAddress e userAgent continuam server-side |
POST /api/v1/auth/change-password | ChangePasswordCommand | ChangePasswordRequest | userId continua extraido do token |
POST /api/v1/auth/complete-first-login | CompleteFirstLoginCommand | CompleteFirstLoginRequest | userId continua extraido do token |
PUT /api/v1/auth/me/preferences/{key} | UpsertUserPreferenceCommand | UpsertUserPreferenceRequest | key continua vindo da rota |
Contrato HTTP (V1)
- Base path:
/api/v1 - Sucesso: payload direto (sem envelope
Result<T>no body HTTP) - Erro:
ProblemDetails(application/problem+json)
Shape de erro
json
{
"type": "https://gryd.io/errors/authentication-error",
"title": "Unauthorized",
"status": 401,
"detail": "Access token expired.",
"instance": "/api/v1/auth/validate",
"traceId": "00-...",
"code": "TOKEN_EXPIRED",
"errors": ["Access token expired."]
}Tipos de response principais
AuthenticationResponse (v5)
v5 (Epic 533): o corpo de resposta não contém mais o
refreshToken. O refresh token é entregue exclusivamente em um cookieHttpOnly(Set-Cookie: __Secure-gryd.refresh; HttpOnly; Secure; SameSite; Path=/api/v1/auth), inacessível ao JavaScript. O access token permanece em memória e continua no headerAuthorization: Bearer. Junto ao refresh é emitido um cookie CSRF legívelgryd.csrf(double-submit) — ver/api/v1/auth/refresh. Ver o levantamento emtoken-storage-backend-change-inventory.md.
json
{
"token": "eyJ...",
"expiresAt": "2026-02-23T18:00:00Z",
"permissions": ["read:users"],
"isFirstLogin": false,
"mustChangePassword": false,
"daysUntilPasswordExpiration": 30,
"isGlobal": false,
"requiresTenantSelection": false,
"tokenType": "Tenant",
"availableTenants": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "Tenant A",
"isDefault": true,
"permissions": ["read:users"],
"domain": "tenant-a",
"type": "company",
"parentTenantId": "11111111-1111-1111-1111-111111111111",
"parentTenantName": "Holding Sul",
"groupId": "11111111-1111-1111-1111-111111111111",
"groupName": "Holding Sul"
}
],
"currentTenant": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "Tenant A",
"isDefault": true,
"permissions": ["read:users"],
"domain": "tenant-a",
"type": "company",
"parentTenantId": "11111111-1111-1111-1111-111111111111",
"parentTenantName": "Holding Sul",
"groupId": "11111111-1111-1111-1111-111111111111",
"groupName": "Holding Sul"
},
"smartAutoSwitched": true,
"groupId": "11111111-1111-1111-1111-111111111111",
"groupName": "Holding Sul",
"tenantType": "company"
}UserBasicProfileDto
json
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "John Doe",
"email": "john@company.com",
"roles": ["Admin"],
"permissions": ["read:users", "update:users"]
}TokenValidationDto
json
{
"isValid": true,
"userId": "550e8400-e29b-41d4-a716-446655440000",
"email": "john@company.com",
"tenantId": "550e8400-e29b-41d4-a716-446655440000",
"expiresAt": "2026-02-23T18:00:00Z"
}PasswordPolicyDto
json
{
"minLength": 8,
"maxLength": 128,
"requireUppercase": true,
"requireLowercase": true,
"requireDigit": true,
"requireSpecialChar": true,
"specialCharacters": "!@#$...",
"enablePasswordExpiration": true,
"expirationDays": 90,
"warnBeforeExpirationDays": 15,
"enablePasswordHistory": true,
"passwordHistoryCount": 5,
"summary": "..."
}ClaimsInfoDto
json
{
"isAuthenticated": true,
"userId": "550e8400-e29b-41d4-a716-446655440000",
"email": "john@company.com",
"tenantId": "550e8400-e29b-41d4-a716-446655440000",
"claims": {
"sub": "550e8400-e29b-41d4-a716-446655440000"
}
}UserPreferenceDto
json
{
"id": "550e8400-e29b-41d4-a716-446655440111",
"key": "theme",
"value": "dark",
"createdAt": "2026-02-23T17:00:00Z",
"updatedAt": "2026-02-23T17:10:00Z"
}UserMenuFavoriteDto
json
{
"id": "550e8400-e29b-41d4-a716-446655440222",
"menuItemId": "dashboard",
"displayOrder": 0,
"createdAt": "2026-02-23T17:00:00Z"
}Modelos de request
LoginRequest
json
{
"email": "john@company.com",
"password": "Password123!",
"preferredTenantId": "550e8400-e29b-41d4-a716-446655440000",
"requestedAppId": "APP_WEB"
}Obs: sem isPasswordEncrypted — RSA client-side foi removido no v5, senha trafega em texto puro sobre TLS. Ver password transport breaking change.
Obs: X-App-Id (header) tem prioridade. Se o header nao for enviado, requestedAppId no body pode ser usado como fallback.
SocialLoginRequest
json
{
"provider": "google",
"accessToken": "oauth-token",
"preferredTenantId": "550e8400-e29b-41d4-a716-446655440000",
"requestedAppId": "APP_WEB"
}Obs: X-App-Id (header) tem prioridade. Se o header nao for enviado, requestedAppId no body pode ser usado como fallback.
RefreshTokenRequest
v5 (Epic 533): o
/api/v1/auth/refreshlê o refresh token do cookie__Secure-gryd.refresh(Path=/api/v1/auth, HttpOnly, não legível por JS) e exige o headerX-Gryd-Csrfcom o valor do cookie legívelgryd.csrf(Path=/, para o SPA conseguir lê-lo viadocument.cookie) — proteção CSRF double-submit. O SPA devedecodeURIComponento valor do cookie antes de enviá-lo no header. O cliente deve enviarcredentials: include. Cookie de refresh ausente →401; header CSRF ausente/divergente →403. A rotação single-use reescreve o cookie a cada refresh.
json
{
"tenantId": "550e8400-e29b-41d4-a716-446655440000"
}tenantId (override opcional de tenant) pode ser omitido; é o único campo do corpo.
SwitchTenantRequest
json
{
"tenantId": "550e8400-e29b-41d4-a716-446655440000"
}RequestPasswordResetRequest
json
{
"email": "john@company.com",
"baseUrl": "https://app.company.com"
}Obs: ipAddress e userAgent sao capturados no servidor.
ResetPasswordRequest
json
{
"token": "reset-token",
"newPassword": "NewPassword123!",
"confirmPassword": "NewPassword123!"
}Obs: ipAddress e userAgent sao capturados no servidor. O token e o valor em texto puro recebido no link de e-mail; a partir do v5 o servidor armazena apenas o hash SHA-256 do token (a coluna Token virou TokenHash). O contrato do endpoint nao muda.
ChangePasswordRequest
json
{
"currentPassword": "OldPassword123!",
"newPassword": "NewPassword123!"
}Obs: userId e extraido do token no servidor.
CompleteFirstLoginRequest
json
{
"currentPassword": "TempPassword123!",
"newPassword": "NewPassword123!",
"confirmPassword": "NewPassword123!"
}Obs: userId e extraido do token no servidor.
UpsertUserPreferenceRequest (PUT por chave)
json
{
"value": "dark"
}Obs: key vem da rota e sobrescreve o body.
UpsertUserPreferencesRequest (bulk)
json
{
"preferences": [
{ "key": "theme", "value": "dark" },
{ "key": "language", "value": "pt-BR" }
]
}AddMenuFavoriteRequest
json
{
"menuItemId": "dashboard"
}ReorderMenuFavoritesRequest
json
{
"orderedMenuItemIds": ["dashboard", "users", "settings"]
}Auth endpoints
Base route: /api/v1/auth
| Metodo | Rota | Auth | Request | Sucesso |
|---|---|---|---|---|
| POST | /api/v1/auth/login | Publico | Body LoginRequest + opcional header X-App-Id (prioritario) | 200 AuthenticationResult |
| POST | /api/v1/auth/social-login | Publico | Body SocialLoginRequest + opcional header X-App-Id (prioritario) | 200 AuthenticationResult |
| POST | /api/v1/auth/refresh | Publico | Body RefreshTokenRequest | 200 AuthenticationResult |
| POST | /api/v1/auth/logout | Publico (idempotente) | Sem body | 204 vazio |
| POST | /api/v1/auth/switch-tenant | Bearer (global ou tenant token) | Body SwitchTenantRequest | 200 AuthenticationResult |
| POST | /api/v1/auth/request-password-reset | Publico | Body RequestPasswordResetRequest | 200 vazio |
| POST | /api/v1/auth/reset-password | Publico | Body ResetPasswordRequest | 200 vazio |
| GET | /api/v1/auth/me | Bearer | - | 200 UserBasicProfileDto |
| GET | /api/v1/auth/me/complete | Bearer | - | 200 UserDto |
| GET | /api/v1/auth/test-claims | Sem atributo de auth no endpoint | - | 200 ClaimsInfoDto |
| GET | /api/v1/auth/validate | Bearer (global ou tenant token) | - | 200 TokenValidationDto |
| POST | /api/v1/auth/change-password | Bearer | Body ChangePasswordRequest | 200 vazio |
| POST | /api/v1/auth/complete-first-login | Bearer (global ou tenant token) | Body CompleteFirstLoginRequest | 200 AuthenticationResult |
| GET | /api/v1/auth/password-policy | Publico | - | 200 PasswordPolicyDto |
GET /api/v1/auth/public-key removido no v5
O endpoint de chave publica para criptografia RSA client-side da senha foi removido — RSA client-side foi eliminado, senha trafega em texto puro sobre TLS. Ver password transport breaking change e o guia de migracao v4 → v5.
JWKS (v5)
Os tokens passam a ser assinados de forma assimetrica (RS256/ES256). As chaves publicas para validacao ficam disponiveis no endpoint padrao GET /.well-known/jwks.json (rota absoluta, fora de /api/v1/auth, publico/anonimo), retornando um JWK Set com kid. Consulte o guia de migracao v4 → v5.
Token type aceito por endpoint
O token_type aceito e declarado uma unica vez, pelo atributo de authorization do endpoint (IAllowedTokenTypesMetadata). O GrydAuthTokenProcessor le essa metadata do endpoint roteado — ele nao mantem uma allowlist propria por path. Enviar um token_type fora da lista resulta em 401 com code: TOKEN_INVALID.
| Rota | token_type aceito |
|---|---|
POST /api/v1/auth/switch-tenant | global, access |
GET /api/v1/auth/validate | global, access |
POST /api/v1/auth/complete-first-login | global, access |
POST /api/v1/mfa/challenge/begin | mfa_pending |
POST /api/v1/mfa/challenge/verify | mfa_pending |
POST /api/v1/mfa/totp/enroll/begin | access, mfa_enrollment |
POST /api/v1/mfa/totp/enroll/confirm | access, mfa_enrollment |
| Demais endpoints autenticados | access (default fail-secure) |
POST /api/v1/auth/logout e [AllowAnonymous] e idempotente: nao valida token type e sempre expira os cookies gryd.refresh e gryd.csrf, mesmo se o Bearer enviado for global, blacklistado ou de uma token_version antiga.
O token global tem escopo tenant-selector-only: ele nunca alcanca endpoints de negocio. Alem disso e single-use em qualquer endpoint que o aceite — o token gasto no complete-first-login e consumido, e o fluxo continua com o novo token global devolvido na resposta.
Erros:
ProblemDetailscom status conformecode- Tipicos:
400,401,403,404,409,422,429
Rate limiting (v5)
Os endpoints sensiveis login, refresh, request-password-reset e reset-password tem rate limiting nativo por IP e por conta. Ao exceder o limite, a resposta e 429 Too Many Requests com header Retry-After e ProblemDetails (errorCode: AUTH_RATE_LIMIT_EXCEEDED). Limites configuraveis em GrydAuth:Security:RateLimiting. Alem disso, em modo degradado (deteccao de reuso indisponivel) o refresh e negado com 401 (fail-secure). Ver o guia de migracao v4 → v5.
Matriz canonica de erro (frontend)
401 (autenticacao/sessao):
TOKEN_MISSINGTOKEN_INVALIDTOKEN_EXPIREDTOKEN_REVOKEDSESSION_INVALIDATEDREFRESH_TOKEN_INVALIDREFRESH_TOKEN_EXPIREDREFRESH_TOKEN_REVOKEDINVALID_CREDENTIALS
403 (autorizacao/permissao/contexto):
FORBIDDENMISSING_PERMISSIONTENANT_FORBIDDENCROSS_TENANT_FORBIDDENAUTH_RISK_FORBIDDEN(ex.: bloqueio por politica de risco emPOST /api/v1/auth/login)
429 (rate limiting):
AUTH_RATE_LIMIT_EXCEEDED(login, refresh, request-password-reset, reset-password) — honrarRetry-After.
Regra de consumo no frontend:
401: considerar sessao invalida (tentar refresh quando aplicavel; se falhar, logout/login).403: usuario autenticado sem permissao/contexto (nao deslogar automaticamente).429: aguardarRetry-Aftere tentar novamente (nao deslogar; exibir feedback de "muitas tentativas").- Sempre ler
ProblemDetails.codecomo fonte primaria para roteamento de UX.
Ajustes obrigatorios no frontend (Zero Trust + sessao)
- Fluxo de login:
- Se
POST /api/v1/auth/loginretornar403comcode=AUTH_RISK_FORBIDDEN, tratar como bloqueio de risco (nao executar logout forçado). - Se retornar
401comcode=SESSION_INVALIDATED, tratar como sessao invalida e redirecionar para login.
- Fluxo autenticado (demais endpoints):
401+SESSION_INVALIDATED,TOKEN_EXPIRED,TOKEN_REVOKED,TOKEN_INVALID=> limpar sessao local e iniciar fluxo de reautenticacao.403+MISSING_PERMISSION,FORBIDDEN,TENANT_FORBIDDEN,CROSS_TENANT_FORBIDDEN=> manter sessao e renderizar tela/estado sem acesso.
- Correlacao de sessao de seguranca:
- O backend resolve
sessionIdporsession_idclaim (prioritario), depois headersX-Security-Session-Id/X-Session-Id. - Quando o cliente nao controlar esses headers, o contrato continua funcional via claim/token.
- Se o frontend usar BFF/gateway que manipula sessao, manter envio consistente de
X-Security-Session-Id.
Security endpoints
Base route: /api/v1/security
| Metodo | Rota | Auth | Request | Sucesso |
|---|---|---|---|---|
| POST | /api/v1/security/clear-state | Bearer + admin:system | Body ClearSecurityStateRequest | 200 ClearSecurityStateResultDto |
Request ClearSecurityStateRequest:
json
{
"ipAddress": "127.0.0.1",
"userId": "11111111-1111-1111-1111-111111111111"
}Regras:
- Pelo menos um entre
ipAddressouuserIddeve ser informado. - Endpoint remove somente estado de seguranca relacionado (nao executa flush global).
User Preferences endpoints
Base route: /api/v1/auth/me/preferences
Todos exigem Authorization: Bearer.
| Metodo | Rota | Request | Sucesso |
|---|---|---|---|
| GET | /api/v1/auth/me/preferences | - | 200 UserPreferenceDto[] |
| GET | /api/v1/auth/me/preferences/{key} | Path key | 200 UserPreferenceDto |
| PUT | /api/v1/auth/me/preferences/{key} | Path key + Body UpsertUserPreferenceRequest | 200 UserPreferenceDto |
| PUT | /api/v1/auth/me/preferences | Body UpsertUserPreferencesRequest | 200 UserPreferenceDto[] |
| DELETE | /api/v1/auth/me/preferences/{key} | Path key | 200 vazio |
Erros:
GET/DELETEpor chave inexistente retornam404(ProblemDetails)- Demais falhas retornam
ProblemDetailsconformecode
User Menu Favorites endpoints
Base route: /api/v1/auth/me/menu-favorites
Todos exigem Authorization: Bearer.
| Metodo | Rota | Request | Sucesso |
|---|---|---|---|
| GET | /api/v1/auth/me/menu-favorites | - | 200 UserMenuFavoriteDto[] |
| POST | /api/v1/auth/me/menu-favorites | Body AddMenuFavoriteRequest | 201 UserMenuFavoriteDto + Location |
| DELETE | /api/v1/auth/me/menu-favorites/{menuItemId} | Path menuItemId | 200 vazio |
| PUT | /api/v1/auth/me/menu-favorites/reorder | Body ReorderMenuFavoritesRequest | 200 UserMenuFavoriteDto[] |
Erros:
DELETEde item inexistente retorna404(ProblemDetails)- Demais falhas retornam
ProblemDetailsconformecode
Observacoes finais
- Contrato HTTP V1 mantido (
/api/v1). - Sucesso sem envelope
Result<T>. - Para erros, sempre tratar
ProblemDetails(detail,code,errors,traceId).