OpenID Connect provider
The fan club gives early entry to members who hold tickets. Its website wants a "Sign in with Box Office" button, so fans don't need yet another account, and it wants to see which tickets a fan holds. The box office already issues tokens to the door scanner. With a few more declarations, it becomes a full OpenID Connect provider.
Admitting the fan club
The fan club signs fans in with the authorization code flow. It authenticates with its own published keys, like the door scanner, and the fan decides what to share on a consent page:
;Every choice is part of the declaration:
authorization_codelists where fans may be sent back to, which scopes the fan club may ask for, and how ID tokens are signed. The fan club runs PHP, which verifies RSA signatures most easily.refresh_tokenlets the fan club stay connected after the 15-minute access token expires.introspectionlets the fan club's backend ask the box office whether a token is still active, instead of checking signatures itself.resourceslists what the fan club's access tokens are for. Here, a new resource for the partner API:
use issues_resource_tokens;
;EmailScope is the scope from Sign in with Google, and OpenidScope comes with Margaret. The same scope type works for both directions.
The endpoints
The authorization code flow needs more endpoints than the scanner did. The framework serves all of them:
;
;OidcEndpoint::Userinfo at GET /userinfo, OidcEndpoint::Revocation at POST /revoke, and OidcEndpoint::Introspection at POST /introspect complete the set. Margaret knows which endpoints your clients need: a client with refresh_token needs revocation, one with introspection needs introspection, and an endpoint nobody can use is a build error. The discovery document lists them all:
$ curl https://tickets.example.com/.well-known/openid-configuration
{"authorization_endpoint":"https://tickets.example.com/authorize",...,"grant_types_supported":["authorization_code","client_credentials","refresh_token"],...,"issuer":"https://tickets.example.com",...,"scopes_supported":["email","openid"],...,"userinfo_endpoint":"https://tickets.example.com/userinfo"}The schema command now also includes the framework's tables for pending authorizations, authorization codes, and refresh tokens.
The consent page
You design the consent page. It's a view that takes ConsentPageProps: the client, the scopes it asks for, and the URL to post the decision to:
The form posts the id and a decision of approve or deny. Margaret sends authorization and consent pages with frame-ancestors 'none', so no other site can frame them to trick fans into clicking "Allow".
Signing fans in
The provider recognizes fans by the box office session from Sessions. A fan who is signed in at the box office sees the consent page, and after "Allow", goes back to the fan club with a code:
$ curl -i -X POST https://tickets.example.com/authorize/consent \
-H "cookie: $BOX_OFFICE_SESSION" -d "id=7b2a912c-dbe0-4b62-ad06-40a2c419df78&decision=approve"
HTTP/1.1 303 See Other
location: https://fanclub.example.org/callback?code=Zn3ILFFw_BKZfJLQgDpViRYR_KNHwk6n_x2pA0OuKOo&iss=https%3A%2F%2Ftickets.example.com&state=xyz
content-security-policy: frame-ancestors 'none'The fan club exchanges the code at the token endpoint for an ID token, an access token, and a refresh token. A fan who isn't signed in at the box office gets a short 401 message instead, asking them to sign in at the box office first and then continue.
Userinfo claims
The userinfo endpoint answers with the fan's claims. You decide what they are, based on the scopes the fan granted:
FanProfileClaims is a singleton with #[provides_userinfo_claims], and FanProfile is a serializable struct with an optional email. grant.includes:: checks the scope by its type, never by its name.
A partner API
The fan club's access tokens are for the partner_api resource. A fan's tickets are just a route away. The provider reads the access token with #[bearer_token], and the token's subject is the customer's id:
;
PartnerFan is its own user type, separate from Customer, because it's recognized in a different way: by a bearer token instead of a session. The route GET /api/me/tickets asks for it with #[authenticated_user] fan: PartnerFan, loads the customer's orders, and answers with their tickets as JSON.