Start HereOrdered learning track

Auth Failure Taxonomy

Learn React Authentication, Authorization, Identity & Permission/ACL - Part 008

Taxonomy kegagalan auth untuk React apps: login failure, session expiry, revocation, OAuth callback failure, permission denied, tenant mismatch, stale privilege, network degradation, dan security incidents.

19 min read3619 words
PrevNext
Lesson 08130 lesson track01–24 Start Here
#react#authentication#authorization#failure-modeling+2 more

Auth Failure Taxonomy

Auth failure yang tidak diberi nama akan berubah menjadi bug acak.

Di banyak React app, hampir semua kegagalan auth dipaksa masuk ke satu bucket:

Something went wrong.

atau:

Please login again.

Akibatnya:

  • user login ulang padahal masalahnya 403 permission;
  • user melihat “no access” padahal session expired;
  • refresh token reuse dianggap network error;
  • tenant mismatch terlihat seperti data kosong;
  • provider SSO down dianggap credential salah;
  • callback OAuth invalid tidak diaudit sebagai potensi serangan;
  • frontend tidak tahu harus retry, redirect, logout, request access, atau freeze UI;
  • metrics tidak bisa membedakan bug UX dari security incident.

Part ini membangun taxonomy kegagalan auth untuk React application. Tujuannya bukan membuat error enum yang panjang. Tujuannya membuat failure bisa dipahami, dirutekan, diuji, dan dioperasikan.


1. Prinsip utama

Setiap auth failure harus punya enam properti:

1. Class      -> jenis kegagalan apa?
2. Stage      -> terjadi di tahap mana?
3. Actor      -> user, browser, IdP, API, policy engine, attacker, operator?
4. Recovery   -> apa langkah aman berikutnya?
5. Severity   -> UX issue, access issue, security signal, incident?
6. Evidence   -> apa yang harus dicatat untuk debugging/audit?

Tanpa ini, frontend akan menebak.

Contoh:

HTTP 401

Belum cukup.

Bisa berarti:

  • session tidak ada;
  • session expired;
  • refresh token expired;
  • session revoked;
  • cookie tidak terkirim;
  • clock skew;
  • CSRF defense menolak request;
  • BFF tidak bisa membaca session store;
  • token audience salah;
  • user berubah password di device lain;
  • organization memaksa re-auth.

Setiap penyebab membutuhkan response berbeda.


2. Failure taxonomy map

Taxonomy ini akan menjadi bahasa bersama antara frontend, backend, security, support, dan product.


3. Error envelope contract

Frontend tidak perlu seluruh detail internal backend. Tetapi frontend butuh error contract yang cukup spesifik.

Contoh response shape:

{
  "error": {
    "code": "session_expired",
    "category": "session",
    "message": "Your session has expired.",
    "recoverability": "reauth_required",
    "severity": "normal",
    "correlationId": "req_01J...",
    "details": {
      "reauthUrl": "/login",
      "returnToAllowed": true
    }
  }
}

Frontend membutuhkan code, category, recoverability, dan correlationId. message boleh dipakai untuk fallback, tetapi UI production biasanya memetakan message sendiri.

TypeScript:

type AuthErrorCategory =
  | 'authentication'
  | 'session'
  | 'oauth_callback'
  | 'authorization'
  | 'identity'
  | 'tenant'
  | 'transport'
  | 'dependency'
  | 'security_signal'
  | 'unknown';

type Recoverability =
  | 'retry_safe'
  | 'retry_after_delay'
  | 'reauth_required'
  | 'step_up_required'
  | 'request_access'
  | 'logout_required'
  | 'contact_admin'
  | 'blocked_security'
  | 'not_recoverable_in_ui';

type AuthErrorSeverity =
  | 'info'
  | 'normal'
  | 'warning'
  | 'security_sensitive'
  | 'incident';

type AuthErrorEnvelope = {
  code: string;
  category: AuthErrorCategory;
  message: string;
  recoverability: Recoverability;
  severity: AuthErrorSeverity;
  correlationId?: string;
  details?: Record<string, unknown>;
};

Rule:

Frontend should not infer security meaning from free-text message.

4. HTTP status is not enough

HTTP status membantu, tetapi tidak cukup.

HTTPGeneric meaningAuth-specific ambiguity
400Bad requestInvalid OAuth callback, malformed return URL, invalid tenant switch.
401UnauthenticatedMissing, expired, revoked, malformed, wrong audience, cookie missing.
403ForbiddenMissing permission, suspended account, tenant denied, step-up required.
404Not foundTruly missing or intentionally hidden due to authorization.
409ConflictSession version conflict, permission version changed, tenant switch race.
419/440Session expiredNon-standard but often used by frameworks/products.
422ValidationCredential policy, profile incomplete, impossible state transition.
429Too many requestsLogin throttling, MFA attempts, refresh abuse.
500Server errorSession store, policy engine, IdP integration failure.
503UnavailableIdP/policy/session dependency down.

