Start HereOrdered learning track

Authentication vs Authorization vs Identity

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

Membedah perbedaan authentication, authorization, identity, account, principal, session, role, permission, scope, claim, dan tenant agar model auth React tidak ambigu.

18 min read3517 words
PrevNext
Lesson 02130 lesson track01–24 Start Here
#react#authentication#authorization#identity+2 more

Part 002 — Authentication vs Authorization vs Identity

Part 001 membangun tesis utama: auth bukan login. Sekarang kita pecah istilah yang paling sering dicampur: authentication, authorization, identity, account, user, principal, session, token, role, permission, scope, claim, tenant, dan membership.

Ini bukan debat terminologi. Di sistem production, istilah yang kabur berubah menjadi bug.

Contoh bug karena istilah kabur:

  • user.role === "admin" memberi akses lintas tenant;
  • JWT scope dipakai sebagai permission domain aplikasi;
  • email dipakai sebagai stable user ID;
  • profile dari IdP dianggap account internal;
  • authenticated user dianggap otomatis authorized;
  • logout aplikasi dianggap sama dengan logout IdP;
  • role global dipakai untuk resource-level workflow;
  • permission cache tidak dibersihkan saat tenant switch;
  • session valid tetapi membership tenant sudah revoked.

Tujuan part ini: setelah membaca, kamu harus bisa melihat objek auth bukan sebagai blob user, tetapi sebagai model berlapis.


1. Satu flow, banyak konsep

Bayangkan Ana membuka aplikasi case management:

  1. Ana menekan Login with Company SSO.
  2. Identity Provider memverifikasi Ana lewat password + MFA.
  3. Backend menerima OIDC callback dan membuat application session.
  4. Backend menemukan bahwa subject IdP 00uabc123 terhubung ke account internal acct_789.
  5. Account itu punya membership pada tenant REGULATOR-JKT sebagai senior_investigator.
  6. Ana membuka /cases/CASE-123.
  7. Backend mengecek apakah acct_789 boleh membaca case tersebut.
  8. Backend mengirim case data yang sudah difilter dan allowedActions.
  9. React menampilkan tombol yang sesuai.
  10. Ana menekan Approve.
  11. Backend mengecek lagi permission action case.approve, termasuk state case, role, region, conflict-of-interest, dan MFA assurance.

Kalau ditulis sebagai diagram:

Dalam flow ini ada banyak konsep:

  • authentication terjadi di IdP/backend;
  • identity claims datang dari IdP;
  • account internal di-load dari database aplikasi;
  • session dibuat oleh backend;
  • authorization dievaluasi oleh policy layer;
  • React hanya mengonsumsi state dan decision contract.

Kalau semua disimpan dalam user, desain kehilangan presisi.


2. Authentication: membuktikan klaim identitas

Authentication menjawab:

Apakah entitas ini benar-benar siapa yang ia klaim?

OWASP mendefinisikan authentication sebagai proses memverifikasi bahwa individu, entitas, atau website adalah seperti yang diklaim, dengan menentukan validitas satu atau lebih authenticator seperti password, biometric, atau security token.

Di aplikasi React, authentication bisa muncul sebagai:

  • username/password form;
  • magic link;
  • social login;
  • OIDC authorization code with PKCE;
  • enterprise SSO;
  • passkey/WebAuthn;
  • MFA challenge;
  • step-up authentication sebelum action sensitif;
  • re-authentication setelah idle timeout.

2.1 React jarang menjadi verifier final

React bisa mengumpulkan input atau mengarahkan flow, tetapi React tidak boleh menjadi verifier final.

Buruk:

if (password === "admin123") {
  setIsAuthenticated(true);
}

Jelas ini contoh ekstrem. Tetapi versi production yang lebih halus juga berbahaya:

const decoded = decodeJwt(token);
setUser(decoded);
setIsAuthenticated(true);

Decode bukan validate. Dan validate token di browser pun bukan pengganti session/policy server.

2.2 Authentication menghasilkan confidence, bukan permission

Setelah authentication berhasil, sistem tahu bahwa subject tertentu telah diverifikasi dengan metode tertentu.

Tetapi belum tentu subject itu boleh melakukan apa pun.

Contoh:

  • user berhasil login tetapi account disabled;
  • user berhasil SSO tetapi belum punya membership tenant;
  • user berhasil MFA tetapi tidak punya permission approve;
  • user login sebagai contractor tetapi contract expired;
  • user login dari device yang tidak memenuhi policy;
  • user login tetapi resource berada di region lain.

