Beta

Create a tenant

A new tenant starts with its own users, OAuth settings, audit history and logs. You are its first Tenant Admin.

BTL Admin

AUTHORIZATION AND POLICY · LAB

Permit and forbid rules in a token policy

Turn the lesson's "editors may download, nobody before the embargo" into a real token policy, and watch the same request change answer when only the time moves.

Partly readyUses your lab tenant

The lesson

Builds on: Codes, links, and approval prompts.

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.

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.

  1. Create the temporary policy manager

    Recorded as tenant.oauth.managers.create succeeded.

  2. Assign it to lab-printer-app only

    Recorded as tenant.oauth.managers.assign succeeded for lab-printer-app.

  3. Get tokens decided by the rules

    Recorded as oauth.token succeeded for lab-printer-app.

  4. A policy that widens scopes fails closed

    Recorded as oauth.token failed (policy_unavailable) for lab-printer-app.

Setup

The tenant has no decision point for application data yet (G22), and directory roles are fixed (G21). Its access token policy is a real rule hook, though: it runs on every token issuance with subject, client, scope and time attributes, and it can narrow scopes or refuse. Here prints.create stands in for the lesson's photo.download: ordering a print hands over the full-resolution file.

  1. Complete Sign in by email link, email code and authenticator code first. Mia's address is verified there, and this lab relies on it.

  2. In OAuth > Scopes, check that photos.read (common) and prints.create (exclusive) exist.

  3. If lab-printer-app does not exist, create it in OAuth > Clients with the single-page application preset: public, PKCE required, redirect URI http://127.0.0.1:8765/callback. Assign it prints.create.

  4. In OAuth > Access token managers, create lab-tmp-policy: JWT format, the default signing key. Assign it to lab-printer-app only, so no other client is affected.

  5. Paste this policy into lab-tmp-policy. Replace the address with Mia's, and EMBARGO_UNTIL with the Unix time 10 minutes from now, which date -d '+10 min' +%s prints.

// Stand-in for "picture editor": directory roles are fixed (G21), so a verified-address allowlist plays the role.
const EDITORS = ['[email protected]'];
const EMBARGO_UNTIL = 1790000000;
const now = context.claims.iat;   // the tenant's clock, never a time from the request
let scopes = [...context.scopes];
const drop = name => { scopes = scopes.filter(item => item !== name); };
// permit prints.create when the subject is an editor; anything else stays at no
if (!(context.subject.email_verified === true && EDITORS.includes(context.subject.email))) drop('prints.create');
// forbid prints.create before the embargo, whoever asks
if (now < EMBARGO_UNTIL) drop('prints.create');
return { allow: true, claims: {}, scopes };
  1. Define two helpers in your shell. authorize prints a URL to open in the person's window and waits for the callback; redeem exchanges the code you paste.

export CLIENT_ID="<lab-printer-app client ID>"
authorize() {   # $1 = scopes, $2 = extra parameters such as "&max_age=0"
  eval "$(btl-lab pkce)"; eval "$(btl-lab state)"
  echo "$ISSUER/oauth/authorize?response_type=code&client_id=$CLIENT_ID&redirect_uri=http://127.0.0.1:8765/callback&scope=${1// /%20}&state=$STATE&nonce=$NONCE&code_challenge=$CHALLENGE&code_challenge_method=S256$2"
  btl-lab callback
}
redeem() {
  read -rsp "Code: " CODE; echo
  RESP=$(curl -s "$ISSUER/oauth/token" -d grant_type=authorization_code -d "code=$CODE" \
    -d redirect_uri=http://127.0.0.1:8765/callback -d "client_id=$CLIENT_ID" -d "code_verifier=$VERIFIER")
  echo "$RESP" | jq '{scope, error, error_description, reference_id}'
  TOKEN=$(echo "$RESP" | jq -r '.access_token // empty'); ID_TOKEN=$(echo "$RESP" | jq -r '.id_token // empty')
}

Walkthrough

  1. Map each phrase to an attribute before running anything. "A picture editor": a subject attribute, here a verified address on the list. "Before its embargo ends": a resource attribute, the embargo time, held as a constant because there is no photo record yet. "Download": the action, the scope prints.create. "Now": an environment attribute, the tenant's own clock.

Why it matters: the rules name properties, not people. Nobody is named in a role; the sentence holds for whoever asks.

  1. As Mia, before the embargo: run authorize "openid photos.read prints.create", open the URL in Mia's window, approve, and redeem. The scope lacks prints.create.

Why it matters: the permit matches, but so does the embargo forbid, and a forbid wins.

  1. As Ava, the same request. No prints.create.

Why it matters: no permit matches, so default deny refuses, whatever the time.

  1. After EMBARGO_UNTIL, repeat as Mia. prints.create is granted, and btl-lab decode "$TOKEN" shows it in the token's scope.

Why it matters: only the time moved. No role, rule or code changed, which is the lesson's 14:00 regatta photo.

  1. Repeat as Ava after the embargo. Still refused.

Why it matters: a forbid that stops matching grants nothing. The permit is what grants.

  1. Write the lesson's three-row table with your own results: who, when, result, and why.

Why it matters: a rule set is understood by the requests it refuses as well as the ones it allows, and the table is the start of the tests a later lab writes.

Break it

  1. Add '[email protected]' to EDITORS and save. After the embargo, request prints.create as Ben. He is still refused: his address has never been verified, and the permit tests both attributes.

Restore: remove Ben's address from EDITORS and save.

Why it matters: an allowlist of email text is only as trustworthy as the verification behind it. The next lab follows where each attribute comes from.

  1. Make the policy return a scope that was not requested: change the last line to return { allow: true, claims: {}, scopes: [...scopes, 'photos.delete'] }; and request openid photos.read as Ava. The token request fails with server_error and a reference_id. Search Logs for that reference: oauth.token failed with reason policy_unavailable.

Restore: put the original last line back and confirm a token is issued again.

Why it matters: a policy may only narrow what was asked for. A result that tries to widen it is treated as a broken policy, and the request fails closed.

Check your work

Press Check my progress. The checks follow the manager, its assignment, tokens decided by the rules and the policy that failed closed.

Also confirm by hand:

  • Four token responses whose scope matches your table: only Mia after the embargo has prints.create.

  • Audit shows tenant.oauth.managers.update each time you saved the policy.

Cleanup

  • Assign lab-printer-app back to the default access token manager, then delete lab-tmp-policy. Later labs create their own.

Missing infrastructure

  • G22, object-level attribute rules. With the photo library API, the same rules would read resource.status and resource.embargo_until from each photo and environment.device.managed from the request, and a decision endpoint would return the lesson's three results for photos 9001 and 9002.

  • G21, role and custom-attribute claims. With real directory roles and attributes in the policy context, subject.roles and subject.desk replace the address allowlist.

Back to all labs

We value your privacy

We use cookies and similar technologies to enhance your browsing experience, and analytics to understand our traffic. By clicking "Allow All", you consent to optional analytics. Cookie Policy

The Lab