Frontend harus menggunakan combination:

HTTP status + stable error code + endpoint context + current auth state

Contoh:

function classifyAuthHttpError(response: Response, body?: AuthErrorEnvelope): ClassifiedAuthFailure {
  if (body?.code) {
    return classifyByCode(body);
  }

  if (response.status === 401) {
    return { kind: 'session_unknown', recoverability: 'reauth_required' };
  }

  if (response.status === 403) {
    return { kind: 'authorization_denied', recoverability: 'request_access' };
  }

  if (response.status === 429) {
    return { kind: 'rate_limited', recoverability: 'retry_after_delay' };
  }

  if (response.status >= 500) {
    return { kind: 'dependency_or_server_failure', recoverability: 'retry_after_delay' };
  }

  return { kind: 'unknown', recoverability: 'not_recoverable_in_ui' };
}

5. Authentication failure

Authentication failure terjadi saat user membuktikan identitasnya.

Contoh:

CodeMeaningUI responseSecurity note
invalid_credentialsIdentifier/password tidak cocokGeneric login failureHindari user enumeration.
mfa_requiredCredential benar, MFA dibutuhkanShow MFA challengeJangan expose terlalu banyak detail.
mfa_failedMFA salahRetry with limitRate limit.
account_lockedAccount lockout policy aktifContact support/adminJangan leak detail ke attacker.
password_expiredPassword harus digantiPassword change flowBukan session failure.
email_unverifiedEmail belum verifiedVerification flowBisa login-limited.
passkey_challenge_failedWebAuthn ceremony gagalRetry ceremonyBisa karena device/platform.
identity_provider_requiredOrg harus login via SSORedirect SSOOrg discovery diperlukan.

Important UI rule:

Login form error should be helpful without becoming an account enumeration oracle.

Bad:

No account exists for alice@example.com.

Better:

The email or password is incorrect.

Untuk enterprise SSO, boleh ada UX lebih spesifik setelah org discovery aman, misalnya “Use your company SSO”. Tetapi jangan membocorkan membership sensitif sembarangan.


6. Session failure

Session failure terjadi setelah authentication pernah berhasil, tetapi continuity bermasalah.

CodeMeaningState transitionRecovery
session_missingTidak ada session/cookie/tokenanonymousLogin.
session_expiredSession/access token expiredrefreshing or expiredRefresh or re-auth.
refresh_token_expiredRefresh tidak bisa dipakaiexpiredRe-auth.
session_revokedServer mencabut sessionrevokedForced logout/re-auth.
refresh_token_reuse_detectedReuse token terdeteksirevokedSecurity logout, audit.
session_version_mismatchSession berubah di device/tab lainre-bootstrapReload auth context.
device_revokedDevice/session tertentu dicabutrevokedRe-auth.
password_changedPassword change invalidates sessionrevokedRe-auth.
cookie_missingCookie tidak terkirimanonymous or failedBrowser/settings/domain fix.
csrf_token_invalidCSRF defense menolak requestfailed or security signalReload/retry only if safe.

Perhatikan session_expired dan session_revoked berbeda.

Expired = normal lifecycle.
Revoked = explicit invalidation/security/admin/user action.

UI untuk expired:

Your session expired. Please sign in again.

UI untuk revoked:

Your session ended. Please sign in again.

Untuk security-sensitive revocation, jangan beri detail berlebihan ke browser. Detail penuh masuk audit/server log.


7. OAuth/OIDC callback failure

OAuth/OIDC callback adalah salah satu area paling rawan karena melibatkan redirect, transient state, code exchange, provider, dan browser history.

CodeMeaningSeverityRecovery
oauth_state_missingstate tidak adasecurity-sensitiveRestart login.
oauth_state_mismatchstate tidak cocoksecurity-sensitiveBlock, restart login, audit.
oauth_nonce_mismatchID token nonce tidak validsecurity-sensitiveBlock, audit.
oauth_code_missingCallback tanpa codenormal/warningRestart login.
oauth_code_exchange_failedBackend gagal exchange codenormal/warningRetry/restart login.
oauth_code_already_usedCode replay/duplicate callbackwarning/securityRestart login, audit.
oauth_redirect_uri_mismatchRedirect URI tidak cocokconfig/securityBlock, operator fix.
oauth_provider_errorProvider mengembalikan errornormalShow provider message class.
oauth_user_cancelledUser membatalkan consent/logininfoReturn to login.
oauth_org_not_allowedOrg/domain tidak diizinkanforbiddenContact admin/request access.

Callback handler harus punya state eksplisit:

authenticating -> handling_callback -> authenticated/loading_permissions/failed