Authentication adalah input. Authorization adalah decision.


3. Identity: representasi siapa subjek di sistem

Identity menjawab:

Siapa entitas ini dalam namespace tertentu?

Identity tidak selalu sama dengan user record aplikasi.

Contoh identity dari IdP:

{
  "iss": "https://idp.company.example",
  "sub": "00uabc123",
  "email": "ana@company.example",
  "name": "Ana Wijaya"
}

Contoh account internal:

{
  "accountId": "acct_789",
  "externalIdentities": [
    {
      "provider": "company-sso",
      "issuer": "https://idp.company.example",
      "subject": "00uabc123"
    }
  ],
  "status": "active",
  "createdAt": "2025-02-10T10:12:00+07:00"
}

Keduanya tidak sama.

3.1 External identity vs internal account

KonsepDimiliki olehStabilitasDigunakan untuk
External identityIdPStabil dalam issuer tertentu, tetapi bisa berubah tergantung providerFederation/login mapping.
Internal accountAplikasiHarus stabil untuk domain aplikasiOwnership, audit, authorization, preferences.
ProfileIdP atau aplikasiBisa berubahDisplay name, avatar, email.
MembershipAplikasi/domainBisa berubahTenant/org/project access.

Jangan jadikan email sebagai primary key authorization. Email bisa berubah, bisa didaur ulang, bisa memiliki case-sensitivity/normalization issue, dan tidak selalu unik lintas provider.

Gunakan stable internal account ID untuk domain authorization.


4. Authorization: memutuskan akses

Authorization menjawab:

Apakah subject boleh melakukan action terhadap resource dalam context ini?

Formula yang akan kita pakai:

decision = can(subject, action, resource, context)

Contoh:

can({
  subject: { accountId: "acct_789" },
  action: "case.approve",
  resource: { type: "case", id: "CASE-123" },
  context: {
    tenantId: "REGULATOR-JKT",
    assuranceLevel: "mfa",
    requestTime: "2026-07-07T09:00:00+07:00",
  },
});

Authorization bukan:

user.role === "admin"

Itu hanya salah satu kemungkinan input untuk authorization.

4.1 Subject

Subject adalah aktor yang meminta akses.

Subject bisa berupa:

  • human user;
  • service account;
  • background worker;
  • API client;
  • admin impersonating another user;
  • delegated actor;
  • organization;
  • device;
  • anonymous visitor.

Di React app, subject biasanya human user, tetapi sistem enterprise sering butuh delegated subject.

Contoh:

type Subject =
  | { kind: "user"; accountId: string }
  | { kind: "service"; serviceAccountId: string }
  | {
      kind: "impersonation";
      actorAccountId: string;
      effectiveAccountId: string;
      reason: string;
    };

Impersonation tidak boleh direpresentasikan sebagai “ganti user saja”. Actor dan effective subject harus tetap terlihat untuk audit dan UI.

4.2 Action

Action adalah capability domain, bukan label tombol.

Buruk:

"showApproveButton"

Lebih baik:

"case.approve"

Lebih spesifik jika perlu:

"case.approve.enforcementRecommendation"
"case.export.redactedPdf"
"case.export.fullEvidenceBundle"

Action harus stabil, testable, dan domain-oriented.

4.3 Resource

Resource adalah target akses.

type ResourceRef =
  | { type: "case"; id: string }
  | { type: "case.comment"; id: string; parentCaseId: string }
  | { type: "tenant"; id: string }
  | { type: "user"; id: string }
  | { type: "report"; id: string };

Resource tidak selalu object tunggal. Kadang resource adalah collection:

{ type: "case.collection", tenantId: "REGULATOR-JKT" }

Authorization untuk list query berbeda dengan authorization untuk object detail. List query sering butuh filter, bukan boolean.

4.4 Context

Context adalah informasi tambahan yang mempengaruhi decision.

Contoh:

  • tenant aktif;
  • region;
  • resource state;
  • time;
  • assurance level;
  • network zone;
  • device trust;
  • emergency mode;
  • purpose of access;
  • request origin;
  • workflow step;
  • conflict-of-interest flag.

Context sering menjadi pembeda antara sistem auth sederhana dan sistem auth yang benar-benar enterprise-grade.


5. Session: continuity setelah authentication

Session menjawab:

Bagaimana sistem menghubungkan request berikutnya dengan authentication yang sudah terjadi?

