Policy Engine Guide

Replace OAuth scopes with fine-grained policy documents. Support RBAC, ABAC, time-based, IP-based, and MFA conditions in a single authorization model.

1. Why Policies Instead of Scopes?

OAuth scopes are strings like read:projects write:users. They seem simple, but they break down quickly in real applications.

OAuth Scopes

  • xScope explosion: hundreds of permission strings
  • xNo time-based or IP-based conditions
  • xToken size grows with permissions
  • xNo inheritance or wildcard matching
  • xChanges require re-issuing tokens

QAuth Policies

  • +One policy reference per token
  • +Time, IP, MFA, and custom conditions
  • +Token size stays constant
  • +Glob patterns for flexible matching
  • +Update policies without re-issuing tokens

2. Policy Document Structure

A policy document is a JSON object with an ID, version, and a list of rules. Rules are evaluated in priority order (highest first).

policy.json
1{
2 "id": "urn:qauth:policy:api-v2",
3 "version": "2026-01-30",
4 "issuer": "https://auth.example.com",
5 "name": "API Access Policy",
6 "description": "Controls access to the v2 API",
7 "rules": [
8 {
9 "id": "allow-read",
10 "effect": "allow",
11 "resources": ["api/v2/projects/*", "api/v2/users/*"],
12 "actions": ["read", "list"],
13 "priority": 0
14 },
15 {
16 "id": "admin-full-access",
17 "effect": "allow",
18 "resources": ["api/v2/**"],
19 "actions": ["*"],
20 "conditions": {
21 "custom": {
22 "role": { "in": ["admin", "superadmin"] }
23 }
24 },
25 "priority": 10
26 },
27 {
28 "id": "deny-dangerous",
29 "effect": "deny",
30 "resources": ["api/v2/system/**"],
31 "actions": ["delete", "purge"],
32 "priority": 100
33 }
34 ]
35}

3. Resource Patterns (Globs)

Resources use glob patterns. * matches a single path segment, ** matches any depth.

patterns.ts
1// Exact match
2"api/users" // matches only "api/users"
3
4// Single wildcard (*) — matches one path segment
5"api/users/*" // matches "api/users/123", NOT "api/users/123/posts"
6"api/*/settings" // matches "api/users/settings", "api/teams/settings"
7
8// Double wildcard (**) — matches any depth
9"api/users/**" // matches "api/users/123", "api/users/123/posts/456"
10"admin/**" // matches everything under "admin/"
11
12// Combining patterns
13["api/users/*", "api/teams/*"] // matches users OR teams (one level)
14["**"] // matches everything (use with caution)

Action Matching

Actions are matched exactly or with * (all actions):

  • ["read"] — only "read" action
  • ["read", "list"] — "read" or "list"
  • ["*"] — any action

4. Conditions

Rules can have conditions that must all be met for the rule to match. Four condition types are supported.

Time-Based Conditions

Restrict access to specific hours and days. Useful for business-hours-only operations.

time-condition.json
1{
2 "id": "business-hours-only",
3 "effect": "allow",
4 "resources": ["api/billing/**"],
5 "actions": ["*"],
6 "conditions": {
7 "time": {
8 "after": "09:00",
9 "before": "17:00",
10 "days": ["mon", "tue", "wed", "thu", "fri"],
11 "timezone": "America/New_York"
12 }
13 }
14}

IP-Based Conditions

Restrict access to specific CIDR ranges. Combine with allow and deny lists.

ip-condition.json
1{
2 "id": "office-network-only",
3 "effect": "allow",
4 "resources": ["api/admin/**"],
5 "actions": ["*"],
6 "conditions": {
7 "ip": {
8 "allow_ranges": ["10.0.0.0/8", "172.16.0.0/12"],
9 "deny_ranges": ["10.0.99.0/24"]
10 }
11 }
12}

MFA Conditions

Require multi-factor authentication for sensitive operations.

mfa-condition.json
1{
2 "id": "sensitive-operations",
3 "effect": "allow",
4 "resources": ["api/billing/payment-methods/**"],
5 "actions": ["create", "update", "delete"],
6 "conditions": {
7 "mfa": {
8 "required": true,
9 "methods": ["totp", "webauthn"]
10 }
11 }
12}

Custom Conditions

Match against any attribute on the subject. Supports in (array membership) and eq (exact equality).

custom-condition.json
1{
2 "id": "premium-feature",
3 "effect": "allow",
4 "resources": ["api/analytics/**"],
5 "actions": ["read", "export"],
6 "conditions": {
7 "custom": {
8 "plan": { "in": ["premium", "enterprise"] },
9 "verified": { "eq": true }
10 }
11 }
12}

5. Evaluation Flow

The policy engine follows a simple, predictable evaluation flow.

1

Sort rules by priority

Higher priority numbers are evaluated first

2

Match resource pattern

Check if the resource path matches any rule's glob patterns

3

Match action

Check if the requested action is in the rule's actions list

4

Evaluate conditions

All conditions must pass (AND logic)

5

Return first match

The first matching rule determines allow or deny

6

Default deny

If no rule matches, access is denied

