AUTHORIZATION AND POLICY · LAB
Every way into your tenant's directory, and two subjects in one request
Inventory every path that can change a user, prove each one enforces the same rule, and refuse a token to one client for a user who is allowed through another.
Partly readyUses your lab tenant
The lesson
Builds on: Checking access to each object, Writing rules with attributes.
New to the labs? Start with the lab toolkit and the shared cast and names every lab uses.
Partly ready. Most of this lab runs today. Steps that wait on platform features are marked, and Missing infrastructure says what they need.
- G3 Sample protected resource API; no RFC 9728 protected resource metadata
- G22 End-user authorization engine (PDP, RBAC/ABAC/ReBAC for application data)
Your progress
Press Start before you begin. Only events your tenant records after that count, in the order below. Checking reads your tenant's Audit, so you need Audit read access in it.
Sign in to start this lab and check your progress. Log in or create an account.
Ben's edit of a role holder is refused in the portal
Recorded as
tenant.users.updaterejected about[email protected].Ava gets prints through the printing service
Recorded as
oauth.tokensucceeded forlab-printer-appabout[email protected].The policy refuses prints for the collage app
Recorded as
oauth.tokenrejected (policy_denied) forlab-collageabout[email protected].A client that never consults the policy gets prints
Recorded as
oauth.tokensucceeded forlab-collageabout[email protected].
Setup
The tenant's directory has several real entry points, and each enforces authorization where the change happens. A gateway in front of a photo API, row filters and partial evaluation for application data are G3 and G22.
Complete Object and field checks in the tenant's own APIs (Cora holds
Auditor, Ben holdsHelp deskwithtenant.users.update, and the SCIM clients exist) and Permit and forbid rules in a token policy.Make sure
lab-collageexists, as in Freshness, step-up for role holders, and SSO across two applications, and assign it the scopeprints.createfor this lab.Create
lab-tmp-policy(JWT, default key), assign it to bothlab-printer-appandlab-collage, and paste this policy with the printer's client ID:
// The tenant's decision about the client, not the person: only the printing service may place print orders.
const PRINTER = '<lab-printer-app client ID>';
if (context.client_id !== PRINTER && context.scopes.includes('prints.create')) return { allow: false, claims: {} };
return { allow: true, claims: {} };
Define helpers for both clients.
startprints an authorization URL for a client and waits for the callback;finishredeems the code, sendinglab-collage's secret only forlab-collage.
PRINTER_ID="<lab-printer-app client ID>"; COLLAGE_ID="<lab-collage client ID>"
read -rs COLLAGE_SECRET
start() { # $1 = client ID
eval "$(btl-lab pkce)"; eval "$(btl-lab state)"
echo "$ISSUER/oauth/authorize?response_type=code&client_id=$1&redirect_uri=http://127.0.0.1:8765/callback&scope=openid%20photos.read%20prints.create&state=$STATE&nonce=$NONCE&code_challenge=$CHALLENGE&code_challenge_method=S256"
btl-lab callback
}
finish() { # $1 = client ID
read -rsp "Code: " CODE; echo
if [ "$1" = "$COLLAGE_ID" ]; then AUTH=(-u "$COLLAGE_ID:$COLLAGE_SECRET"); else AUTH=(-d "client_id=$1"); fi
curl -s "${AUTH[@]}" "$ISSUER/oauth/token" -d grant_type=authorization_code -d "code=$CODE" \
-d redirect_uri=http://127.0.0.1:8765/callback -d "code_verifier=$VERIFIER" | jq '{scope, error, error_description}'
}
Walkthrough
Inventory every way a user record can change in your tenant, and the identity each path acts as: the portal's Users page (you, a BTL user),
$ISSUER/manage(a tenant user with a management role), SCIM/Userswith PUT, PATCH and DELETE, SCIM/Bulk(an OAuth client), the HR simulator (its own client,lab-hr-feed, through SCIM), and$ISSUER/account(the user, for their own name only).
Why it matters: a policy protects only the paths that consult it. The lesson's archive job was a path nobody listed.
The same protection on each path. In Ben's window at
$ISSUER/manage, edit Cora's name: refused, although Ben holdstenant.users.update. Through SCIM aslab-provisioning, PATCH Cora'sactivetofalse: 403 withprotected_user. Put the same PATCH inside a/Bulkrequest: that operation fails with the same error.
Why it matters: the rule lives in the one place every change goes through, so a client or a batch cannot skip it, the structural fix the lesson recommends.
A background job with its own identity. Open Audit and find events from an HR simulator run, or run its lifecycle template briefly. The actor is the
lab-hr-feedclient. Its SCIM scopes grant user and group changes and nothing else: no role assignment, no Audit, no Logs.
Why it matters: the archive job in the lesson needed its own identity with only what an archive job needs. Rules that forbid everyone apply to it unchanged.
Two subjects. As Ava, run
start "$PRINTER_ID", approve in Ava's window, andfinish "$PRINTER_ID":prints.createis granted. Thenstart "$COLLAGE_ID"andfinish "$COLLAGE_ID": refused withinvalid_grantand "The tenant's access token policy refused to issue this token."
Why it matters: the request is limited by the scope, by Ava, and by the policy for that client. Ava is the same person both times; only the client changed, and the client comes from the registered client ID, never from anything the request claims.
Coarse checks at the edge, fine checks inside. Get a
lab-scim-readertoken, which has only the read scope, and send a PATCH for Ava, who holds no role. It is refused with 403 for insufficient scope before any user is looked at. Compare it with theprotected_userrefusal in step 2.
Why it matters: the scope is the early, cheap check; the role-holder rule is the fine check inside. Both run, and neither replaces the other.
Break it
A path that never asks. Unassign
lab-tmp-policyfromlab-collage, so it falls back to the default access token manager. As Ava, runstart "$COLLAGE_ID"andfinish "$COLLAGE_ID"again:prints.createis granted.
Why it matters: the rule was right; it was simply never consulted, exactly like the nightly archive job. Policy has to be attached to every path, and an inventory is how you notice one that is not.
Restore: assign lab-tmp-policy to lab-collage again and confirm the request is refused.
Check your work
Press Check my progress. The checks follow Ben's refused edit, the printer's token, the refusal for the collage app and the unprotected path in Break it.
Also confirm by hand:
Your inventory table names the identity for every path.
SCIM refused Cora's change with
protected_useron both/Usersand/Bulk, and the read-only client's PATCH with insufficient scope.
Cleanup
Remove
prints.createfromlab-collage.Assign
lab-printer-appandlab-collageback to the default access token manager and deletelab-tmp-policy.
Missing infrastructure
G3 and G22, enforcement inside a photo API. A gateway in front of the photo API that checks token and scope, a service that makes every object decision itself, a data layer that offers only scoped queries, and a
/downloadslist driven by a partially evaluated plan with a filter and per-photo checks, so each page is filled with 50 photos the caller may really download.