Authentication

Dual auth (Admin + User), multi-workspace routing, JWT tokens, 2FA with backup codes, password expiry enforcement, SSO suspension gate, and tenant-scoped password policy.

SCRIPE provides a secure authentication system with JWT access tokens, refresh token rotation, optional two-factor authentication, multi-workspace login discovery, password expiry enforcement, and comprehensive rate limiting. The system supports separate Admin and User authentication flows with different JWT claims and permissions.

Authentication Flow

Login & Workspace Routing Flow
POST /api/v1/auth/admin/login
Is Account Locked Out? (LockoutEnd > UtcNow)
Yes
Reject Login: auth.accountLocked
Is TenantId / isPlatformAdmin provided?
Has TenantId
Case A: Decrypt TenantId & Query Candidate
Case A': Query TenantId = null
Case B: Cross-Tenant Email Discovery
Verify Password via BCrypt
Verify Candidates (Password Verified?)
Workspace Matches Count?
N > 1 workspaces
Return: RequiresWorkspaceSelection
Proceed to Post-Login Checks

Two-Factor Authentication (Deep)

Post-Login Security Gates & 2FA Flow
Post-Login Security Gate
Is Tenant Suspended / Deactivated?
Yes
Block login: tenantSuspended / tenantDeactivated
ITenantPasswordValidator: Password Expired?
Yes
Set MustChangePassword = true, JWT has mcp:true
Is 2FA enabled on Admin?
Yes
Issue Temporary 2FA Token
POST /api/v1/auth/admin/2fa/verify
Check 60s Window & SHA256 Code Match
Replayed
Reject: 2FA Code Already Used
Backup Code Used? Consume One-Time Code
Valid OTP/Backup
Issue Final JWT Access & Refresh Tokens

Multi-Workspace Login Discovery

When an admin logs in from the platform domain (no pre-resolved tenant), the backend executes a 3-case routing algorithm. Case A uses a provided tenantId for strict domain isolation. Case A' triggers when the isPlatformAdmin flag is set — this directly looks up the platform admin (TenantId = null) and bypasses workspace discovery entirely, preventing an infinite loop. Case B performs workspace discovery: it first checks for a platform admin, then searches all tenants by email — returning a workspace picker if multiple matches are found.

CaseConditionBehavior
Case A — Tenant-ScopedtenantId is providedStrict domain isolation. Decrypts TenantId and queries exclusively for the admin in that tenant.
Case A' — Platform AdminisPlatformAdmin = trueBypasses workspace discovery. Queries for TenantId = null (platform level) to prevent workspace picker infinite loop.
Case B — DiscoveryNo tenantId, not isPlatformAdminStep 1: Check for platform admin. Step 2: Search all tenants by email. 0 matches -> invalid, 1 match -> direct login, N matches -> return workspace selection list.

OIDC/SSO Callback & Workspace Selection

For Single Sign-On (OIDC/SAML) callbacks, the system resolves the email and checks if multiple workspaces are linked. If there are multiple candidate workspaces, a temporary workspace selection token is generated and stored in a cache under `sso-login-selection:{token}`. The user is redirected to choose a workspace, which then dispatches `CompleteOidcWorkspaceSelectionCommand` to decrypt the selected tenant ID and issue final JWTs.

SSO Callback & Selection CacheC#
// Temporary selection cache setup in OIDC Callback:
var token = Guid.NewGuid().ToString("N");
await _cache.SetAsync($"sso-login-selection:{token}", new SsoTempLoginData
{
    Email = email,
    Provider = provider
}, TimeSpan.FromMinutes(10));

// Complete Workspace Selection command handler:
var loginData = await _cache.GetAsync<SsoTempLoginData>($"sso-login-selection:{request.Token}");
var targetTenantId = _idEncryption.Decrypt(request.TenantId);
var admin = allMatchingAdmins.FirstOrDefault(a => a.IsActive && a.TenantId == targetTenantId);

Password Expiry Enforcement