Jangan callback langsung membuat isAuthenticated = true sebelum exchange/validation selesai.

Callback failure UI:

function CallbackError({ error }: { error: AuthErrorEnvelope }) {
  switch (error.code) {
    case 'oauth_state_mismatch':
    case 'oauth_nonce_mismatch':
      return <SecuritySensitiveLoginError />;

    case 'oauth_user_cancelled':
      return <LoginCancelled />;

    case 'oauth_provider_error':
      return <ProviderLoginError correlationId={error.correlationId} />;

    default:
      return <GenericLoginRecovery correlationId={error.correlationId} />;
  }
}

8. Authorization failure

Authorization failure terjadi saat user authenticated, tetapi tidak diizinkan melakukan action/resource tertentu.

CodeMeaningUI responseSecurity note
missing_permissionUser tidak punya permissionHide/disable/request accessServer tetap enforce.
resource_not_accessibleResource tidak boleh diakses403/404 depending policyHindari enumeration.
action_not_allowed_in_stateAction tidak valid untuk lifecycle resourceExplain current stateState-based auth.
field_read_deniedField tertentu tidak boleh dilihatOmit/mask fieldJangan kirim field jika tidak boleh.
field_write_deniedField tidak boleh dieditRead-only/hiddenServer validates.
tenant_membership_requiredUser bukan member tenantTenant access flowTenant isolation critical.
step_up_requiredAction butuh assurance lebih tinggiMFA/re-authBukan generic forbidden.
approval_requiredButuh approval workflowRequest approvalDomain policy.
delegation_requiredActing user tidak punya delegationExplain delegationAudit subject vs actor.

Frontend rule:

Authorization failure should not clear session.

Kecuali backend menyatakan account/session revoked, 403 tidak boleh otomatis logout.


9. 403 vs 404 untuk resource authorization

Untuk resource yang sensitif, backend kadang mengembalikan 404 agar attacker tidak tahu resource ada.

Frontend harus siap dengan dua mode:

403 = resource exists but access denied, if product wants explicit access UX.
404 = not found or hidden, if existence itself is sensitive.

Decision matrix:

Resource typePreferReason
Public-ish internal resource403User bisa request access.
Highly sensitive case/investigation404 or generic deniedExistence may be sensitive.
User-owned object404 for non-ownedPrevent enumeration.
Admin console route403User knows feature exists, lacks role.
Collaboration document403/request accessAccess request UX useful.

React UI jangan menganggap semua 404 berarti data hilang. Untuk route protected, 404 bisa menjadi authorization shape.


10. Identity/profile failure

Identity bukan hanya user ID. Ada status identitas yang mempengaruhi app.

CodeMeaningUI
profile_incompleteUser harus melengkapi profileOnboarding gate.
email_unverifiedEmail belum verifiedVerification prompt.
phone_verification_requiredPhone needed for compliance/MFAVerification flow.
account_suspendedAccount disabled/suspendedBlock with support/admin path.
account_pending_approvalWaiting approvalPending screen.
terms_acceptance_requiredMust accept termsTerms gate.
privacy_consent_requiredConsent requiredConsent flow.
identity_link_requiredAccount linking requiredLink account flow.

Jangan campur semua ini dengan authorization.

profile_incomplete != missing_permission
account_suspended != session_expired
email_unverified != unauthenticated

State machine boleh punya route-level gates:

authenticated -> identity_gate -> loading_permissions -> ready

Atau identity gate bisa menjadi bagian dari loading_permissions result. Yang penting: error code-nya jelas.


11. Tenant failure

Untuk SaaS dan case management platform, tenant failure adalah class tersendiri.

CodeMeaningRecovery
tenant_missingNo active tenant selectedTenant picker.
tenant_not_foundTenant slug/id invalid404/tenant picker.
tenant_membership_requiredUser bukan memberRequest membership/contact admin.
tenant_suspendedTenant disabledSupport/admin.
tenant_mismatchRequest tenant != session tenantClear cache/re-bootstrap/security log.
tenant_switch_failedSwitch endpoint gagalRetry.
tenant_context_staleClient context version lamaRe-fetch context.
cross_tenant_cache_detectedClient hampir menampilkan data tenant salahClear cache, audit client bug.

Tenant mismatch adalah high-risk.

Contoh bug:

URL: /tenant/acme/cases/123
Session active tenant: globex
Cached query key: ['case', '123']

Query key tidak memasukkan tenant. UI bisa menampilkan case tenant lama pada tenant baru.

Query key sehat:

const caseQueryKey = ['tenant', tenantId, 'case', caseId] as const;

Failure response sehat:

{
  "error": {
    "code": "tenant_mismatch",
    "category": "tenant",
    "recoverability": "retry_safe",
    "severity": "security_sensitive",
    "correlationId": "req_abc"
  }
}

