OAUTH 2.0 · LAB
Make your discovery client refuse a document that describes a different API
Refuse a resource document that does not match the address called, refuse authorization servers outside your trust policy, and refuse to fetch internal or non-HTTPS addresses before any request leaves.
PlannedIncludes a simulationUses both lab tenants
The lesson
Builds on: Locating its authorization servers.
New to the labs? Start with the lab toolkit and the shared cast and names every lab uses.
Planned. The core of this lab waits on platform features that are not built yet. The planned walkthrough shows exactly how it will run; Do today is a real exercise you can do now.
- G3 Sample protected resource API; no RFC 9728 protected resource metadata
- G66 Second lab tenant for every learner: additional tenants need a paid subscription or a BTL grant, so labs that use Lab Mail cannot be completed by an ordinary learner yet
Needs a second tenant. This lab also uses Lab Mail, a second tenant. Additional tenants currently need a paid subscription or a BTL grant, so you may not be able to do the Lab Mail steps yet (gap G66).
Setup
These steps are real today. You need fixture.mjs and find-api.mjs from Find an API's metadata starting from nothing but its address, and ISSUER and ISSUER2 for Lab Photos and Lab Mail in the shell.
Planned walkthrough
This walkthrough runs once both lab tenants host the sample photo API with RFC 9728 metadata (G3), each listing its own issuer.
A challenge that points at another API's genuine document. Run a small server of your own on
127.0.0.1:8791whose401carriesresource_metadata="<your ISSUER>/.well-known/oauth-protected-resource/resource", then:
FIXTURE_ORIGIN=http://127.0.0.1:8791 TRUSTED_ISSUERS="$ISSUER" node find-api.mjs http://127.0.0.1:8791/
It stops with resource_mismatch. The document is Lab Photos' own and names <your ISSUER>/resource, not the address you called, so no token is ever requested for it.
Why it matters: everything fetched after such a challenge is authentic, and an audience-restricted token would not help. Only comparing the document's resource with the address the client actually called stops the token from being sent to the wrong API.
An authorization server outside your policy:
TRUSTED_ISSUERS="$ISSUER" node find-api.mjs "$ISSUER2/resource"
TRUSTED_ISSUERS="$ISSUER $ISSUER2" node find-api.mjs "$ISSUER2/resource"
The first stops with no_trusted_authorization_server and lists Lab Mail. The second resolves to Lab Mail, an authorization server where the collage app already has a registration.
Why it matters: a valid document proves only that the API said it. The client decides whom to trust, shows the user the real domain before redirecting, and never presents credentials registered at one server to another.
Refresh on a new challenge. Change the sample API's documentation address in Lab Photos. When the API next sends a challenge with
resource_metadata, fetch the document again; the fresh copy must pass the same checks before any new value is used.
Do today
All checks below run in your own client code against the local fixture.
Simulation. the API is your local fixture.mjs, because no tenant publishes protected resource metadata yet (G3). The authorization servers it names are your real tenants.
The called address versus the document. With
node fixture.mjsrunning:
export FIXTURE_ORIGIN=http://127.0.0.1:8790 TRUSTED_ISSUERS="$ISSUER"
node find-api.mjs http://127.0.0.1:8790/other
node find-api.mjs http://127.0.0.1:8790/api/
node find-api.mjs http://127.0.0.1:8790/api
The first stops with resource_mismatch: /other's challenge points at /api's document. The second stops with resource_mismatch too: the trailing slash makes a different string. The third is discovered.
Why it matters: the comparison is exact, against the address as the client stored it, never a copy an HTTP library has rewritten.
Your trust policy. Stop the fixture and restart it naming Lab Mail:
AS="$ISSUER2" node fixture.mjs. Then:
TRUSTED_ISSUERS="$ISSUER" node find-api.mjs http://127.0.0.1:8790/api
TRUSTED_ISSUERS="$ISSUER $ISSUER2" node find-api.mjs http://127.0.0.1:8790/api
The first stops with no_trusted_authorization_server; the second is discovered with Lab Mail as the issuer. The same decision applies when an API you already use starts naming a different authorization server: judge it as carefully as a stranger.
Fetching safely. Remove the fixture exception and try addresses another party might choose:
unset FIXTURE_ORIGIN
for u in http://127.0.0.1:8790/api https://127.0.0.1/ https://localhost/ https://169.254.169.254/ https://10.0.0.1/; do node find-api.mjs "$u"; done
The first stops with not_https, the rest with internal_address, each before any request is sent.
Why it matters: discovery makes your backend fetch addresses other parties chose, which invites server-side request forgery. HTTPS only, no private, loopback or link-local destinations, the same rule for redirects (the client does not follow them), and a time limit on every fetch. A production client also pins the address it checked, so a second DNS answer cannot change it.
Check your work
There are no automated checks while this lab is planned. For Do today, your terminal shows resource_mismatch twice, one discovered resource, no_trusted_authorization_server then a resolved Lab Mail issuer, not_https once and internal_address four times.
Cleanup
Stop the fixture and run unset TRUSTED_ISSUERS. Keep find-api.mjs if you continue to the dynamic client registration labs; otherwise delete it with fixture.mjs.
Missing infrastructure
G3 Sample protected resource API on each lab tenant, with RFC 9728 metadata and
resource_metadatachallenges, so the planned steps use genuine documents. Optional later:signed_metadata, which this lab does not plan.G66 Second lab tenant: this lab uses Lab Mail, a second tenant. Additional tenants currently need a paid subscription or a BTL grant, so an ordinary learner can do only the Lab Photos steps until every learner can have a second lab tenant.