OWASP Session Management menjelaskan session ID/token sebagai pengikat authenticated user session dengan HTTP traffic dan access control yang ditegakkan aplikasi.

Session bisa berbentuk:

  • cookie berisi session ID;
  • cookie berisi encrypted session;
  • access token bearer;
  • access token + refresh token;
  • BFF session;
  • mTLS-bound token;
  • device-bound session.

5.1 Session bukan identity

Session adalah instance login.

Satu account bisa punya banyak session:

Account acct_789
  ├─ session s_1: Chrome laptop office
  ├─ session s_2: Safari phone
  └─ session s_3: Firefox home

Logout satu session tidak selalu berarti logout semua session. Password reset mungkin harus revoke semua. Admin force logout mungkin target session tertentu atau semua session user.

5.2 Session bukan permission

Session valid tidak berarti semua permission valid.

Permission bisa berubah saat session masih aktif:

  • role dicabut;
  • tenant membership expired;
  • resource reassigned;
  • case state berubah;
  • policy version berubah;
  • emergency access expired.

Karena itu permission snapshot perlu TTL/invalidation, dan backend harus check ulang.


6. Token: artefak transport, bukan domain model

Token sering membuat frontend auth tampak sederhana:

localStorage.setItem("access_token", token);

Tetapi token adalah artefak transport/security protocol, bukan domain model aplikasi.

6.1 ID token

Dalam OIDC, ID token merepresentasikan authentication event dan identity claims untuk client/relying party. ID token bukan token untuk memanggil API resource server.

Frontend sering salah memakai ID token sebagai access token.

6.2 Access token

Access token dipakai untuk mengakses resource server/API. Access token punya audience dan scope. Resource server yang harus memvalidasi access token.

6.3 Refresh token

Refresh token dipakai untuk mendapatkan access token baru. Di browser-based app, refresh token harus diperlakukan sangat hati-hati: rotation, reuse detection, lifetime, storage, dan threat model XSS sangat penting.

6.4 Token claims bukan permission final

JWT bisa berisi claims seperti:

{
  "sub": "00uabc123",
  "scope": "openid profile email api:read",
  "tenant": "REGULATOR-JKT",
  "roles": ["senior_investigator"]
}

Claims ini bisa membantu bootstrap. Tetapi untuk domain authorization yang kompleks, claims sering tidak cukup.

Masalah:

  • stale sampai token expiry;
  • tidak tahu current resource state;
  • tidak tahu conflict-of-interest terbaru;
  • tidak tahu object-level grant terbaru;
  • tidak tahu policy version terbaru;
  • tidak tahu tenant switch jika token terlalu global.

Gunakan claims sebagai input, bukan sebagai keputusan final.


7. Role, permission, scope, claim, entitlement: jangan dicampur

Ini area yang sering rusak.

7.1 Role

Role adalah grouping responsibility atau grouping permission.

Contoh:

senior_investigator
legal_reviewer
tenant_admin
read_only_auditor

Role bagus untuk administrasi manusia. Role buruk jika dipakai sebagai satu-satunya decision.

7.2 Permission

Permission adalah capability untuk action tertentu.

case.read
case.comment
case.assign
case.approve
case.export.redacted
case.export.full
user.invite
user.disable
tenant.settings.update

Permission lebih dekat ke UI dan API contract.

7.3 Scope

Scope dalam OAuth adalah delegated access boundary untuk client/API. Scope bukan selalu sama dengan permission domain aplikasi.

Contoh OAuth scope:

openid profile email api:read api:write

Contoh domain permission:

case.approve
case.viewConfidentialNotes
case.exportEvidenceBundle

Jangan otomatis memetakan api:write ke semua write action di domain.

7.4 Claim

Claim adalah statement tentang subject/token.

Contoh:

{
  "email_verified": true,
  "acr": "urn:mfa",
  "department": "enforcement"
}

Claim bisa menjadi attribute dalam ABAC, tetapi bukan decision final.

7.5 Entitlement

Entitlement biasanya berarti hak akses atau paket kemampuan yang diberikan kepada user/org, sering di SaaS/commercial domain.

Contoh:

feature.advanced_reporting
plan.enterprise_audit_log
quota.case_export_per_day=100

Entitlement bisa beririsan dengan feature flag dan permission, tetapi tujuannya beda.


8. Tabel perbedaan inti