Frontend response:

  • cancel in-flight tenant-scoped request;
  • clear tenant-scoped cache;
  • re-bootstrap session/tenant context;
  • do not render old data as current tenant;
  • log client-side diagnostic without sensitive data.

12. Permission freshness failure

Permission bukan hanya allowed/denied. Ia punya freshness.

CodeMeaningRecovery
permission_snapshot_staleClient permission version lamaRe-fetch permissions.
permission_version_conflictMutation memakai permission version lamaRe-fetch and retry only if safe.
role_changedRole updated since session/bootstrapRe-bootstrap permissions.
grant_revokedObject grant revokedRemove action/data.
policy_version_changedPolicy engine version changedRe-evaluate.

Contoh optimistic mutation:

async function deleteCase(caseId: string) {
  const decision = can('case.delete', { type: 'case', id: caseId });

  if (!decision.allowed) {
    throw new ForbiddenInUiError(decision.reason);
  }

  const response = await api.delete(`/cases/${caseId}`, {
    headers: {
      'X-Permission-Snapshot-Version': permissions.version,
    },
  });

  if (response.status === 409) {
    await reloadPermissions();
    throw new PermissionChangedError();
  }
}

Rule:

Permission allowed in UI is only a prediction. Server decision is final.

13. Transport/browser failure

Browser environment bisa membuat auth gagal tanpa ada masalah di credential atau permission.

CodeMeaningCommon causeRecovery
cookie_blockedCookie tidak tersimpan/dikirimThird-party cookie, SameSite, domain, browser settingExplain or fallback architecture.
cors_rejectedBrowser menolak responseCORS configOperator fix.
csrf_token_missingCSRF token hilangstale page, missing bootstrap, blocked cookieReload/retry safe.
csrf_token_invalidCSRF mismatchstale token, attack, multi-tabReload, audit if repeated.
network_offlineBrowser offlineconnectivityOffline/read-only.
request_timeoutTimeoutnetwork/dependencyRetry with backoff.
storage_unavailableStorage not availableprivate mode, quota, policyMemory-only/degraded.
broadcast_unavailableCross-tab sync unavailableold browser/restricted envPoll/re-bootstrap.
service_worker_staleSW serving stale app/sessionbad SW cacheUpdate/reload.

Frontend harus membedakan:

Auth denied by server
vs
Browser could not carry auth material correctly

Cookie problem example:

Login succeeds, redirect returns, /session says 401 because cookie was not stored.

Jangan tampilkan “wrong password”. Ini bukan authentication failure.


14. Dependency failure

Auth punya banyak dependency.

Dependency failure harus terlihat sebagai dependency failure, bukan user fault.

CodeDependencyUI response
identity_provider_unavailableIdPLogin temporarily unavailable.
session_store_unavailableSession DB/cacheCannot restore session; retry.
policy_service_unavailablePermission serviceBlock sensitive actions, retry.
directory_service_unavailableOrg/profileDegraded profile/tenant lookup.
audit_service_unavailableAuditPossibly block sensitive write.
key_service_unavailableJWKS/KMSCannot verify token, fail closed.

Security rule:

If a dependency required for safe authorization is unavailable, fail closed for protected action.

Product may allow read-only degraded mode only when data already loaded and compliance allows it.


15. Security signal failure

Beberapa “failure” bukan sekadar UX. Ia adalah signal security.

CodeMeaningFrontend behavior
refresh_token_reuse_detectedPossible token theft/replayStop, forced logout, generic message.
oauth_state_mismatchPossible CSRF/login injectionBlock, restart login.
oauth_nonce_mismatchPossible token substitution/replayBlock.
token_audience_invalidToken for wrong APIBlock, log correlation.
token_issuer_invalidWrong issuerBlock.
session_binding_mismatchSession/device binding changedRe-auth.
impossible_travel_detectedRisk engine signalStep-up/re-auth.
open_redirect_attemptMalicious return URLDrop return URL, audit.
suspicious_tenant_switchTenant switch anomalyBlock/step-up.

Frontend should not show attacker-friendly detail.

Bad:

OAuth state mismatch. Expected xyz but got abc.

Better:

We could not complete sign-in safely. Please try again.

With correlation ID:

Reference: req_abc123

16. Recovery matrix

Recovery should be deterministic.

RecoverabilityMeaningUI action
retry_safeSame operation can be retried safelyRetry button/auto retry with limit.
retry_after_delayRetry but with backoffToast/banner, backoff.
reauth_requiredUser must authenticate againRedirect login with safe returnTo.
step_up_requiredMore assurance neededMFA/re-auth challenge.
request_accessUser lacks permissionRequest access/contact admin.
logout_requiredSession must endForced logout cleanup.
contact_adminUser cannot self-recoverAdmin/support path.
blocked_securityPotential attack/security issueBlock flow, generic message.
not_recoverable_in_uiOperator/code/config issueError page + correlation ID.