During login, after BCrypt verification and lockout checks, the handler invokes ITenantPasswordValidator to check if the admin's password has exceeded the tenant's PasswordExpiryDays setting. If expired, the response includes MustChangePassword = true, forcing the frontend to redirect the admin to the change-password page. The admin receives a valid JWT but cannot access the dashboard until the password is updated.

MustChangePassword Middleware & Whitelist

When a password expires, the user's JWT receives the `mcp: true` claim. The `MustChangePasswordMiddleware` blocks all incoming requests with 403 Forbidden unless the request matches the whitelist: password change endpoint `/api/v1/admins/{adminId}/change-password`, profile details `/api/v1/auth/admin/me`, token refresh `/api/v1/auth/admin/refresh`, session logouts `/api/v1/auth/admin/logout` or `/api/v1/auth/admin/revoke`, and the system `/health` endpoint.

MustChangePasswordMiddleware.cs Whitelist EnforcementC#
public async Task InvokeAsync(HttpContext context)
{
    if (context.User.Identity?.IsAuthenticated == true)
    {
        var mcpClaim = context.User.FindFirst("mcp")?.Value;
        if (mcpClaim == "true")
        {
            var path = context.Request.Path.Value ?? "";
            var isAllowed = AllowedPatterns.Any(pattern => pattern.IsMatch(path));

            if (!isAllowed)
            {
                context.Response.StatusCode = (int)HttpStatusCode.Forbidden;
                context.Response.ContentType = "application/json";
                await context.Response.WriteAsync("{\"error\":\"Password change required.\",\"code\":\"MustChangePassword\"}");
                return;
            }
        }
    }
    await _next(context);
}

SSO Tenant Suspension Gate

The ExternalLoginCommandHandler includes a tenant suspension security gate. After SSO token validation and account linking, the handler checks the admin's tenant status. If the tenant is Suspended or Cancelled, login is rejected with a localized error — preventing deactivated users from bypassing standard login checks via SSO providers like Google or Azure AD.

JWT Token Configuration

The system uses short-lived access tokens (15 minutes) with long-lived refresh tokens (7 days). Refresh tokens are rotated on each use to prevent reuse attacks.

Token TypeLifetimeStorageRotation / Purpose
Access Token (JWT)15 minutesMemory / CookieRefreshed automatically
Refresh Token7 daysHttpOnly CookieRotated on each use (one-time use)
2FA Temporary Token5 minutesMemoryDiscarded after 2FA verification

Token Validation & Claim Checks

Both authentication pipelines share the same JWT token validation process. The middleware verifies name identifier claims, checks the 'admin' flag, and queries the database bypassing global query filters using `IgnoreQueryFilters()` to verify the admin or user account is still active and has not been soft-deleted.

JwtBearerEvents.OnTokenValidated Pipeline EnforcementC#
OnTokenValidated = async context =>
{
    var idClaim = context.Principal?.FindFirst(ClaimTypes.NameIdentifier)?.Value;
    var isAdmin = context.Principal?.FindFirst("admin")?.Value == "true";

    if (string.IsNullOrEmpty(idClaim) || !Guid.TryParse(idClaim, out var id))
    {
        context.Fail("Invalid token: missing or invalid ID claim");
        return;
    }

    using var scope = context.HttpContext.RequestServices.CreateScope();
    var dbContext = scope.ServiceProvider.GetRequiredService<IdentityDbContext>();

    if (isAdmin)
    {
        var admin = await dbContext.Admins
            .IgnoreQueryFilters()
            .FirstOrDefaultAsync(a => a.Id == id && !a.IsDeleted, context.HttpContext.RequestAborted);

        if (admin == null || !admin.IsActive)
        {
            context.Fail("Admin no longer exists or is inactive");
        }
    }
    else
    {
        var user = await dbContext.Users
            .IgnoreQueryFilters()
            .FirstOrDefaultAsync(u => u.Id == id && !u.IsDeleted, context.HttpContext.RequestAborted);

        if (user == null || !user.IsActive)
        {
            context.Fail("User no longer exists or is inactive");
        }
    }
}

Dual Authentication (Admin & User)