IstilahMenjawabContohKesalahan umum
AuthenticationApakah klaim identity valid?Password+MFA, OIDC loginMenganggap login sukses berarti boleh akses semua.
IdentitySiapa subject dalam namespace tertentu?IdP sub, internal account IDPakai email sebagai ID stabil.
SessionBagaimana login diingat?Cookie session, access tokenMenganggap session valid berarti permission valid.
AuthorizationBoleh melakukan apa?can(case.approve, CASE-123)Role check global.
RoleGrup responsibility/permissiontenant_adminRole explosion, role dipakai tanpa context.
PermissionCapability granularcase.export.fullPermission terlalu global tanpa resource.
ScopeDelegated OAuth/API accessapi:readDisamakan dengan domain permission.
ClaimPernyataan tentang subject/tokenemail_verified, acrDipercaya tanpa memahami issuer/audience/staleness.
ACLGrant pada objectUser X viewer pada doc YTidak menangani inheritance/revocation.
Tenant MembershipHubungan account dengan org/tenantowner/member/auditorTenant switch dianggap hanya UI filter.

9. Principal: object yang sebaiknya dikonsumsi React

Daripada memberi React semua raw identity/account/session info, lebih baik berikan Principal yang sudah dipetakan.

type Principal = {
  subjectId: string;
  accountId: string;
  displayName: string;
  primaryEmail?: string;
  activeTenant?: {
    tenantId: string;
    displayName: string;
    membershipId: string;
    membershipStatus: "active" | "suspended" | "expired";
  };
  assurance: {
    level: "anonymous" | "password" | "mfa" | "phishing_resistant";
    authenticatedAt?: string;
    methods?: string[];
  };
};

Principal adalah representation yang aman dan cukup untuk frontend. Tidak semua IdP claims perlu diekspos ke React.

Prinsip:

React butuh principal yang berguna untuk UX dan request context, bukan seluruh identity dossier.


10. Account lifecycle vs session lifecycle

Account lifecycle:

Session lifecycle:

Keduanya berbeda.

  • Account suspended harus membuat session tidak boleh dipakai lagi.
  • Session expired tidak berarti account disabled.
  • Logout session tidak menghapus account.
  • Password reset bisa revoke sessions tetapi account tetap active.

React harus merespons keduanya secara berbeda.


11. Tenant, organization, workspace, project

Multi-tenant auth adalah sumber bug besar.

Istilah bisa berbeda:

  • tenant;
  • organization;
  • workspace;
  • account group;
  • agency;
  • regulator office;
  • project;
  • team.

Yang penting: active context harus eksplisit.

type TenantContext = {
  activeTenantId: string;
  membershipId: string;
  tenantRole?: string;
  region?: string;
  dataResidency?: string;
};

11.1 Tenant switch bukan cosmetic action

Tenant switch harus memicu:

  • permission cache invalidation;
  • query cache invalidation;
  • route revalidation;
  • active resource reset jika resource bukan milik tenant baru;
  • audit context update;
  • possibly session context update;
  • UI shell rebuild.

Buruk:

setActiveTenantId(tenantId);

Lebih benar:

await auth.switchTenant(tenantId);
queryClient.clear();
router.revalidate();
audit.setContext({ tenantId });

Tenant switch adalah auth event, bukan dropdown biasa.


12. Membership sebagai authorization input

Membership adalah hubungan account dengan tenant/org/project.

type Membership = {
  membershipId: string;
  accountId: string;
  tenantId: string;
  status: "active" | "suspended" | "expired";
  roles: string[];
  attributes: {
    region?: string;
    department?: string;
    clearance?: "public" | "internal" | "confidential";
  };
};

Authorization sering tidak langsung menggunakan account, tetapi membership.

Contoh:

can({
  subject: { accountId: "acct_789", membershipId: "mem_456" },
  action: "case.approve",
  resource: { type: "case", id: "CASE-123", tenantId: "REGULATOR-JKT" },
  context: { activeTenantId: "REGULATOR-JKT" },
});

Jika membership suspended, session masih bisa valid tetapi akses tenant harus ditolak.


13. React state design: pisahkan lima hal

Dalam React, jangan campur semua menjadi satu user.

Pisahkan minimal:

  1. auth state;
  2. session state;
  3. principal;
  4. active tenant context;
  5. permission/capability state.

Contoh:

type AuthStore = {
  auth: AuthState;
  session: Session | null;
  principal: Principal | null;
  activeTenant: TenantContext | null;
  permissionSnapshot: PermissionSnapshot | null;
};