Do not design recovery from scratch in every component. Centralize mapping.

function recoveryFor(error: AuthErrorEnvelope): RecoveryAction {
  switch (error.recoverability) {
    case 'retry_safe':
      return { type: 'retry' };

    case 'retry_after_delay':
      return { type: 'retry_with_backoff' };

    case 'reauth_required':
      return { type: 'redirect_login', returnTo: safeCurrentPath() };

    case 'step_up_required':
      return { type: 'start_step_up' };

    case 'request_access':
      return { type: 'show_request_access' };

    case 'logout_required':
      return { type: 'force_logout' };

    case 'contact_admin':
      return { type: 'show_contact_admin' };

    case 'blocked_security':
      return { type: 'block_with_generic_security_message' };

    case 'not_recoverable_in_ui':
      return { type: 'show_error_boundary' };
  }
}

17. UI mapping

Different failures deserve different surfaces.

Failure classUI surface
Invalid login credentialInline form error.
MFA failedInline challenge error.
Session expiredFull-page or modal re-auth.
Session revokedFull-page forced sign-in.
Permission deniedNo-access page or disabled action with reason.
Field permission deniedMask/omit/read-only.
Tenant mismatchClear shell/reload tenant context.
OAuth callback failedCallback error page.
Dependency outageError boundary/degraded banner.
Security signalGeneric safe block with correlation ID.

Avoid using toast for critical auth failure. Toasts disappear. Session revoked, forbidden route, tenant mismatch, and callback failures need stable UI.


18. Error boundary strategy

Auth error boundary should distinguish route-level failure.

export function AuthRouteErrorBoundary() {
  const error = useRouteError();
  const classified = classifyRouteError(error);

  switch (classified.kind) {
    case 'unauthenticated':
      return <RedirectToLogin returnTo={classified.returnTo} />;

    case 'session_expired':
      return <SessionExpiredScreen />;

    case 'forbidden':
      return <NoAccessScreen reason={classified.reason} />;

    case 'tenant_mismatch':
      return <TenantRecoveryScreen />;

    case 'dependency_unavailable':
      return <TemporaryUnavailableScreen correlationId={classified.correlationId} />;

    default:
      return <UnexpectedAuthError correlationId={classified.correlationId} />;
  }
}

Component-level permission failure should usually not throw route boundary. Route-level authorization failure should.

Route denied -> route error boundary.
Button denied -> render absent/disabled/request-access.
Mutation denied -> mutation error UI + permission refresh.

19. API endpoint failure semantics

Different endpoint classes should return different auth semantics.

EndpointAuth failure expectation
GET /session200 session or 401 no/expired session.
POST /session/refresh200 refreshed or 401/403 revoked/expired/reuse.
DELETE /sessionIdempotent 204 even if already gone.
GET /me200 user or 401.
GET /permissions200 snapshot, 403 tenant/account forbidden, 503 dependency.
GET /resource/:id200, 401, 403/404 depending policy.
POST /resource/:id/action200/204, 401, 403, 409 permission/resource state conflict.
POST /tenant/switch200 context, 403 membership, 404 tenant hidden, 409 stale.
OAuth callback exchange200 session, 400 invalid callback, 403 org denied, 503 provider.

Endpoint semantics should be documented. Frontend cannot classify correctly if backend returns random status codes.


20. Mutating request failure

Mutation failures deserve special handling.

Example:

User clicks Approve Case.
UI predicted allowed.
Server returns 403 action_not_allowed_in_state.

Possible cause:

  • another user changed case state;
  • permission changed;
  • stale UI;
  • user lost role;
  • backend state machine rejected transition;
  • action requires step-up.

Frontend response:

  1. Stop optimistic UI.
  2. Roll back local state.
  3. Re-fetch resource and permission snapshot.
  4. Show specific reason if safe.
  5. Do not retry automatically unless server says retry-safe.

Example:

onError(error, variables, context) {
  const authFailure = classifyAuthFailure(error);

  if (authFailure?.code === 'action_not_allowed_in_state') {
    rollback(context);
    queryClient.invalidateQueries({ queryKey: ['case', variables.caseId] });
    queryClient.invalidateQueries({ queryKey: ['permissions'] });
    showActionNoLongerAvailable();
    return;
  }

  if (authFailure?.recoverability === 'step_up_required') {
    startStepUpFlow({ returnToAction: variables });
    return;
  }

  throw error;
}

21. Auth failure and cache invalidation

Auth failure often means cache must change.

