Skip to content

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)

EndpointAntesAgoraObservacao
POST /api/v1/auth/loginLoginCommandLoginRequestrequestedAppId suportado no body; X-App-Id no header e priorizado
POST /api/v1/auth/social-loginSocialLoginCommandSocialLoginRequestrequestedAppId suportado no body; X-App-Id no header e priorizado
POST /api/v1/auth/refreshRefreshTokenCommandRefreshTokenRequesttenantId opcional
POST /api/v1/auth/switch-tenantSwitchTenantCommandSwitchTenantRequestsem mudanca de payload
POST /api/v1/auth/request-password-resetRequestPasswordResetCommandRequestPasswordResetRequestipAddress e userAgent continuam server-side
POST /api/v1/auth/reset-passwordResetPasswordCommandResetPasswordRequestipAddress e userAgent continuam server-side
POST /api/v1/auth/change-passwordChangePasswordCommandChangePasswordRequestuserId continua extraido do token
POST /api/v1/auth/complete-first-loginCompleteFirstLoginCommandCompleteFirstLoginRequestuserId continua extraido do token
PUT /api/v1/auth/me/preferences/{key}UpsertUserPreferenceCommandUpsertUserPreferenceRequestkey 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 cookie HttpOnly (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 header Authorization: Bearer. Junto ao refresh é emitido um cookie CSRF legível gryd.csrf (double-submit) — ver /api/v1/auth/refresh. Ver o levantamento em token-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/refresh lê o refresh token do cookie __Secure-gryd.refresh (Path=/api/v1/auth, HttpOnly, não legível por JS) e exige o header X-Gryd-Csrf com o valor do cookie legível gryd.csrf (Path=/, para o SPA conseguir lê-lo via document.cookie) — proteção CSRF double-submit. O SPA deve decodeURIComponent o valor do cookie antes de enviá-lo no header. O cliente deve enviar credentials: 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

MetodoRotaAuthRequestSucesso
POST/api/v1/auth/loginPublicoBody LoginRequest + opcional header X-App-Id (prioritario)200 AuthenticationResult
POST/api/v1/auth/social-loginPublicoBody SocialLoginRequest + opcional header X-App-Id (prioritario)200 AuthenticationResult
POST/api/v1/auth/refreshPublicoBody RefreshTokenRequest200 AuthenticationResult
POST/api/v1/auth/logoutPublico (idempotente)Sem body204 vazio
POST/api/v1/auth/switch-tenantBearer (global ou tenant token)Body SwitchTenantRequest200 AuthenticationResult
POST/api/v1/auth/request-password-resetPublicoBody RequestPasswordResetRequest200 vazio
POST/api/v1/auth/reset-passwordPublicoBody ResetPasswordRequest200 vazio
GET/api/v1/auth/meBearer-200 UserBasicProfileDto
GET/api/v1/auth/me/completeBearer-200 UserDto
GET/api/v1/auth/test-claimsSem atributo de auth no endpoint-200 ClaimsInfoDto
GET/api/v1/auth/validateBearer (global ou tenant token)-200 TokenValidationDto
POST/api/v1/auth/change-passwordBearerBody ChangePasswordRequest200 vazio
POST/api/v1/auth/complete-first-loginBearer (global ou tenant token)Body CompleteFirstLoginRequest200 AuthenticationResult
GET/api/v1/auth/password-policyPublico-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.

Rotatoken_type aceito
POST /api/v1/auth/switch-tenantglobal, access
GET /api/v1/auth/validateglobal, access
POST /api/v1/auth/complete-first-loginglobal, access
POST /api/v1/mfa/challenge/beginmfa_pending
POST /api/v1/mfa/challenge/verifymfa_pending
POST /api/v1/mfa/totp/enroll/beginaccess, mfa_enrollment
POST /api/v1/mfa/totp/enroll/confirmaccess, mfa_enrollment
Demais endpoints autenticadosaccess (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:

  • ProblemDetails com status conforme code
  • 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_MISSING
  • TOKEN_INVALID
  • TOKEN_EXPIRED
  • TOKEN_REVOKED
  • SESSION_INVALIDATED
  • REFRESH_TOKEN_INVALID
  • REFRESH_TOKEN_EXPIRED
  • REFRESH_TOKEN_REVOKED
  • INVALID_CREDENTIALS

403 (autorizacao/permissao/contexto):

  • FORBIDDEN
  • MISSING_PERMISSION
  • TENANT_FORBIDDEN
  • CROSS_TENANT_FORBIDDEN
  • AUTH_RISK_FORBIDDEN (ex.: bloqueio por politica de risco em POST /api/v1/auth/login)

429 (rate limiting):

  • AUTH_RATE_LIMIT_EXCEEDED (login, refresh, request-password-reset, reset-password) — honrar Retry-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: aguardar Retry-After e tentar novamente (nao deslogar; exibir feedback de "muitas tentativas").
  • Sempre ler ProblemDetails.code como fonte primaria para roteamento de UX.

Ajustes obrigatorios no frontend (Zero Trust + sessao)

  1. Fluxo de login:
  • Se POST /api/v1/auth/login retornar 403 com code=AUTH_RISK_FORBIDDEN, tratar como bloqueio de risco (nao executar logout forçado).
  • Se retornar 401 com code=SESSION_INVALIDATED, tratar como sessao invalida e redirecionar para login.
  1. 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.
  1. Correlacao de sessao de seguranca:
  • O backend resolve sessionId por session_id claim (prioritario), depois headers X-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

MetodoRotaAuthRequestSucesso
POST/api/v1/security/clear-stateBearer + admin:systemBody ClearSecurityStateRequest200 ClearSecurityStateResultDto

Request ClearSecurityStateRequest:

json
{
  "ipAddress": "127.0.0.1",
  "userId": "11111111-1111-1111-1111-111111111111"
}

Regras:

  • Pelo menos um entre ipAddress ou userId deve 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.

MetodoRotaRequestSucesso
GET/api/v1/auth/me/preferences-200 UserPreferenceDto[]
GET/api/v1/auth/me/preferences/{key}Path key200 UserPreferenceDto
PUT/api/v1/auth/me/preferences/{key}Path key + Body UpsertUserPreferenceRequest200 UserPreferenceDto
PUT/api/v1/auth/me/preferencesBody UpsertUserPreferencesRequest200 UserPreferenceDto[]
DELETE/api/v1/auth/me/preferences/{key}Path key200 vazio

Erros:

  • GET/DELETE por chave inexistente retornam 404 (ProblemDetails)
  • Demais falhas retornam ProblemDetails conforme code

User Menu Favorites endpoints

Base route: /api/v1/auth/me/menu-favorites

Todos exigem Authorization: Bearer.

MetodoRotaRequestSucesso
GET/api/v1/auth/me/menu-favorites-200 UserMenuFavoriteDto[]
POST/api/v1/auth/me/menu-favoritesBody AddMenuFavoriteRequest201 UserMenuFavoriteDto + Location
DELETE/api/v1/auth/me/menu-favorites/{menuItemId}Path menuItemId200 vazio
PUT/api/v1/auth/me/menu-favorites/reorderBody ReorderMenuFavoritesRequest200 UserMenuFavoriteDto[]

Erros:

  • DELETE de item inexistente retorna 404 (ProblemDetails)
  • Demais falhas retornam ProblemDetails conforme code

Observacoes finais

  • Contrato HTTP V1 mantido (/api/v1).
  • Sucesso sem envelope Result<T>.
  • Para erros, sempre tratar ProblemDetails (detail, code, errors, traceId).

Released under the MIT License.