Tetapi interface konsumsi UI boleh sederhana:

const { status, principal } = useAuth();
const tenant = useActiveTenant();
const canApprove = useCan("case.approve", { type: "case", id: caseId });

Internal model boleh kaya. API hook harus ergonomis.


14. Authentication result vs authorization result

Authentication result:

type AuthenticationResult = {
  status: "success";
  subject: {
    issuer: string;
    externalSubjectId: string;
  };
  authenticatedAt: string;
  assuranceLevel: "password" | "mfa" | "phishing_resistant";
};

Authorization result:

type AuthorizationResult = {
  effect: "allow" | "deny";
  action: string;
  resource: ResourceRef;
  reason?: string;
  constraints?: Record<string, unknown>;
  evaluatedAt: string;
  policyVersion: string;
};

Jangan menggabungkan keduanya menjadi:

type LoginResult = {
  token: string;
  user: User;
  permissions: string[];
};

Itu boleh sebagai bootstrap response sederhana, tetapi jangan jadikan mental model utama.


15. Claims mapping pipeline

Saat menggunakan OIDC/SSO, claims eksternal harus dipetakan ke model internal.

Mapping yang buruk:

const user = decodedToken;

Mapping yang lebih sehat:

const externalIdentity = await validateOidcClaims(idToken);
const account = await accountRepository.findByExternalIdentity(externalIdentity);
const memberships = await membershipRepository.findActiveByAccount(account.id);
const principal = principalFactory.from(account, memberships, externalIdentity.assurance);
const session = await sessionService.create(principal);

React menerima principal yang sudah aman dan minimal.


16. Scope OAuth vs permission aplikasi

Misalkan access token punya scope:

api:read api:write

Itu biasanya berarti client boleh memanggil kategori API tertentu. Bukan berarti user boleh approve semua case.

Backend bisa melakukan dua lapis check:

assertTokenHasScope(token, "api:write");

const decision = await policy.can({
  subject,
  action: "case.approve",
  resource: caseResource,
  context,
});

if (decision.effect !== "allow") {
  throw new ForbiddenError(decision.reason);
}

Scope menjawab: client/token ini punya delegated API access?

Permission menjawab: subject ini boleh melakukan action domain ini terhadap resource ini?


17. RBAC, ABAC, ReBAC, ACL secara singkat

Detailnya akan dibahas di phase authorization modelling. Untuk Part 002, pahami perbedaan dasarnya.

17.1 RBAC — Role-Based Access Control

Decision berdasarkan role.

role senior_investigator has permission case.comment
role supervisor has permission case.approve

Bagus untuk administrasi. Lemah untuk context/resource kompleks jika berdiri sendiri.

17.2 ABAC — Attribute-Based Access Control

Decision berdasarkan attribute subject/resource/environment.

allow if subject.region == resource.region
and subject.clearance >= resource.classification
and resource.status == "pending_approval"

Bagus untuk rule kompleks. Butuh governance agar policy tidak liar.

17.3 ReBAC — Relationship-Based Access Control

Decision berdasarkan relationship graph.

user Ana is investigator on CASE-123
user Budi is supervisor of team that owns CASE-123

Bagus untuk collaboration dan object hierarchy.

17.4 ACL — Access Control List

Grant langsung pada object.

CASE-123:
  Ana: viewer
  Budi: editor
  LegalTeam: reviewer

Bagus untuk object sharing. Perlu strategi inheritance, revocation, dan scale.

Dalam sistem nyata, model sering hybrid.


18. Authorization sebagai fungsi state machine domain

Authorization sering harus mempertimbangkan state transition.

Perhatikan bahwa tidak semua denial adalah 403.

  • Jika session invalid: 401.
  • Jika authenticated tapi tidak punya capability: 403.
  • Jika case state berubah: 409.
  • Jika butuh MFA: step-up challenge.
  • Jika resource tidak ada atau disembunyikan: 404.

React harus bisa menampilkan UX yang sesuai.


19. Permission response yang berguna untuk UI

UI sering butuh lebih dari allowed/denied.

type Capability = {
  action: string;
  effect: "allow" | "deny";
  reasonCode?: string;
  reasonText?: string;
  constraints?: {
    requiresMfa?: boolean;
    maxExportRows?: number;
    redactionLevel?: "none" | "partial" | "full";
  };
};

Contoh response:

{
  "resource": { "type": "case", "id": "CASE-123" },
  "capabilities": [
    {
      "action": "case.comment",
      "effect": "allow"
    },
    {
      "action": "case.approve",
      "effect": "deny",
      "reasonCode": "SUBMITTER_CANNOT_APPROVE",
      "reasonText": "You cannot approve a case you submitted."
    },
    {
      "action": "case.export.full",
      "effect": "allow",
      "constraints": {
        "requiresMfa": true,
        "maxExportRows": 5000
      }
    }
  ]
}

React bisa menggunakan ini untuk:

  • render action;
  • disable action;
  • show reason;
  • trigger step-up auth;
  • adjust UI constraint.

Tetapi action submit tetap harus dicek backend.


20. Identity assurance: login tidak selalu sama kuat

Tidak semua authentication event punya tingkat assurance yang sama.

Contoh level praktis:

AssuranceContohCocok untuk
AnonymousBelum loginPublic page.
PasswordPassword sajaBasic account access.
MFAPassword + second factorSensitive view/action.
Phishing-resistantPasskey/WebAuthn/security keyHigh-risk admin/financial/regulatory action.

UI perlu memahami saat action butuh step-up.

const decision = useCan("case.export.full", { type: "case", id: caseId });

if (decision.effect === "allow" && decision.constraints?.requiresMfa) {
  return <Button onClick={startStepUp}>Verify again to export</Button>;
}

Authentication assurance adalah input authorization.


21. Auth bugs yang berasal dari istilah salah

Bug 1 — Email sebagai identity key

const user = await users.findByEmail(claims.email);

Masalah:

  • email bisa berubah;
  • provider bisa mengirim email belum verified;
  • email bisa reusable;
  • normalization berbeda;
  • beberapa identities bisa punya email sama di environment berbeda.

Lebih baik mapping berdasarkan issuer + subject.

const user = await users.findByExternalIdentity({
  issuer: claims.iss,
  subject: claims.sub,
});

Bug 2 — Role global pada multi-tenant app

if (user.role === "admin") allow();

Masalah: admin tenant A menjadi admin tenant B.

Lebih baik:

const membership = await memberships.find(user.accountId, activeTenantId);
if (membership.roles.includes("tenant_admin")) allow();

Tetap perlu resource/context check.

Bug 3 — Permission disimpan dalam JWT terlalu lama

Role dicabut pukul 10:00, token baru expired pukul 11:00. Selama satu jam UI/backend yang hanya percaya JWT claim bisa memberikan akses lama.

Solusi tergantung architecture:

  • short-lived access token;
  • introspection;
  • session version;
  • permission version;
  • token revocation;
  • backend policy lookup;
  • event-driven cache invalidation.

Bug 4 — Scope dianggap permission

if (token.scope.includes("api:write")) {
  approveCase();
}

api:write terlalu luas. Domain action tetap harus dicek.

Bug 5 — Logout hanya clear local state

setUser(null);
localStorage.clear();

Masalah:

  • server session masih valid;
  • refresh token belum revoked;
  • tab lain masih aktif;
  • query cache masih menyimpan data;
  • service worker/cache mungkin masih punya response;
  • IdP session masih ada.

Logout adalah distributed cleanup.


22. React implementation boundary: siapa bertanggung jawab apa?

LayerTanggung jawabTidak boleh
Auth ProviderMenyediakan auth state untuk React treeMenjadi policy engine final.
Session ManagerRefresh, expiry, logout propagationMenyimpan token secara sembarangan.
API ClientAttach credential, handle 401/403Mengubah 403 menjadi login otomatis.
Router GuardCoarse auth dan loader redirectMenganggap route guard cukup.
Permission HookExposure control dan UXMemberi izin final terhadap mutation.
Backend APISession validation dan enforcementPercaya role dari body/frontend.
Policy EngineDecision berdasarkan subject/action/resource/contextMengembalikan reason sensitif sembarangan.
Audit LayerMerekam event pentingMenyimpan token/secret di log.

23. Contract bootstrap yang sehat

Endpoint /session atau /me sering menjadi pusat bootstrap.

Contoh response yang lebih sehat:

{
  "auth": {
    "status": "authenticated",
    "sessionId": "sess_abc",
    "expiresAt": "2026-07-07T17:00:00+07:00",
    "idleExpiresAt": "2026-07-07T10:30:00+07:00"
  },
  "principal": {
    "subjectId": "sub_123",
    "accountId": "acct_789",
    "displayName": "Ana Wijaya",
    "primaryEmail": "ana@company.example",
    "assurance": {
      "level": "mfa",
      "authenticatedAt": "2026-07-07T09:00:00+07:00"
    }
  },
  "activeTenant": {
    "tenantId": "REGULATOR-JKT",
    "displayName": "Jakarta Enforcement Office",
    "membershipId": "mem_456",
    "roles": ["senior_investigator"]
  },
  "globalCapabilities": [
    "case.create",
    "case.search",
    "user.invite.request"
  ]
}

Hal yang sengaja tidak dikirim:

  • raw refresh token;
  • seluruh IdP claims;
  • secrets;
  • permissions lintas tenant yang tidak aktif;
  • internal policy internals;
  • sensitive audit metadata.

24. Authorization untuk list berbeda dari detail

Detail check:

Can Ana read CASE-123?

List check:

Which cases can Ana see?

List authorization sering berupa filter, bukan boolean.

type CaseVisibilityFilter = {
  tenantId: string;
  allowedRegions: string[];
  allowedClassifications: string[];
  assignedTeamIds?: string[];
};

Backend menerjemahkan filter itu ke query database.

React tidak boleh mengambil semua case lalu filter di client jika data yang tidak boleh dilihat ikut terkirim.

Buruk:

const allCases = await api.getAllCases();
const visibleCases = allCases.filter(canViewCaseInFrontend);

Lebih baik:

const visibleCases = await api.searchCases({ q, filters });

Backend menerapkan authorization filter.


25. Denial reason: informatif tapi aman

UI yang baik memberi alasan. Tetapi alasan tidak boleh membocorkan informasi sensitif.

Contoh aman:

You do not have permission to view this case.

Contoh mungkin aman untuk internal app:

Only supervisors in the assigned region can approve this case.

Contoh berisiko:

You cannot view this case because it belongs to the Anti-Corruption Investigation Unit and contains witness identity data.

Denial reason harus didesain berdasarkan sensitivity domain.

Pattern:

type DenialReason = {
  code: string;
  safeMessage: string;
  supportReference?: string;
};

Audit log boleh menyimpan reason detail. UI mungkin hanya menampilkan safe message.


26. Testing vocabulary

Setelah istilah jelas, test juga lebih jelas.

Authentication tests

  • invalid credentials rejected;
  • MFA required for certain account;
  • callback validates state/nonce;
  • expired session returns 401;
  • revoked session returns 401.

Identity mapping tests

  • issuer+subject maps to correct account;
  • email change does not create duplicate account;
  • disabled account cannot create session;
  • unlinked external identity requires provisioning flow.

Authorization tests

  • user without membership denied tenant resource;
  • tenant admin A denied tenant B;
  • investigator cannot approve own case;
  • auditor receives redacted data;
  • MFA required for full export;
  • object grant revoked means future request denied.

React UI tests

  • anonymous sees login prompt;
  • forbidden route shows 403, not login;
  • unknown permission disables action;
  • denied action shows safe reason;
  • tenant switch clears stale permission;
  • logout clears query cache.

27. Practical naming convention

Naming matters. Gunakan nama yang memaksa presisi.

Avoid

user
isLoggedIn
isAdmin
tokenData
authData
canAccess

Prefer

principal
session
activeTenant
membership
permissionSnapshot
authorizationDecision
assuranceLevel
requireAuthenticatedSession
requireCapability
canPerformAction

isAdmin mungkin tetap boleh untuk UI lokal yang sangat terbatas, tetapi jangan jadikan policy primitive.


28. Mini architecture: type-level separation

type ExternalIdentity = {
  provider: "oidc" | "saml" | "password" | "passkey";
  issuer?: string;
  subject: string;
  email?: string;
  emailVerified?: boolean;
};

type Account = {
  accountId: string;
  status: "active" | "suspended" | "deactivated";
  createdAt: string;
};

type Profile = {
  displayName: string;
  avatarUrl?: string;
  locale?: string;
};

type Membership = {
  membershipId: string;
  accountId: string;
  tenantId: string;
  status: "active" | "suspended" | "expired";
  roles: string[];
};

type Session = {
  sessionId: string;
  accountId: string;
  createdAt: string;
  expiresAt: string;
  assuranceLevel: "password" | "mfa" | "phishing_resistant";
};

type Principal = {
  accountId: string;
  subjectId: string;
  profile: Profile;
  activeMembership?: Membership;
  assuranceLevel: Session["assuranceLevel"];
};