FailureCache action
session_expiredPause protected queries; maybe keep non-sensitive cache until re-auth policy decides.
session_revokedClear sensitive cache immediately.
logout_requiredClear all user/tenant-scoped cache.
tenant_mismatchClear tenant-scoped cache.
permission_snapshot_staleInvalidate permission and affected resource queries.
grant_revokedRemove or re-fetch affected resource.
account_suspendedClear protected cache; show blocked state.
policy_service_unavailableMark permission unknown; fail closed for actions.

Never let cached permission mask backend denial.

Cache is an optimization, not an authority.

22. Auth failure and redirect

Redirect is dangerous if mixed with untrusted return URL.

Error recovery may include:

redirect to login with returnTo

But returnTo must be internal and validated.

function safeReturnTo(raw: string | null, origin: string): string {
  if (!raw) return '/';

  try {
    const url = new URL(raw, origin);

    if (url.origin !== origin) return '/';
    if (!url.pathname.startsWith('/')) return '/';
    if (url.pathname.startsWith('//')) return '/';

    return `${url.pathname}${url.search}${url.hash}`;
  } catch {
    return '/';
  }
}

Failure class open_redirect_attempt should drop the return URL, not try to “fix” it.


23. Auth failure and audit

Not every auth failure is audit-worthy. But some are.

EventAudit?Why
Login successYesIdentity access trail.
Invalid passwordUsually aggregate/rate-limitAbuse detection.
MFA failed repeatedlyYes/securityAccount protection.
Session expiredUsually metric, not auditNormal lifecycle.
Session revokedYesSecurity/admin/user action.
Refresh token reuseYes/securityPossible compromise.
OAuth state mismatchYes/securityPossible CSRF/login injection.
Permission denied for sensitive resourceOften yesAccess attempt trail.
Tenant mismatchYes/securityIsolation risk.
Open redirect attemptYes/securityAttack signal.
Policy service unavailableOperational eventIncident/debugging.

Frontend audit should be limited. Server is better positioned to audit authoritative events. Frontend can send telemetry for client-only signals, but should avoid sending secrets.


24. Observability schema

Client-side event example:

{
  "event": "auth_failure_observed",
  "category": "authorization",
  "code": "missing_permission",
  "recoverability": "request_access",
  "surface": "route_loader",
  "routeId": "case-detail",
  "tenantId": "tnt_123",
  "correlationId": "req_abc",
  "authState": "ready"
}

Do not log:

  • token values;
  • authorization code;
  • full callback URL if it contains sensitive parameters;
  • password/MFA code;
  • full cookie;
  • raw ID token claims unless explicitly reviewed;
  • sensitive resource content.

Useful metrics:

auth.failure.count{category,code,surface}
auth.failure.recovery.count{recoverability}
auth.session.expired.count
auth.session.revoked.count
auth.oauth.callback_failed.count{code}
auth.authorization.denied.count{action,resourceType}
auth.tenant.mismatch.count
auth.security_signal.count{code}
auth.dependency.unavailable.count{dependency}

25. Classification function

Centralize classification.

type ClassifiedAuthFailure = {
  code: string;
  category: AuthErrorCategory;
  recoverability: Recoverability;
  severity: AuthErrorSeverity;
  transition?: AuthEvent;
  cacheAction?: 'none' | 'clear_user' | 'clear_tenant' | 'invalidate_permissions';
  uiSurface: 'inline' | 'modal' | 'route_error' | 'full_page' | 'silent_metric';
};

function classifyAuthFailure(error: unknown): ClassifiedAuthFailure {
  const envelope = parseAuthErrorEnvelope(error);

  if (!envelope) {
    return {
      code: 'unknown_auth_error',
      category: 'unknown',
      recoverability: 'not_recoverable_in_ui',
      severity: 'warning',
      cacheAction: 'none',
      uiSurface: 'route_error',
    };
  }

  switch (envelope.code) {
    case 'session_expired':
      return {
        code: envelope.code,
        category: 'session',
        recoverability: 'reauth_required',
        severity: 'normal',
        transition: { type: 'REFRESH_EXPIRED', expiredAt: Date.now() },
        cacheAction: 'none',
        uiSurface: 'full_page',
      };

    case 'session_revoked':
      return {
        code: envelope.code,
        category: 'session',
        recoverability: 'logout_required',
        severity: 'security_sensitive',
        transition: { type: 'SESSION_REVOKED', reason: 'server_revoked' },
        cacheAction: 'clear_user',
        uiSurface: 'full_page',
      };

    case 'missing_permission':
      return {
        code: envelope.code,
        category: 'authorization',
        recoverability: 'request_access',
        severity: 'normal',
        cacheAction: 'none',
        uiSurface: 'route_error',
      };

    case 'tenant_mismatch':
      return {
        code: envelope.code,
        category: 'tenant',
        recoverability: 'retry_safe',
        severity: 'security_sensitive',
        cacheAction: 'clear_tenant',
        uiSurface: 'full_page',
      };

    case 'permission_snapshot_stale':
      return {
        code: envelope.code,
        category: 'authorization',
        recoverability: 'retry_safe',
        severity: 'normal',
        cacheAction: 'invalidate_permissions',
        uiSurface: 'inline',
      };

    case 'oauth_state_mismatch':
      return {
        code: envelope.code,
        category: 'oauth_callback',
        recoverability: 'blocked_security',
        severity: 'security_sensitive',
        cacheAction: 'clear_user',
        uiSurface: 'full_page',
      };

    default:
      return {
        code: envelope.code,
        category: envelope.category,
        recoverability: envelope.recoverability,
        severity: envelope.severity,
        cacheAction: 'none',
        uiSurface: 'route_error',
      };
  }
}