SCRIPE has two separate authentication pipelines. Admin auth (AdminAuthController) issues JWTs with admin-specific claims (TenantId, IsSuperAdmin, Roles). User auth (UserAuthController) issues JWTs with user-specific claims (NationalId, Gender, Country). Each has its own login, register, and token refresh endpoints.

DimensionAdmin AuthenticationUser (Client) Authentication
API Route Prefix/api/v1/auth/admin/api/v1/auth/user
Controller FileAdminAuthController.csUserAuthController.cs
Primary CommandAdminLoginCommandUserLoginCommand
Domain EntityAdminUser
Token ClaimsSub (Id), Username, TenantId, HierarchyPath, MCPSub (Id), Username, TenantId
Permission SchemeServer-side cache IAdminPermissionCacheLean JWT (implied tenant scopes, no cache)
2FA SupportYes (TOTP + Backup Codes)Yes (TOTP + Backup Codes)
Lockout Policy5 attempts / 15 min lockout5 attempts / 15 min lockout

Admin Entity (Security Features)

The Admin entity has several security-critical fields that control account behavior and protection.

FieldTypePurpose
IsSuperAdminboolSystem-level access, bypasses tenant scoping
IsProtectedboolCannot be deleted, deactivated, or demoted (Guardian enforced)
IsLastSuperAdminInTenantboolSafety flag; blocks deletion/deactivation if true
PasswordLastChangedDateTime?Checked against TenantSettings.PasswordExpiryDays for rotation
UsernameLastChangedDateTime?30-day cooldown on username changes
IsTwoFactorEnabledboolWhether 2FA is active for this admin
TwoFactorSecretstring?Secret key for TOTP generation
BackupCodesJsonstring?JSON array of hashed backup codes
LastTwoFactorCodeUsedstring?Anti-replay: hash of last OTP code used
LastTwoFactorCodeUsedAtDateTime?Anti-replay: timestamp of last OTP use
FailedLoginAttemptsintCounter for lockout threshold
LockoutEndDateTime?When lockout expires

Anti-Replay Windowing & TOTP Security

To prevent token reuse attacks, the `Verify2FACommandHandler` implements a strict 60-second anti-replay window. It computes a SHA256 hash of the verification code and matches it against `LastTwoFactorCodeUsed` and checks `LastTwoFactorCodeUsedAt` timestamp. If the same code is used again within 60 seconds, it is rejected.

CheckAndTrackTotpReplay ImplementationC#
public bool CheckAndTrackTotpReplay(string code, Func<string, string> hashCode)
{
    var hashedCode = hashCode(code);
    if (LastTwoFactorCodeUsedAt.HasValue)
    {
        var timeSinceLastUse = DateTime.UtcNow - LastTwoFactorCodeUsedAt.Value;
        if (timeSinceLastUse.TotalSeconds < 60 && LastTwoFactorCodeUsed == hashedCode)
        {
            return false; // Code replayed within the 60-second window
        }
    }

    LastTwoFactorCodeUsed = hashedCode;
    LastTwoFactorCodeUsedAt = DateTime.UtcNow;
    return true;
}

Tenant-Scoped Password Policy

Password requirements are configurable per-tenant via TenantSettings. Each tenant can set minimum length, uppercase requirements, numbers, special characters, and expiry duration. The PasswordLastChanged field on the Admin entity is checked against the tenant's PasswordExpiryDays to enforce password rotation.

SettingDefaultDescription
MinPasswordLength8Minimum characters
RequireUppercasetrueMust contain A-Z
RequireNumbertrueMust contain 0-9
RequireSpecialCharactertrueMust contain !@#$%...
PasswordExpiryDays900 = never expire
LockoutThreshold5Failed attempts before lockout
LockoutDurationMinutes30Lockout duration
Require2FAfalseForce 2FA for all admins in tenant

Admin Auth Endpoints

