api-service example
A standalone backend service whose API routes are gated by OAuth2 bearer tokens issued by an external identity provider.
Use this shape when you have a backend (microservice, M2M API, worker-fronting endpoint) that needs to accept tokens but has no frontend of its own.
What this example includes
HonoResourceServerwithIntrospectionTokenReadervalidating bearer tokens against the external IDP's/oauth2/introspectendpoint (RFC 7662).API routes demonstrating different scope requirements:
GET /api/public— no token requiredGET /api/private— any valid tokenGET /api/write— token must carrywritescopeGET /api/admin— token must carryadminscope
An interactive
GET /walkthrough page that drives every OAuth2 grant type against the companionapp-with-own-auth/example and tests the resulting token against/api/*.A dev-only
/dev/callbackredirect target and/dev/tokenproxy so the homepage can exercise the auth-code flow without exposing the client secret in the browser. Real api-services don't host these. The auth-code flow uses PKCE (required by default under OAuth 2.1, even for confidential clients): the homepage mints a verifier, stashes it in an HttpOnly cookie, and puts only the S256 challenge in the authorize URL;/dev/callbackreads the verifier back to complete the token exchange.Tests that stub
tokenReader.getTokendirectly (no introspection wire format faked in test code — the framework owns that).
Running it
By default this example points at app-with-own-auth/ on port 8001 as its IDP.
Start both:
# Terminal 1 — the IDP
deno task serve:app-with-own-auth
# Terminal 2 — this service
deno task serve:api-serviceThen open http://localhost:8002/.
What to change to make this production-ready
The example deliberately skips operational concerns to keep the OAuth2 wiring readable. When you copy this to a real project:
Move
AUTH_SERVER_URL,CLIENT_ID,CLIENT_SECRET,REDIRECT_URIout ofoauth2/server.tsinto env config. Hardcoded credentials ship with the demo because the example must stand up without setup.Register this service as a client with the external IDP (e.g. Udibo, Auth0, Cognito). The
CLIENT_ID/CLIENT_SECREThere authenticate the introspection request — the IDP gates who can introspect tokens.Drop the
/dev/*mount. Token acquisition is a client-app concern; this service shouldn't host it.Persist anything you cache on the introspection response. The example calls introspection on every request; production deployments typically cache active tokens in memory (or Redis) until expiry to avoid hitting the IDP on every call.
Add rate limiting and logging on
/api/*— both standard hardening, neither shown here.Wire OpenTelemetry / your tracing system through the resource server's
getContext(c)so you can correlate API calls back to the client and user that made them.If you need user data beyond
subandusername, enrich it ingetUser(data)by joining against your own user table ondata.sub(the IDP's stable user identifier), or by calling the IDP's/userinfoendpoint (OIDC). A machine token (client credentials) carries nosub, sodata.subis present only when there is a real user to join against.
The introspection wire format itself is covered by
@udibo/oauth2/server/resource's IntrospectionTokenReader — you do not write
that code yourself, just the getClient / getUser projection mappers in
oauth2/server.ts.