Klasifikasi ini bukan hanya untuk UI. Ia menghubungkan error ke state machine, cache policy, dan telemetry.


26. State machine mapping

Auth failure harus mengubah state dengan jelas.

FailureAuth state event
session_missingBOOTSTRAP_NO_SESSION or RESET
session_expiredREFRESH_EXPIRED
session_revokedSESSION_REVOKED
refresh_token_reuse_detectedSESSION_REVOKED with security reason
oauth_state_mismatchCALLBACK_FAILED with security error
missing_permissionRoute-level forbidden or PERMISSIONS_DENIED
tenant_membership_requiredTENANT_SWITCH_FORBIDDEN or PERMISSIONS_DENIED
policy_service_unavailableRECOVERABLE_FAILURE degraded
identity_provider_unavailableFAILURE during login or degraded login
network_offlineRECOVERABLE_FAILURE degraded

No floating auth error.

Every auth failure must either become state, route error, mutation error, or telemetry-only event.

27. Example: route loader failure

Conceptual route loader:

export async function caseLoader({ request, params }: LoaderFunctionArgs) {
  const session = await requireSession(request);

  if (!session) {
    throw jsonAuthError(401, {
      code: 'session_missing',
      category: 'session',
      recoverability: 'reauth_required',
      severity: 'normal',
    });
  }

  const decision = await can(session, 'case.read', {
    type: 'case',
    id: params.caseId!,
  });

  if (!decision.allowed) {
    throw jsonAuthError(decision.hideExistence ? 404 : 403, {
      code: decision.code,
      category: 'authorization',
      recoverability: decision.requestable ? 'request_access' : 'contact_admin',
      severity: 'normal',
    });
  }

  return loadCase(params.caseId!);
}

Boundary behavior:

function CaseRouteBoundary() {
  const error = useRouteError();
  const failure = classifyAuthFailure(error);

  if (failure.code === 'session_missing') {
    return <LoginRedirect />;
  }

  if (failure.category === 'authorization') {
    return <NoAccess resource="case" />;
  }

  return <RouteError correlationId={getCorrelationId(error)} />;
}

28. Example: mutation failure

function ApproveCaseButton({ caseId }: { caseId: string }) {
  const mutation = useMutation({
    mutationFn: () => approveCase(caseId),
    onError(error) {
      const failure = classifyAuthFailure(error);

      if (failure.code === 'step_up_required') {
        startStepUp({ action: 'case.approve', resourceId: caseId });
        return;
      }

      if (failure.code === 'action_not_allowed_in_state') {
        queryClient.invalidateQueries({ queryKey: ['case', caseId] });
        showToast('This case can no longer be approved.');
        return;
      }

      if (failure.category === 'authorization') {
        queryClient.invalidateQueries({ queryKey: ['permissions'] });
        showNoAccessDialog();
        return;
      }

      throw error;
    },
  });

  return <button onClick={() => mutation.mutate()}>Approve</button>;
}

Do not automatically replay mutating requests after auth failure unless the action is idempotent and the backend explicitly supports it.


29. Common anti-patterns

Anti-pattern: all 401 means logout

if (response.status === 401) logout();

Better:

401 session_expired -> refresh or re-auth
401 session_revoked -> forced logout
401 cookie_missing -> browser/config recovery
401 token_audience_invalid -> security/config failure

Anti-pattern: all 403 means hide page silently

Silently hiding can make support impossible.

Better:

Route-level 403 -> no access page with request/access path when safe.
Component-level denial -> hidden/disabled with reason when useful.
Mutation-level 403 -> rollback + refresh permission/resource.

Anti-pattern: OAuth callback failure returns to dashboard

Callback failure must not fall through to previous app state.

Anti-pattern: network failure treated as auth failure

Network timeout does not prove user is logged out.

Anti-pattern: token decode failure in frontend becomes “not admin”

Token decode failure is not permission denial. It is invalid auth material or implementation mismatch.

Anti-pattern: backend returns plain text error

Frontend cannot build safe UX or telemetry from Forbidden alone.