evaluation.ts
1import { PolicyEngine } from '@quantumshield/qauth';
2
3const engine = new PolicyEngine();
4
5engine.loadPolicy({
6 id: 'urn:qauth:policy:app',
7 version: '1.0',
8 issuer: 'https://auth.example.com',
9 rules: [
10 // Priority 100: Explicit deny (always wins)
11 {
12 id: 'deny-system',
13 effect: 'deny',
14 resources: ['api/system/**'],
15 actions: ['*'],
16 priority: 100,
17 },
18 // Priority 10: Admin access
19 {
20 id: 'admin-access',
21 effect: 'allow',
22 resources: ['api/**'],
23 actions: ['*'],
24 conditions: { custom: { role: { in: ['admin'] } } },
25 priority: 10,
26 },
27 // Priority 0: Default read access
28 {
29 id: 'default-read',
30 effect: 'allow',
31 resources: ['api/public/**'],
32 actions: ['read'],
33 priority: 0,
34 },
35 ],
36});
37
38// Evaluation order:
39// 1. Sort rules by priority (100 → 10 → 0)
40// 2. Check each rule against the context
41// 3. First matching rule determines the effect
42// 4. If no rule matches → default DENY

6. Real-World: RBAC

Classic role-based access control with viewer, editor, and admin roles.

rbac-policy.ts
1const engine = new PolicyEngine();
2
3// Role-based access control
4engine.loadPolicy({
5 id: 'urn:qauth:policy:rbac',
6 version: '1.0',
7 issuer: 'https://auth.example.com',
8 rules: [
9 // Viewers can read
10 {
11 id: 'viewer-read',
12 effect: 'allow',
13 resources: ['api/**'],
14 actions: ['read', 'list'],
15 conditions: { custom: { role: { in: ['viewer', 'editor', 'admin'] } } },
16 },
17 // Editors can create and update
18 {
19 id: 'editor-write',
20 effect: 'allow',
21 resources: ['api/**'],
22 actions: ['create', 'update'],
23 conditions: { custom: { role: { in: ['editor', 'admin'] } } },
24 },
25 // Admins can delete
26 {
27 id: 'admin-delete',
28 effect: 'allow',
29 resources: ['api/**'],
30 actions: ['delete'],
31 conditions: { custom: { role: { in: ['admin'] } } },
32 },
33 ],
34});
35
36// Usage
37const result = engine.evaluate('urn:qauth:policy:rbac', {
38 subject: { id: 'user-1', attributes: { role: 'editor' } },
39 resource: { path: 'api/posts/123' },
40 request: { action: 'update' },
41});
42// result.effect === 'allow'

7. Real-World: Multi-Tenant SaaS

Tenant isolation with per-tenant roles and platform admin override.

saas-policy.ts
1const engine = new PolicyEngine();
2
3// Multi-tenant SaaS policy
4engine.loadPolicy({
5 id: 'urn:qauth:policy:saas',
6 version: '1.0',
7 issuer: 'https://auth.saas.com',
8 rules: [
9 // Users can only access their own tenant's data
10 {
11 id: 'tenant-isolation',
12 effect: 'allow',
13 resources: ['api/tenants/*/**'],
14 actions: ['read', 'list', 'create', 'update'],
15 conditions: {
16 custom: {
17 tenant_member: { eq: true },
18 },
19 },
20 },
21 // Tenant admins can manage tenant settings
22 {
23 id: 'tenant-admin',
24 effect: 'allow',
25 resources: ['api/tenants/*/settings/**'],
26 actions: ['*'],
27 conditions: {
28 custom: {
29 tenant_role: { in: ['owner', 'admin'] },
30 },
31 },
32 priority: 10,
33 },
34 // Platform admins can access everything
35 {
36 id: 'platform-admin',
37 effect: 'allow',
38 resources: ['**'],
39 actions: ['*'],
40 conditions: {
41 custom: {
42 platform_role: { in: ['superadmin'] },
43 },
44 },
45 priority: 100,
46 },
47 ],
48});

8. Real-World: Healthcare / HIPAA

HIPAA-compliant policy combining role, time, IP, and MFA conditions with emergency break-glass access.

hipaa-policy.ts
1const engine = new PolicyEngine();
2
3// HIPAA-compliant healthcare policy
4engine.loadPolicy({
5 id: 'urn:qauth:policy:hipaa',
6 version: '1.0',
7 issuer: 'https://auth.hospital.com',
8 rules: [
9 // Doctors can access patient records during business hours from hospital network
10 {
11 id: 'doctor-patient-access',
12 effect: 'allow',
13 resources: ['api/patients/*/records/**'],
14 actions: ['read'],
15 conditions: {
16 custom: { role: { in: ['doctor', 'specialist'] } },
17 time: {
18 after: '06:00',
19 before: '22:00',
20 timezone: 'America/New_York',
21 },
22 ip: {
23 allow_ranges: ['10.1.0.0/16'],
24 },
25 },
26 },
27 // Writing records requires MFA
28 {
29 id: 'write-records-mfa',
30 effect: 'allow',
31 resources: ['api/patients/*/records/**'],
32 actions: ['create', 'update'],
33 conditions: {
34 custom: { role: { in: ['doctor'] } },
35 mfa: {
36 required: true,
37 methods: ['totp', 'webauthn'],
38 },
39 },
40 priority: 10,
41 },
42 // Emergency access (highest priority, breaks glass)
43 {
44 id: 'emergency-access',
45 effect: 'allow',
46 resources: ['api/patients/**'],
47 actions: ['*'],
48 conditions: {
49 custom: {
50 emergency: { eq: true },
51 role: { in: ['doctor', 'nurse', 'emt'] },
52 },
53 },
54 priority: 1000,
55 },
56 ],
57});

Next Steps