MethodEndpointDescriptionAuth
POST/api/v1/auth/admin/loginUsername + password login with discovery option🔒 Required
POST/api/v1/auth/admin/refreshRotate refresh token and issue new JWT🔒 Required
POST/api/v1/auth/admin/discover-workspacesDiscover all workspaces associated with email🔒 Required
POST/api/v1/auth/admin/logoutRevoke current refresh token and clear session🔒 Required
GET/api/v1/auth/admin/sessionsList all active sessions for current admin🔒 Required
DELETE/api/v1/auth/admin/sessions/{tokenId}Revoke specific active session by ID🔒 Required
POST/api/v1/auth/admin/sessions/revoke-allRevoke all sessions except the current one🔒 Required
POST/api/v1/auth/admin/2fa/enableGenerate TOTP secret and QR code for activation🔒 Required
POST/api/v1/auth/admin/2fa/confirmConfirm 2FA setup by verifying code from authenticator🔒 Required
POST/api/v1/auth/admin/2fa/verifyVerify 2FA code during login process🔒 Required
POST/api/v1/auth/admin/2fa/disableDisable 2FA for the account🔒 Required
POST/api/v1/auth/admin/2fa/backup-codes/regenerateRegenerate a new set of backup codes🔒 Required
GET/api/v1/auth/admin/security-logGet security activity log for the current admin🔒 Required

User Auth Endpoints

MethodEndpointDescriptionAuth
POST/api/v1/auth/user/loginEmail/phone + password login🔒 Required
POST/api/v1/auth/user/login/{provider}External provider login (Google, Facebook, Apple, Microsoft)🔒 Required
POST/api/v1/auth/user/registerSelf-registration using TenantCode🔒 Required
POST/api/v1/auth/user/verify-emailVerify email address with OTP code🔒 Required
POST/api/v1/auth/user/verify-phoneVerify phone number with OTP code🔒 Required
POST/api/v1/auth/user/send-email-verificationSend or resend email verification OTP🔒 Required
POST/api/v1/auth/user/send-phone-verificationSend or resend phone verification OTP🔒 Required
POST/api/v1/auth/user/request-password-resetRequest password reset via email OTP🔒 Required
POST/api/v1/auth/user/reset-passwordReset password using OTP code🔒 Required
POST/api/v1/auth/user/refreshRotate refresh token and issue new JWT🔒 Required
POST/api/v1/auth/user/logoutRevoke current refresh token and log out🔒 Required

Rate Limiting

Authentication endpoints are protected by multiple rate limiting policies to prevent brute-force attacks and abuse.

Policy NameWindow TypeLimit ThresholdScope / Partition Key
GlobalFixed Window1000 requests per minuteServer-wide global DDoS ceiling
PerIpFixed Window200 requests per minuteClient IP Address
LoginFixed Window10 requests per 5 minutesBrute-force protection: login:{IP}
read-apiSliding Window200 requests per minuteInfinite loop/scraping protection: read:{UserId/IP}
mutation-apiSliding Window30 requests per minuteAutomated modification abuse: mutation:{UserId/IP}
per-userSliding Window100 requests per minuteUser-session activity ceiling: user:{UserId/IP}
export-heavyFixed Window5 requests per minuteHigh CPU reports/exports: export:{UserId/IP}
webhookSliding Window500 requests per minuteWebhook delivery burster: webhook:{IP}
signupFixed Window3 requests per hourRegistration/Tenant creation spam: signup:{IP}
phone-otp-sendFixed Window3 requests per 15 minutesSMS billing bombing prevention: phone-otp:{IP}
passkey-authFixed Window5 requests per 15 minutesWebAuthn brute-force protection: passkey:{IP}
qr-pollSliding Window60 requests per minuteSession polling limits: qr-poll:{IP}
password-resetFixed Window5 requests per 15 minutesRecovery enumeration protection: password-reset:{IP}
token-refreshSliding Window20 requests per minuteRefresh farming prevention: token-refresh:{IP}

Lockout Policies & brute-force limits

Brute-force protection sets `MaxFailedAttempts = 5` and a lockout duration of `LockoutMinutes = 15`. During a lockout, password validation is bypassed entirely to avoid timing attacks and CPU exhaustion, returning an immediate account locked error.