30. Failure review checklist

Untuk setiap auth failure, pastikan ada jawaban:

1. Apa stable error code-nya?
2. Apa category-nya?
3. Apa HTTP status yang sesuai?
4. Apakah frontend boleh retry?
5. Apakah user perlu login ulang?
6. Apakah user bisa request access?
7. Apakah cache harus dibersihkan?
8. Apakah permission harus di-refresh?
9. Apakah session harus dicabut?
10. Apakah event ini perlu audit?
11. Apakah message aman dari enumeration/leak?
12. Apakah correlation ID tersedia?
13. Apakah route/component/mutation punya UI berbeda?
14. Apakah test case sudah ada?
15. Apakah failure ini security signal?

Jika error tidak bisa dijawab dengan checklist ini, taxonomy belum cukup matang.


31. Testing failure taxonomy

Test bukan hanya happy path.

Minimal test matrix:

ScenarioExpected
/session returns session_missinganonymous/login.
API returns session_expiredrefresh/re-auth path.
refresh returns session_revokedrevoked state + clear cache.
refresh returns refresh_token_reuse_detectedsecurity-sensitive forced logout.
route loader returns missing_permissionno-access route boundary.
mutation returns permission_snapshot_staleinvalidate permission and show retry.
callback returns oauth_state_mismatchblock login, generic safe message.
tenant switch returns tenant_membership_requiredforbidden/request access.
API returns policy_service_unavailabledegraded/fail closed.
browser offline during refreshdegraded, bounded retry.

Example unit test:

describe('classifyAuthFailure', () => {
  it('classifies session revocation as logout required and cache clearing', () => {
    const failure = classifyAuthFailure(makeError({
      code: 'session_revoked',
      category: 'session',
      recoverability: 'logout_required',
      severity: 'security_sensitive',
    }));

    expect(failure.recoverability).toBe('logout_required');
    expect(failure.cacheAction).toBe('clear_user');
    expect(failure.severity).toBe('security_sensitive');
  });

  it('does not treat forbidden as unauthenticated', () => {
    const failure = classifyAuthFailure(makeError({
      code: 'missing_permission',
      category: 'authorization',
      recoverability: 'request_access',
      severity: 'normal',
    }));

    expect(failure.category).toBe('authorization');
    expect(failure.recoverability).not.toBe('reauth_required');
  });
});

32. Production readiness signal

Auth failure handling dianggap matang jika:

  • 401/403/404/409/429/503 punya meaning konsisten;
  • backend mengirim stable error code;
  • frontend punya central classifier;
  • session failure mengubah auth state machine;
  • authorization failure tidak otomatis logout;
  • permission stale menginvalidate permission cache;
  • tenant mismatch membersihkan tenant-scoped cache;
  • OAuth callback failure tidak pernah silently ignored;
  • security-sensitive failure tidak menampilkan detail teknis;
  • correlation ID terlihat di UI error page;
  • metrics membedakan failure category;
  • tests mencakup failure matrix;
  • support bisa menelusuri error dari correlation ID;
  • sensitive data tidak masuk log client.

33. Ringkasan Phase 1

Sampai Part 008, kita sudah membangun foundation:

Part 001 -> Auth is not login.
Part 002 -> Authentication, authorization, identity adalah concern berbeda.
Part 003 -> React auth architecture map.
Part 004 -> Frontend bukan enforcement boundary.
Part 005 -> Browser threat model.
Part 006 -> Auth system invariants.
Part 007 -> Auth state machine.
Part 008 -> Auth failure taxonomy.

Phase 1 belum membahas implementasi OAuth/OIDC, cookie/token, React Router detail, RBAC/ABAC/ReBAC, atau permission-aware component secara penuh. Itu sengaja. Tanpa foundation ini, implementasi lanjut hanya menjadi kumpulan snippet yang terlihat benar tetapi rapuh.

Mulai Part 009, kita masuk Phase 2: Browser, Session, Token, Cookie.


34. Final mental model

Auth failure harus diperlakukan seperti failure dalam distributed system.

Ia punya:

  • source;
  • boundary;
  • state transition;
  • retry policy;
  • cache impact;
  • user recovery;
  • audit implication;
  • security severity.

Jika semua failure hanya menjadi error: true, React app akan gagal secara tidak deterministik.

Jika failure diberi taxonomy, sistem menjadi bisa dipahami:

failure -> classified -> state transition -> recovery -> telemetry -> test

Itulah perbedaan antara auth yang sekadar bisa login dan auth yang production-grade.


References

Lesson Recap

You just completed lesson 08 in start here. Use the series map if you want to review the broader track, or continue directly into the next lesson while the context is still warm.

Continue The Track

Keep the momentum while the lesson is still fresh. Move backward for review or continue forward into the next concept.