Appearance
First Login Flow
This page documents the login flow when a user logs in for the first time or when password change is required.
Overview
When a user's account is newly created or an administrator forces a password reset, the IsFirstLogin or MustChangePassword flag is set to true. The login response includes these flags so the client can redirect the user to the password change screen before tenant selection.
Key architectural decision: Password is a user-level concern, not tenant-level. The user changes their password with a global token, then selects a tenant.
Triggers for Mandatory Password Change
| Condition | Description |
|---|---|
IsFirstLogin = true | User has never logged in |
MustChangePassword = true | Admin-forced password reset |
PasswordExpiresAt < NOW() | Password has expired (policy-based) |
Sequence Diagram
100% 💡 Use Ctrl + Scroll para zoom | Arraste para navegar
Step-by-Step Explanation
Phase 1: Initial Login
1.1 Credential Validation
The user provides valid credentials. The system verifies:
- Email exists and user is active
- Password is correct
- AppId is valid (if configured)
1.2 Password Change Detection
After successful credential validation, the login response includes flags:
json
{
"isFirstLogin": true,
"mustChangePassword": true
}The client detects these flags and redirects to the password change form before tenant selection.
1.3 Global Token
For multi-tenant users without a default tenant, the login returns a global token:
json
{
"token": "eyJhbGciOiJIUzI1NiIs...",
"isGlobal": true,
"requiresTenantSelection": true,
"tokenType": "Global",
"availableTenants": [
{ "id": "...", "name": "Tenant A", "isDefault": false },
{ "id": "...", "name": "Tenant B", "isDefault": false }
]
}Token Properties
- TTL: 2 minutes (short-lived for security)
- Scope:
tenant-selector-only— cannot access tenant resources - Single-use: Enforced by the authorization handler
Phase 2: Complete First Login
2.1 Submit New Password
The client calls POST /api/v1/auth/complete-first-login with the global token:
json
{
"currentPassword": "TemporaryPassword123!",
"newPassword": "NewSecurePassword456!",
"confirmPassword": "NewSecurePassword456!"
}RSA Encryption
Passwords can be RSA-encrypted client-side using the public key from GET /api/v1/auth/public-key. Set isCurrentPasswordEncrypted and isNewPasswordEncrypted to true.
2.2 Password Validation Pipeline
The handler delegates to IPasswordChangeService (shared with change-password — DRY):
- Decrypt passwords if encrypted
- Verify current (temporary) password matches
- Check history — prevents reusing the last N passwords
- Validate strength — minimum length, complexity, etc.
- Hash new password and update user
2.3 Token Invalidation
After password change, all previous tokens are invalidated:
csharp
user.CompleteFirstLogin(); // IsFirstLogin=false, MustChangePassword=false
user.InvalidateAllTokens("first_login"); // TokenVersion++ → old tokens rejected by middleware2.4 New Global Token
A fresh global token is generated with the updated TokenVersion. This allows the user to proceed to tenant selection without re-authenticating:
json
{
"token": "eyJhbGciOiJIUzI1NiIs...",
"isFirstLogin": false,
"mustChangePassword": false,
"isGlobal": true,
"requiresTenantSelection": true,
"availableTenants": [...]
}Why a new token?
- The original global token was consumed (single-use enforcement)
- The
TokenVersionwas incremented, invalidating all previous tokens - The user has already proven their identity (valid token + correct old password + new password set)
- Forcing re-login would be redundant and poor UX
Phase 3: Tenant Selection
The user selects a tenant from availableTenants. This follows the standard Switch Tenant flow, producing a full tenant token with 60-minute TTL, refresh token, roles, and permissions.
Error Scenarios
| Error | HTTP Status | Cause |
|---|---|---|
| Invalid current password | 400 | Temporary password doesn't match |
| Password reuse | 400 | New password matches a recent one |
| Weak password | 400 | Doesn't meet strength requirements |
| Passwords don't match | 400 | newPassword ≠ confirmPassword |
| Token expired | 401 | Global token expired (2 min TTL) |
| Token already used | 403 | Global token was already consumed |
| Not first login | 400 | User doesn't need to change password |
Security Considerations
Global Token Limitations
The global token used during first login:
- Has a 2-minute TTL (short-lived)
- Is single-use (replay attack prevention)
- Contains no roles or permissions
- Cannot access tenant-scoped resources
- Is validated by
AllowGlobalAndTenantTokenHandler
Password History
Password history prevents:
- Reusing recent passwords (configurable count via
PasswordPolicyOptions.HistoryCount) - Cycling through passwords to return to a favorite
- Predictable password rotation
Token Invalidation
After password change:
TokenVersionis incremented in the database- The middleware (
GrydAuthTokenProcessor) compares the token'stoken_versionclaim against Redis cache - All tokens with old version are rejected (fail-secure)
Code Example
Client-Side Flow (TypeScript)
typescript
async function handleLogin(email: string, password: string) {
const response = await authService.login({ email, password });
if (response.data.isFirstLogin || response.data.mustChangePassword) {
// Store global token and redirect to password change
authStore.setGlobalToken(response.data.token);
authStore.setAvailableTenants(response.data.availableTenants);
router.push('/change-password');
return;
}
if (response.data.requiresTenantSelection) {
// Multi-tenant user, go to tenant selection
authStore.setGlobalToken(response.data.token);
authStore.setAvailableTenants(response.data.availableTenants);
router.push('/select-tenant');
return;
}
// Single tenant or auto-switched — ready to go
authStore.setTokens(response.data.token, response.data.refreshToken);
router.push('/dashboard');
}
async function handleFirstLoginPasswordChange(
currentPassword: string,
newPassword: string
) {
const globalToken = authStore.getGlobalToken();
const response = await authService.completeFirstLogin({
currentPassword,
newPassword,
confirmPassword: newPassword
}, globalToken);
// Response contains NEW global token + available tenants
authStore.setGlobalToken(response.data.token);
authStore.setAvailableTenants(response.data.availableTenants);
// Now go to tenant selection
router.push('/select-tenant');
}
async function handleTenantSelection(tenantId: string) {
const globalToken = authStore.getGlobalToken();
const response = await authService.switchTenant({ tenantId }, globalToken);
// Full tenant token received
authStore.setTokens(response.data.token, response.data.refreshToken);
router.push('/dashboard');
}Related Flows
- Login Flow - Standard login with default tenant
- Switch Tenant - Tenant selection after login