Dengan pemisahan ini, kita bisa menjawab pertanyaan secara presisi:

  • identity berubah? update ExternalIdentity mapping;
  • profile berubah? update Profile;
  • user suspended? update Account.status;
  • tenant access dicabut? update Membership.status;
  • session expired? update Session;
  • permission denied? hasil AuthorizationDecision.

29. Checklist Part 002

Gunakan checklist ini untuk mereview auth model.

Identity

  • Apakah external identity dipisah dari internal account?
  • Apakah mapping memakai issuer + subject, bukan email saja?
  • Apakah profile tidak dipakai sebagai source of truth authorization?
  • Apakah account status diperiksa saat session dibuat/direstore?

Authentication

  • Apakah authentication menghasilkan assurance level?
  • Apakah MFA/step-up dibedakan dari login biasa?
  • Apakah React tidak menjadi verifier final?
  • Apakah callback/redirect flow divalidasi server-side?

Session

  • Apakah session lifecycle dipisah dari account lifecycle?
  • Apakah logout/revocation jelas?
  • Apakah session valid tidak otomatis berarti permission valid?
  • Apakah multi-device/multi-tab dipertimbangkan?

Authorization

  • Apakah authorization memakai subject/action/resource/context?
  • Apakah role hanya input, bukan decision final?
  • Apakah tenant/membership eksplisit?
  • Apakah list authorization tidak dilakukan dengan client-side filtering atas data sensitif?
  • Apakah permission reason aman untuk UI?

React

  • Apakah user tidak menjadi blob semua informasi?
  • Apakah useAuth, useSession, useActiveTenant, dan useCan punya boundary jelas?
  • Apakah 401 dan 403 dibedakan?
  • Apakah tenant switch menghapus stale cache?

30. Latihan desain

Ambil object sederhana:

type User = {
  id: string;
  email: string;
  name: string;
  role: "admin" | "manager" | "user";
  permissions: string[];
  token: string;
};

Pecah menjadi:

  1. ExternalIdentity;
  2. Account;
  3. Profile;
  4. Membership;
  5. Session;
  6. Principal;
  7. PermissionSnapshot;
  8. AuthorizationDecision.

Lalu jawab:

  • field mana yang boleh dikirim ke React?
  • field mana yang hanya boleh ada di backend?
  • field mana yang boleh dicache?
  • field mana yang harus invalidated saat tenant switch?
  • field mana yang harus masuk audit log?

Kalau pertanyaan ini sulit dijawab, model auth masih terlalu kabur.


31. Ringkasan mental model

Authentication membuktikan klaim identity. Identity menjelaskan siapa subject dalam namespace tertentu. Account adalah representasi internal aplikasi. Session menjaga continuity setelah authentication. Authorization memutuskan apakah subject boleh melakukan action terhadap resource dalam context tertentu. Role, scope, claim, permission, ACL, dan entitlement adalah input/model yang berbeda; jangan dicampur.

Untuk React engineer, kesimpulan praktisnya:

  • jangan desain auth di sekitar user blob;
  • jangan desain authorization di sekitar isAdmin;
  • jangan treat token sebagai domain model;
  • jangan treat authenticated sebagai authorized;
  • jangan treat tenant switch sebagai filter UI;
  • jangan treat permission snapshot sebagai truth permanen;
  • buat boundary yang jelas antara identity, session, principal, membership, permission, dan decision.

Kalimat yang harus diingat:

Login tells the system who is speaking. Authorization decides whether that speaker may do this action to this resource now.


32. Referensi utama

  • OWASP Authentication Cheat Sheet — definisi authentication dan praktik login/authenticator: https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html
  • OWASP Authorization Cheat Sheet — access control dan validasi authorization per request: https://cheatsheetseries.owasp.org/cheatsheets/Authorization_Cheat_Sheet.html
  • OWASP Session Management Cheat Sheet — session lifecycle dan session identifier: https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html
  • OWASP OAuth2 Cheat Sheet — OAuth 2.0 dan OIDC sebagai identity layer di atas OAuth: https://cheatsheetseries.owasp.org/cheatsheets/OAuth2_Cheat_Sheet.html
  • RFC 9700 OAuth 2.0 Security Best Current Practice: https://www.rfc-editor.org/rfc/rfc9700.html
  • NIST SP 800-63B Digital Identity Guidelines — authenticator assurance dan lifecycle authenticator: https://pages.nist.gov/800-63-3/sp800-63b.html
Lesson Recap

You just completed lesson 02 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.