<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>OpenID Connect on josiete.com</title><link>https://www.josiete.com/en/tags/openid-connect/</link><description>Recent content in OpenID Connect on josiete.com</description><generator>Hugo</generator><language>en-us</language><lastBuildDate>Tue, 29 Sep 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://www.josiete.com/en/tags/openid-connect/index.xml" rel="self" type="application/rss+xml"/><item><title>Authentication vs. authorization: OAuth 2.0, OpenID Connect, and SAML explained with examples</title><link>https://www.josiete.com/en/posts/authentication-authorization-oauth-oidc-saml/</link><pubDate>Tue, 29 Sep 2026 00:00:00 +0000</pubDate><guid>https://www.josiete.com/en/posts/authentication-authorization-oauth-oidc-saml/</guid><description>&lt;p&gt;&lt;img src="https://www.josiete.com/images/autenticacion-autorizacion-oauth-oidc-saml.webp" alt="Illustration of an identity receiving access to a gallery, with permission to edit an owned photograph and an unrelated photograph protected"&gt;&lt;/p&gt;&#10;&lt;p&gt;A photo application is a useful place to separate concepts that are often blended together. Imagine that Alice signs in, browses a gallery, and changes the title of a photograph she uploaded. She can see one of Bob&amp;rsquo;s photographs, but cannot edit it. What did each step establish?&lt;/p&gt;&#10;&lt;p&gt;The sign-in established Alice&amp;rsquo;s identity. The decision to edit each photograph applied rules to an action and a specific resource. OAuth 2.0, OpenID Connect (OIDC), and SAML solve different parts of that problem. Keeping their boundaries clear avoids two common mistakes: treating a token as unlimited authorization, and treating an identity assertion as a credential for every API.&lt;/p&gt;&#10;&lt;h2 id="authentication-and-authorization-are-not-synonyms"&gt;Authentication and authorization are not synonyms&lt;/h2&gt;&#10;&lt;p&gt;&lt;strong&gt;Authentication&lt;/strong&gt; answers “who are you?” A password, passkey, corporate session, or authenticator app can let a provider establish that the person is Alice.&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;Authorization&lt;/strong&gt; answers “what can you do, to which resource, and under which conditions?” Alice might have &lt;code&gt;photos:read&lt;/code&gt; and &lt;code&gt;photos:write&lt;/code&gt;, while the API must still decide whether she owns photograph &lt;code&gt;p-42&lt;/code&gt;, whether it is archived, or whether a team policy grants access. Being authenticated does not make Alice the owner of every photograph.&lt;/p&gt;&#10;&lt;p&gt;The distinction also appears in HTTP:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;401 Unauthorized&lt;/strong&gt;—despite its historical name—means valid credentials are missing or could not be validated. A Bearer scheme commonly includes &lt;code&gt;WWW-Authenticate&lt;/code&gt;.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;403 Forbidden&lt;/strong&gt; means that the request was understood and the credentials were valid, but application authorization forbids the operation. Some APIs deliberately use 404 instead to avoid revealing that a resource exists.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;The exact error contract should be documented. Using 401 for an expired access token or a wrong audience, and 403 for insufficient scope, role, or ownership gives clients and operators a helpful signal. &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Status/401"&gt;MDN&amp;rsquo;s HTTP documentation distinguishes the two&lt;/a&gt;.&lt;/p&gt;&#10;&lt;h2 id="oauth-20-delegated-access-not-a-complete-permission-system"&gt;OAuth 2.0: delegated access, not a complete permission system&lt;/h2&gt;&#10;&lt;p&gt;OAuth 2.0 lets an application gain limited access to a resource without receiving the person&amp;rsquo;s password. In this case, the gallery web application never receives Alice&amp;rsquo;s password: she enters it, when required, at the identity provider. The application gets a token it can present to the photo API.&lt;/p&gt;&#10;&lt;p&gt;Its four roles describe the journey:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;the &lt;strong&gt;resource owner&lt;/strong&gt; is Alice, the person entitled to the photos or the access decision;&lt;/li&gt;&#10;&lt;li&gt;the &lt;strong&gt;client&lt;/strong&gt; is the web application that wants to call the API;&lt;/li&gt;&#10;&lt;li&gt;the &lt;strong&gt;authorization server&lt;/strong&gt; authenticates Alice, collects consent when appropriate, and issues credentials;&lt;/li&gt;&#10;&lt;li&gt;the &lt;strong&gt;resource server&lt;/strong&gt; is the photo API that protects the resources and validates the received credential.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;Consent, &lt;em&gt;scopes&lt;/em&gt;, and API authorization are related but separate layers. Consent can let a client ask for &lt;code&gt;photos:read&lt;/code&gt;; a scope limits the delegated access requested or granted; and the API decides on every request whether the token, action, and resource satisfy its policy. In particular, &lt;code&gt;photos:write&lt;/code&gt; does not mean “edit any photograph”: the API must compare the subject in the token with the owner of &lt;code&gt;p-42&lt;/code&gt;, or query a policy that permits a team editor.&lt;/p&gt;&#10;&lt;p&gt;OAuth defines how an access token is obtained and used, not a universal business-permission model. A token can be opaque or a JWT, and contain scopes, roles, or provider-specific attributes. Do not design a business policy assuming every provider represents those facts in the same way. Start with &lt;a href="https://datatracker.ietf.org/doc/html/rfc6749"&gt;OAuth 2.0&lt;/a&gt; and its &lt;a href="https://datatracker.ietf.org/doc/html/rfc9700"&gt;security BCP&lt;/a&gt;.&lt;/p&gt;&#10;&lt;h2 id="openid-connect-adds-who-signed-in"&gt;OpenID Connect adds “who signed in?”&lt;/h2&gt;&#10;&lt;p&gt;OAuth alone handles delegated authorization: an application may be allowed to invoke a resource without that being a sufficient identity statement for the application itself. OIDC adds an authentication layer on OAuth 2.0 and standardizes discovery, claims, and the &lt;strong&gt;ID token&lt;/strong&gt;. It is not the historical OpenID 2.0 protocol; the names are similar, but the technologies are different.&lt;/p&gt;&#10;&lt;p&gt;An OIDC flow can return three distinct artifacts:&lt;/p&gt;&#10;&lt;table&gt;&#10;&#9;&lt;thead&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Artifact&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Main recipient&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Purpose&lt;/th&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/thead&gt;&#10;&#9;&lt;tbody&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;strong&gt;ID token&lt;/strong&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;The client&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;States that the provider authenticated a user and carries claims such as &lt;code&gt;iss&lt;/code&gt;, &lt;code&gt;sub&lt;/code&gt;, and &lt;code&gt;aud&lt;/code&gt;, plus requested profile data.&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;strong&gt;Access token&lt;/strong&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;The API or resource server&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Is presented as &lt;code&gt;Authorization: Bearer …&lt;/code&gt; to access protected resources.&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;strong&gt;Refresh token&lt;/strong&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;The authorized client, when granted&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Lets it request new access tokens in certain circumstances.&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;p&gt;An ID token is intended for the client that initiated the sign-in; do not send it to the API as a credential. Both artifacts may look like JWTs, but their intended audiences, claims, and security contracts differ. &lt;a href="https://openid.net/specs/openid-connect-core-1_0-18.html"&gt;OIDC Core&lt;/a&gt; requires validation of, among other things, issuer, audience, signature, and expiry for ID tokens.&lt;/p&gt;&#10;&lt;h2 id="a-modern-sign-in-authorization-code-with-pkce"&gt;A modern sign-in: Authorization Code with PKCE&lt;/h2&gt;&#10;&lt;p&gt;For a new browser application, use &lt;strong&gt;Authorization Code with PKCE&lt;/strong&gt;. The browser receives a short-lived code through the redirect and the client exchanges it at the token endpoint. PKCE binds that exchange to the instance that initiated it through an ephemeral &lt;code&gt;code_verifier&lt;/code&gt;; only its derived challenge, normally using &lt;code&gt;S256&lt;/code&gt;, is sent to the authorization server.&lt;/p&gt;&#10;&lt;p&gt;This is a plain-text diagram, so it needs no JavaScript or special site renderer:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Browser / SPA Authorization server Photo API&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; | | |&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; |-- /authorize ----------&amp;gt;| |&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; | response_type=code, state, nonce, |&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; | code_challenge=S256(verifier), scope |&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; |&amp;lt;-- login and consent ---------------------------------------|&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; |&amp;lt;-- redirect_uri?code=...&amp;amp;state=... --| |&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; | | |&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; |-- /token: code + code_verifier ----&amp;gt;| |&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; |&amp;lt;-- access_token, id_token (maybe refresh_token) -----------|&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; | |&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; |-- GET /photos/p-42, Authorization: Bearer access_token ---&amp;gt;|&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; |&amp;lt;-- 200, 403, or 401 ----------------------------------------|&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The random values are not interchangeable:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;&lt;code&gt;state&lt;/code&gt;&lt;/strong&gt; binds the authorization response to the browser session and transaction that started it; it mitigates CSRF and can carry navigation state protected against tampering.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;&lt;code&gt;nonce&lt;/code&gt;&lt;/strong&gt; binds an OIDC request to the returning ID token and helps detect replay of that authentication assertion. The client must check it.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;PKCE&lt;/strong&gt; binds the code to the client instance&amp;rsquo;s secret &lt;code&gt;code_verifier&lt;/code&gt;, so a person who steals the code cannot redeem it without that value.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;The &lt;a href="https://datatracker.ietf.org/doc/html/rfc9700"&gt;OAuth security BCP&lt;/a&gt; requires PKCE for public clients and recommends &lt;code&gt;S256&lt;/code&gt;; current browser guidance also favours Authorization Code with PKCE. Implicit and direct password exchange are not recommended choices for a new application: the former puts tokens in browser navigation, and the latter makes a client handle credentials that should be handled by the provider.&lt;/p&gt;&#10;&lt;h2 id="saml-enterprise-federation-and-sso"&gt;SAML: enterprise federation and SSO&lt;/h2&gt;&#10;&lt;p&gt;SAML 2.0 is common in enterprise federation. A company can have one corporate &lt;strong&gt;Identity Provider (IdP)&lt;/strong&gt;—such as an employee directory with MFA—and several &lt;strong&gt;Service Providers (SPs)&lt;/strong&gt;: payroll, intranet, support, and an internal gallery. When Alice opens the gallery, its SP redirects her to the IdP; because she already has an IdP SSO session, it returns a signed &lt;strong&gt;assertion&lt;/strong&gt; that establishes her identity and, depending on configuration, attributes or groups.&lt;/p&gt;&#10;&lt;p&gt;Alice therefore does not enter her password at every application. The SP validates issuer, recipient, signature, audience, time conditions, and response correlation before creating &lt;strong&gt;its own application session&lt;/strong&gt;. Subsequent requests can carry that session cookie; a SAML exchange need not occur on every page request.&lt;/p&gt;&#10;&lt;table&gt;&#10;&#9;&lt;thead&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;SAML 2.0&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;OpenID Connect&lt;/th&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/thead&gt;&#10;&#9;&lt;tbody&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Common format and environment&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;XML assertions; broadly deployed for enterprise web SSO.&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;JSON/JWT and OAuth endpoints; natural for web, mobile, and APIs.&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Typical role&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;IdP–SP federation and application session.&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Client authentication and delegated API authorization.&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Practical choice&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;A good fit when the organization or SP already uses SAML.&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;A good fit for modern clients and OAuth APIs.&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;p&gt;Neither universally replaces the other. Existing integrations, SaaS products, the session model, and API needs drive the decision. &lt;a href="https://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf"&gt;SAML 2.0 Core&lt;/a&gt; defines assertions; OIDC covers a different family of exchanges and profiles.&lt;/p&gt;&#10;&lt;h2 id="do-we-have-to-ask-who-you-are-on-every-request"&gt;Do we have to ask who you are on every request?&lt;/h2&gt;&#10;&lt;p&gt;Not necessarily. Separate &lt;strong&gt;initial-login traffic&lt;/strong&gt;—redirects, authentication, consent, and code exchange—from ordinary API calls. The latter should not make the authorization server an unnecessary synchronous dependency, though every design has a trade-off.&lt;/p&gt;&#10;&lt;p&gt;An access token can be a &lt;strong&gt;signed JWT&lt;/strong&gt;. The API downloads the issuer&amp;rsquo;s public keys from JWKS, caches them, and verifies the signature, an allowed algorithm, &lt;code&gt;iss&lt;/code&gt;, &lt;code&gt;aud&lt;/code&gt;, and &lt;code&gt;exp&lt;/code&gt; locally. When an unknown &lt;code&gt;kid&lt;/code&gt; arrives, a correct library fetches JWKS again to allow key rotation. After the cache is warm, latency is local and the API can validate during a brief provider outage, but revocation does not automatically affect an already issued JWT.&lt;/p&gt;&#10;&lt;p&gt;It can instead be an &lt;strong&gt;opaque token&lt;/strong&gt;, a reference without meaning to the API. The API authenticates itself to the introspection endpoint and receives, for example, &lt;code&gt;active: true&lt;/code&gt;. That can reflect revocation quickly, at the cost of authorization-server latency, availability, and load. Positive or negative responses can be cached, but the cache lifetime determines how long revocation is delayed.&lt;/p&gt;&#10;&lt;p&gt;An application session is a third option: each request consults—or uses a cache of—an application session store. It makes central invalidation straightforward but makes that store part of the critical path. There is no universal count of identity calls: it depends on JWT or opaque tokens, cache TTLs, gateways, accepted risk, consistency requirements, and topology.&lt;/p&gt;&#10;&lt;p&gt;JWT is a claims format, not OAuth. OAuth can issue JWT or opaque access tokens, and a JWT can be used outside OAuth. A &lt;a href="https://datatracker.ietf.org/doc/html/rfc7517"&gt;JWKS is a set of public JWKs&lt;/a&gt;; &lt;a href="https://datatracker.ietf.org/doc/html/rfc7662"&gt;OAuth introspection&lt;/a&gt; defines the &lt;code&gt;active&lt;/code&gt; signal for centrally managed tokens.&lt;/p&gt;&#10;&lt;h2 id="why-tokens-expire"&gt;Why tokens expire&lt;/h2&gt;&#10;&lt;p&gt;Short expiry reduces the period in which a stolen credential can be used. It does not prevent theft: an attacker can use a valid token until it expires. A refresh token, when the provider grants one, can obtain a new access token without forcing a fresh sign-in. During that renewal, the server can reassess the account, client, consent, and permissions.&lt;/p&gt;&#10;&lt;p&gt;A permission change does not magically invalidate every JWT already issued. Until &lt;code&gt;exp&lt;/code&gt;, an API doing only local validation sees the earlier claims. Depending on risk, systems combine short expiry, introspection or revocation lists for sensitive actions, token versioning, sign-out, and API policy re-evaluation.&lt;/p&gt;&#10;&lt;p&gt;Refresh tokens deserve more protection than a short-lived access token. &lt;strong&gt;Rotation&lt;/strong&gt; issues a replacement on use and invalidates the old one; reuse of an old token can signal theft. &lt;strong&gt;Revocation&lt;/strong&gt; explicitly removes credential or grant validity. &lt;strong&gt;Sign-out&lt;/strong&gt; clears the client session and may notify the provider; &lt;strong&gt;expiry&lt;/strong&gt; happens at &lt;code&gt;exp&lt;/code&gt;; &lt;strong&gt;revocation&lt;/strong&gt; is a server decision before then. They complement one another.&lt;/p&gt;&#10;&lt;h2 id="a-reproducible-local-example-keycloak-javascript-and-fastapi"&gt;A reproducible local example: Keycloak, JavaScript, and FastAPI&lt;/h2&gt;&#10;&lt;p&gt;The following is functional code, not pseudocode, once the described configuration is applied. It deliberately uses in-memory data and a small ownership policy to make the three important checks visible: a valid token, a permission, and a resource. It does not build an OAuth provider or implement cryptography.&lt;/p&gt;&#10;&lt;h3 id="1-start-and-configure-keycloak"&gt;1. Start and configure Keycloak&lt;/h3&gt;&#10;&lt;p&gt;Run a pinned local provider image; this is development configuration, not a production recipe:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;docker run --rm --name photos-keycloak -p 8080:8080 &lt;span style="color:#ae81ff"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; -e KC_BOOTSTRAP_ADMIN_USERNAME&lt;span style="color:#f92672"&gt;=&lt;/span&gt;admin &lt;span style="color:#ae81ff"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; -e KC_BOOTSTRAP_ADMIN_PASSWORD&lt;span style="color:#f92672"&gt;=&lt;/span&gt;admin &lt;span style="color:#ae81ff"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; quay.io/keycloak/keycloak:26.0.7 start-dev&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;At &lt;code&gt;http://localhost:8080/admin/&lt;/code&gt;, create realm &lt;code&gt;photos&lt;/code&gt; and users &lt;code&gt;alice&lt;/code&gt;, &lt;code&gt;bob&lt;/code&gt;, and &lt;code&gt;reader&lt;/code&gt;. Create public OIDC client &lt;code&gt;photos-spa&lt;/code&gt; with &lt;em&gt;Client authentication&lt;/em&gt; off, &lt;em&gt;Standard flow&lt;/em&gt; on, PKCE &lt;code&gt;S256&lt;/code&gt;, redirect URI &lt;code&gt;http://localhost:5173/*&lt;/code&gt;, and web origin &lt;code&gt;http://localhost:5173&lt;/code&gt;. Create &lt;code&gt;photos-api&lt;/code&gt; too, then add an &lt;em&gt;Audience mapper&lt;/em&gt; to &lt;code&gt;photos-spa&lt;/code&gt; with &lt;code&gt;photos-api&lt;/code&gt; as its audience.&lt;/p&gt;&#10;&lt;p&gt;For the exercise, create optional client scopes &lt;code&gt;photos:read&lt;/code&gt; and &lt;code&gt;photos:write&lt;/code&gt;, and request them from the SPA. Assign realm role &lt;code&gt;photo-editor&lt;/code&gt; to Alice and Bob, but not Reader. In a real system, scope or role issuance must come from authorization-server policy, not from a browser writing a scope name in a URL. The extra role makes that distinction observable in this demo.&lt;/p&gt;&#10;&lt;p&gt;Keycloak maintains &lt;a href="https://www.keycloak.org/securing-apps/javascript-adapter"&gt;&lt;code&gt;keycloak-js&lt;/code&gt;&lt;/a&gt;: its documentation says browser clients must be public, the adapter defaults to Authorization Code, and it keeps tokens in memory.&lt;/p&gt;&#10;&lt;h3 id="2-public-javascript-client"&gt;2. Public JavaScript client&lt;/h3&gt;&#10;&lt;p&gt;Install the library and serve the file from &lt;code&gt;http://localhost:5173&lt;/code&gt; with your normal development tool:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;npm install keycloak-js&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-js" data-lang="js"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// main.js — functional SPA demo code&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;import&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;Keycloak&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;from&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;keycloak-js&amp;#34;&lt;/span&gt;;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;keycloak&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;new&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;Keycloak&lt;/span&gt;({&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;url&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;http://localhost:8080&amp;#34;&lt;/span&gt;,&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;realm&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;photos&amp;#34;&lt;/span&gt;,&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;clientId&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;photos-spa&amp;#34;&lt;/span&gt;,&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;});&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;await&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;keycloak&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;init&lt;/span&gt;({&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;onLoad&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;login-required&amp;#34;&lt;/span&gt;,&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;scope&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;photos:read photos:write&amp;#34;&lt;/span&gt;,&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;pkceMethod&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;S256&amp;#34;&lt;/span&gt;,&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;checkLoginIframe&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;false&lt;/span&gt;,&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;});&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;async&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;function&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;api&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;path&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;options&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; {}) {&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;await&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;keycloak&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;updateToken&lt;/span&gt;(&lt;span style="color:#ae81ff"&gt;30&lt;/span&gt;);&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;fetch&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;`http://localhost:8000&lt;/span&gt;&lt;span style="color:#e6db74"&gt;${&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;path&lt;/span&gt;&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;`&lt;/span&gt;, {&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ...&lt;span style="color:#a6e22e"&gt;options&lt;/span&gt;,&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;headers&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; {&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ...&lt;span style="color:#a6e22e"&gt;options&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;headers&lt;/span&gt;,&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;Authorization&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;`Bearer &lt;/span&gt;&lt;span style="color:#e6db74"&gt;${&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;keycloak&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;token&lt;/span&gt;&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;`&lt;/span&gt;,&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;Content-Type&amp;#34;&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;application/json&amp;#34;&lt;/span&gt;,&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; },&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; });&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;export&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;async&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;function&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;readPhoto&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;id&lt;/span&gt;) {&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;api&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;`/photos/&lt;/span&gt;&lt;span style="color:#e6db74"&gt;${&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;id&lt;/span&gt;&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;`&lt;/span&gt;);&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;export&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;async&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;function&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;renamePhoto&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;id&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;title&lt;/span&gt;) {&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;api&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;`/photos/&lt;/span&gt;&lt;span style="color:#e6db74"&gt;${&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;id&lt;/span&gt;&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;`&lt;/span&gt;, {&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;method&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;PATCH&amp;#34;&lt;/span&gt;,&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;body&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;JSON&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;stringify&lt;/span&gt;({ &lt;span style="color:#a6e22e"&gt;title&lt;/span&gt; }),&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; });&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;There is no—and must not be—a &lt;code&gt;client_secret&lt;/code&gt; in browser code: anyone can read it. In-memory access tokens reduce persistence after a later XSS or profile copy, but &lt;strong&gt;do not eliminate XSS&lt;/strong&gt; or protect an already compromised page. Reloading loses the state and resumes the provider session. Never log tokens to console, analytics, URLs, or error reports.&lt;/p&gt;&#10;&lt;h3 id="3-fastapi-validates-the-access-token"&gt;3. FastAPI validates the access token&lt;/h3&gt;&#10;&lt;p&gt;Install PyJWT with cryptographic support:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;python -m pip install &lt;span style="color:#e6db74"&gt;&amp;#34;fastapi[standard]&amp;#34;&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;pyjwt[crypto]&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Save this as &lt;code&gt;app.py&lt;/code&gt; and run &lt;code&gt;fastapi dev app.py&lt;/code&gt;. Its audience matches Keycloak&amp;rsquo;s Audience mapper; allowing only &lt;code&gt;RS256&lt;/code&gt; prevents the untrusted token header from selecting an algorithm.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# app.py — functional demo code; replace PHOTOS with a database&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;from&lt;/span&gt; uuid &lt;span style="color:#f92672"&gt;import&lt;/span&gt; uuid4&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;import&lt;/span&gt; jwt&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;from&lt;/span&gt; fastapi &lt;span style="color:#f92672"&gt;import&lt;/span&gt; Depends, FastAPI, HTTPException, status&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;from&lt;/span&gt; fastapi.middleware.cors &lt;span style="color:#f92672"&gt;import&lt;/span&gt; CORSMiddleware&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;from&lt;/span&gt; fastapi.security &lt;span style="color:#f92672"&gt;import&lt;/span&gt; HTTPAuthorizationCredentials, HTTPBearer&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;from&lt;/span&gt; jwt &lt;span style="color:#f92672"&gt;import&lt;/span&gt; PyJWKClient&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;from&lt;/span&gt; jwt.exceptions &lt;span style="color:#f92672"&gt;import&lt;/span&gt; ExpiredSignatureError, InvalidTokenError, PyJWKClientError&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;from&lt;/span&gt; pydantic &lt;span style="color:#f92672"&gt;import&lt;/span&gt; BaseModel&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;ISSUER &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;http://localhost:8080/realms/photos&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;AUDIENCE &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;photos-api&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;ALGORITHMS &lt;span style="color:#f92672"&gt;=&lt;/span&gt; [&lt;span style="color:#e6db74"&gt;&amp;#34;RS256&amp;#34;&lt;/span&gt;]&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;JWKS_URL &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;f&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#e6db74"&gt;{&lt;/span&gt;ISSUER&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;/protocol/openid-connect/certs&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;jwks_client &lt;span style="color:#f92672"&gt;=&lt;/span&gt; PyJWKClient(JWKS_URL, cache_keys&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;True&lt;/span&gt;, lifespan&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;300&lt;/span&gt;)&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;bearer &lt;span style="color:#f92672"&gt;=&lt;/span&gt; HTTPBearer(auto_error&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;False&lt;/span&gt;)&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;app &lt;span style="color:#f92672"&gt;=&lt;/span&gt; FastAPI()&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;app&lt;span style="color:#f92672"&gt;.&lt;/span&gt;add_middleware(&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; CORSMiddleware,&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; allow_origins&lt;span style="color:#f92672"&gt;=&lt;/span&gt;[&lt;span style="color:#e6db74"&gt;&amp;#34;http://localhost:5173&amp;#34;&lt;/span&gt;],&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; allow_credentials&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;False&lt;/span&gt;,&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; allow_methods&lt;span style="color:#f92672"&gt;=&lt;/span&gt;[&lt;span style="color:#e6db74"&gt;&amp;#34;GET&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;POST&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;PATCH&amp;#34;&lt;/span&gt;],&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; allow_headers&lt;span style="color:#f92672"&gt;=&lt;/span&gt;[&lt;span style="color:#e6db74"&gt;&amp;#34;Authorization&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;Content-Type&amp;#34;&lt;/span&gt;],&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;)&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;PHOTOS: dict[str, dict[str, str]] &lt;span style="color:#f92672"&gt;=&lt;/span&gt; {}&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;PhotoPatch&lt;/span&gt;(BaseModel):&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; title: str&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;def&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;unauthorized&lt;/span&gt;(detail: str) &lt;span style="color:#f92672"&gt;-&amp;gt;&lt;/span&gt; HTTPException:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; HTTPException(&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; status_code&lt;span style="color:#f92672"&gt;=&lt;/span&gt;status&lt;span style="color:#f92672"&gt;.&lt;/span&gt;HTTP_401_UNAUTHORIZED,&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; detail&lt;span style="color:#f92672"&gt;=&lt;/span&gt;detail,&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; headers&lt;span style="color:#f92672"&gt;=&lt;/span&gt;{&lt;span style="color:#e6db74"&gt;&amp;#34;WWW-Authenticate&amp;#34;&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;Bearer&amp;#34;&lt;/span&gt;},&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; )&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;def&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;current_claims&lt;/span&gt;(&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; credentials: HTTPAuthorizationCredentials &lt;span style="color:#f92672"&gt;|&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;None&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; Depends(bearer),&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;) &lt;span style="color:#f92672"&gt;-&amp;gt;&lt;/span&gt; dict:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; credentials &lt;span style="color:#f92672"&gt;is&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;None&lt;/span&gt; &lt;span style="color:#f92672"&gt;or&lt;/span&gt; credentials&lt;span style="color:#f92672"&gt;.&lt;/span&gt;scheme&lt;span style="color:#f92672"&gt;.&lt;/span&gt;lower() &lt;span style="color:#f92672"&gt;!=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;bearer&amp;#34;&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;raise&lt;/span&gt; unauthorized(&lt;span style="color:#e6db74"&gt;&amp;#34;Missing bearer access token&amp;#34;&lt;/span&gt;)&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;try&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; signing_key &lt;span style="color:#f92672"&gt;=&lt;/span&gt; jwks_client&lt;span style="color:#f92672"&gt;.&lt;/span&gt;get_signing_key_from_jwt(credentials&lt;span style="color:#f92672"&gt;.&lt;/span&gt;credentials)&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; jwt&lt;span style="color:#f92672"&gt;.&lt;/span&gt;decode(&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; credentials&lt;span style="color:#f92672"&gt;.&lt;/span&gt;credentials,&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; signing_key&lt;span style="color:#f92672"&gt;.&lt;/span&gt;key,&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; algorithms&lt;span style="color:#f92672"&gt;=&lt;/span&gt;ALGORITHMS,&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; issuer&lt;span style="color:#f92672"&gt;=&lt;/span&gt;ISSUER,&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; audience&lt;span style="color:#f92672"&gt;=&lt;/span&gt;AUDIENCE,&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; options&lt;span style="color:#f92672"&gt;=&lt;/span&gt;{&lt;span style="color:#e6db74"&gt;&amp;#34;require&amp;#34;&lt;/span&gt;: [&lt;span style="color:#e6db74"&gt;&amp;#34;exp&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;iss&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;sub&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;aud&amp;#34;&lt;/span&gt;]},&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; )&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;except&lt;/span&gt; ExpiredSignatureError:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;raise&lt;/span&gt; unauthorized(&lt;span style="color:#e6db74"&gt;&amp;#34;Access token expired&amp;#34;&lt;/span&gt;)&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;except&lt;/span&gt; (InvalidTokenError, PyJWKClientError):&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;raise&lt;/span&gt; unauthorized(&lt;span style="color:#e6db74"&gt;&amp;#34;Invalid access token&amp;#34;&lt;/span&gt;)&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;def&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;require_scope&lt;/span&gt;(required: str):&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;def&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;dependency&lt;/span&gt;(claims: dict &lt;span style="color:#f92672"&gt;=&lt;/span&gt; Depends(current_claims)) &lt;span style="color:#f92672"&gt;-&amp;gt;&lt;/span&gt; dict:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; scopes &lt;span style="color:#f92672"&gt;=&lt;/span&gt; set(claims&lt;span style="color:#f92672"&gt;.&lt;/span&gt;get(&lt;span style="color:#e6db74"&gt;&amp;#34;scope&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;&amp;#34;&lt;/span&gt;)&lt;span style="color:#f92672"&gt;.&lt;/span&gt;split())&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; required &lt;span style="color:#f92672"&gt;not&lt;/span&gt; &lt;span style="color:#f92672"&gt;in&lt;/span&gt; scopes:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;raise&lt;/span&gt; HTTPException(status_code&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;403&lt;/span&gt;, detail&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;f&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;Missing scope: &lt;/span&gt;&lt;span style="color:#e6db74"&gt;{&lt;/span&gt;required&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;)&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; claims&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; dependency&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;def&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;require_editor&lt;/span&gt;(claims: dict) &lt;span style="color:#f92672"&gt;-&amp;gt;&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;None&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; roles &lt;span style="color:#f92672"&gt;=&lt;/span&gt; set(claims&lt;span style="color:#f92672"&gt;.&lt;/span&gt;get(&lt;span style="color:#e6db74"&gt;&amp;#34;realm_access&amp;#34;&lt;/span&gt;, {})&lt;span style="color:#f92672"&gt;.&lt;/span&gt;get(&lt;span style="color:#e6db74"&gt;&amp;#34;roles&amp;#34;&lt;/span&gt;, []))&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;photo-editor&amp;#34;&lt;/span&gt; &lt;span style="color:#f92672"&gt;not&lt;/span&gt; &lt;span style="color:#f92672"&gt;in&lt;/span&gt; roles:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;raise&lt;/span&gt; HTTPException(status_code&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;403&lt;/span&gt;, detail&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;Editor role required&amp;#34;&lt;/span&gt;)&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@app.post&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;/photos&amp;#34;&lt;/span&gt;, status_code&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;201&lt;/span&gt;)&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;def&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;create_photo&lt;/span&gt;(claims: dict &lt;span style="color:#f92672"&gt;=&lt;/span&gt; Depends(require_scope(&lt;span style="color:#e6db74"&gt;&amp;#34;photos:write&amp;#34;&lt;/span&gt;))):&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; require_editor(claims)&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; photo_id &lt;span style="color:#f92672"&gt;=&lt;/span&gt; str(uuid4())&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; PHOTOS[photo_id] &lt;span style="color:#f92672"&gt;=&lt;/span&gt; {&lt;span style="color:#e6db74"&gt;&amp;#34;owner_sub&amp;#34;&lt;/span&gt;: claims[&lt;span style="color:#e6db74"&gt;&amp;#34;sub&amp;#34;&lt;/span&gt;], &lt;span style="color:#e6db74"&gt;&amp;#34;title&amp;#34;&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;New photo&amp;#34;&lt;/span&gt;}&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; {&lt;span style="color:#e6db74"&gt;&amp;#34;id&amp;#34;&lt;/span&gt;: photo_id, &lt;span style="color:#e6db74"&gt;&amp;#34;owner_sub&amp;#34;&lt;/span&gt;: claims[&lt;span style="color:#e6db74"&gt;&amp;#34;sub&amp;#34;&lt;/span&gt;]}&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@app.get&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;/photos/&lt;/span&gt;&lt;span style="color:#e6db74"&gt;{photo_id}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;)&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;def&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;read_photo&lt;/span&gt;(photo_id: str, claims: dict &lt;span style="color:#f92672"&gt;=&lt;/span&gt; Depends(require_scope(&lt;span style="color:#e6db74"&gt;&amp;#34;photos:read&amp;#34;&lt;/span&gt;))):&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; photo &lt;span style="color:#f92672"&gt;=&lt;/span&gt; PHOTOS&lt;span style="color:#f92672"&gt;.&lt;/span&gt;get(photo_id)&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; photo &lt;span style="color:#f92672"&gt;is&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;None&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;raise&lt;/span&gt; HTTPException(status_code&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;404&lt;/span&gt;, detail&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;Photo not found&amp;#34;&lt;/span&gt;)&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; {&lt;span style="color:#e6db74"&gt;&amp;#34;id&amp;#34;&lt;/span&gt;: photo_id, &lt;span style="color:#e6db74"&gt;&amp;#34;title&amp;#34;&lt;/span&gt;: photo[&lt;span style="color:#e6db74"&gt;&amp;#34;title&amp;#34;&lt;/span&gt;]}&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@app.patch&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;/photos/&lt;/span&gt;&lt;span style="color:#e6db74"&gt;{photo_id}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;)&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;def&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;update_photo&lt;/span&gt;(&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; photo_id: str,&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; patch: PhotoPatch,&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; claims: dict &lt;span style="color:#f92672"&gt;=&lt;/span&gt; Depends(require_scope(&lt;span style="color:#e6db74"&gt;&amp;#34;photos:write&amp;#34;&lt;/span&gt;)),&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;):&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; require_editor(claims)&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; photo &lt;span style="color:#f92672"&gt;=&lt;/span&gt; PHOTOS&lt;span style="color:#f92672"&gt;.&lt;/span&gt;get(photo_id)&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; photo &lt;span style="color:#f92672"&gt;is&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;None&lt;/span&gt;:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;raise&lt;/span&gt; HTTPException(status_code&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;404&lt;/span&gt;, detail&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;Photo not found&amp;#34;&lt;/span&gt;)&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; photo[&lt;span style="color:#e6db74"&gt;&amp;#34;owner_sub&amp;#34;&lt;/span&gt;] &lt;span style="color:#f92672"&gt;!=&lt;/span&gt; claims[&lt;span style="color:#e6db74"&gt;&amp;#34;sub&amp;#34;&lt;/span&gt;]:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;raise&lt;/span&gt; HTTPException(status_code&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;403&lt;/span&gt;, detail&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;You do not own this photo&amp;#34;&lt;/span&gt;)&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; photo[&lt;span style="color:#e6db74"&gt;&amp;#34;title&amp;#34;&lt;/span&gt;] &lt;span style="color:#f92672"&gt;=&lt;/span&gt; patch&lt;span style="color:#f92672"&gt;.&lt;/span&gt;title&#10;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; {&lt;span style="color:#e6db74"&gt;&amp;#34;id&amp;#34;&lt;/span&gt;: photo_id, &lt;span style="color:#e6db74"&gt;&amp;#34;title&amp;#34;&lt;/span&gt;: photo[&lt;span style="color:#e6db74"&gt;&amp;#34;title&amp;#34;&lt;/span&gt;]}&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;PyJWKClient&lt;/code&gt; resolves a key by &lt;code&gt;kid&lt;/code&gt;, caches JWKS and keys, and retrieves JWKS again when it cannot find a key; &lt;a href="https://pyjwt.readthedocs.io/en/stable/usage.html"&gt;PyJWT documents both behaviours&lt;/a&gt;. The API neither trusts an unverified decoded &lt;code&gt;sub&lt;/code&gt; nor accepts algorithms outside its configuration. Add HTTPS, limits, secret-free logs, a transactional database, and an explicit private-read policy in production.&lt;/p&gt;&#10;&lt;h3 id="4-manual-verification-matrix"&gt;4. Manual verification matrix&lt;/h3&gt;&#10;&lt;p&gt;After signing in as Alice, create a photo with &lt;code&gt;POST /photos&lt;/code&gt; in the SPA and retain its id. The above configuration should produce:&lt;/p&gt;&#10;&lt;table&gt;&#10;&#9;&lt;thead&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Case&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Request&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Expected result&lt;/th&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/thead&gt;&#10;&#9;&lt;tbody&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Allowed read&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Alice: &lt;code&gt;GET /photos/{id}&lt;/code&gt; with &lt;code&gt;photos:read&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;200.&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Own write&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Alice: &lt;code&gt;PATCH /photos/{id}&lt;/code&gt; with &lt;code&gt;photos:write&lt;/code&gt; and &lt;code&gt;photo-editor&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;200.&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Insufficient scope/role&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Reader: &lt;code&gt;PATCH /photos/{id}&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;403 before any change.&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Someone else&amp;rsquo;s photo&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Bob: &lt;code&gt;PATCH /photos/{id}&lt;/code&gt; with write and editor&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;403: &lt;code&gt;sub&lt;/code&gt; does not equal &lt;code&gt;owner_sub&lt;/code&gt;.&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Wrong audience&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;An access token without &lt;code&gt;aud: photos-api&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;401 in &lt;code&gt;jwt.decode&lt;/code&gt;.&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Expired token&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Temporarily shorten the realm Access Token Lifespan, capture a token, and use it after &lt;code&gt;exp&lt;/code&gt; without calling &lt;code&gt;updateToken&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;401 &lt;code&gt;Access token expired&lt;/code&gt;.&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;p&gt;For the wrong-audience case, create a second identical public client &lt;strong&gt;without&lt;/strong&gt; the Audience mapper, sign in through it, and call the API. The validator must not accept a token minted for a different audience. This is a 401, not a 403: the credential fails the API&amp;rsquo;s basic contract.&lt;/p&gt;&#10;&lt;p&gt;An attractive alternative for web applications is a &lt;strong&gt;BFF&lt;/strong&gt; (Backend for Frontend): a confidential server exchanges the code and stores tokens server-side, while the browser gets only an &lt;code&gt;HttpOnly&lt;/code&gt;, &lt;code&gt;Secure&lt;/code&gt;, suitably &lt;code&gt;SameSite&lt;/code&gt; session cookie. That reduces direct token exposure to JavaScript, but requires CSRF defenses: validate &lt;code&gt;Origin&lt;/code&gt;/&lt;code&gt;Referer&lt;/code&gt; where applicable, use an anti-forgery token for state-changing operations, and never open CORS or cookies to every origin.&lt;/p&gt;&#10;&lt;h2 id="facebook-consent-is-not-unlimited-access"&gt;Facebook: consent is not unlimited access&lt;/h2&gt;&#10;&lt;p&gt;Facebook is a useful reminder that a scope never operates in a vacuum. Its permissions, app mode, test accounts, and app review determine which data a client may receive even after a person consents. The official &lt;a href="https://developers.facebook.com/docs/permissions/reference/user_friends/"&gt;&lt;code&gt;user_friends&lt;/code&gt; documentation&lt;/a&gt; limits results to friends who also use the app and granted that permission; it does not permit freely traversing friends of friends. Permissions needing advanced access are subject to policy and &lt;a href="https://developers.facebook.com/docs/apps/review/"&gt;App Review&lt;/a&gt;.&lt;/p&gt;&#10;&lt;p&gt;So “the user accepted” does not prove that an API will return some data: the provider may restrict it, the app may not be approved, and the resource API retains its own policy. It is the same principle as our gallery: consent and a token are inputs to the decision, not substitutes for it.&lt;/p&gt;&#10;&lt;h2 id="sources-and-further-reading"&gt;Sources and further reading&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;a href="https://datatracker.ietf.org/doc/html/rfc6749"&gt;OAuth 2.0 (RFC 6749)&lt;/a&gt;, &lt;a href="https://datatracker.ietf.org/doc/html/rfc9700"&gt;OAuth 2.0 Security BCP (RFC 9700)&lt;/a&gt;, and &lt;a href="https://datatracker.ietf.org/doc/rfc10017/"&gt;OAuth for Browser-Based Applications (RFC 10017)&lt;/a&gt;.&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://openid.net/specs/openid-connect-core-1_0-18.html"&gt;OpenID Connect Core 1.0&lt;/a&gt; and &lt;a href="https://openid.net/specs/openid-connect-discovery-1_0.html"&gt;OpenID Connect Discovery&lt;/a&gt;.&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf"&gt;SAML 2.0 Core&lt;/a&gt;.&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://datatracker.ietf.org/doc/html/rfc7519"&gt;JWT (RFC 7519)&lt;/a&gt;, &lt;a href="https://datatracker.ietf.org/doc/html/rfc7517"&gt;JWK/JWKS (RFC 7517)&lt;/a&gt;, and &lt;a href="https://datatracker.ietf.org/doc/html/rfc7662"&gt;token introspection (RFC 7662)&lt;/a&gt;.&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://www.keycloak.org/securing-apps/javascript-adapter"&gt;Keycloak JavaScript adapter&lt;/a&gt;, &lt;a href="https://pyjwt.readthedocs.io/en/stable/usage.html"&gt;PyJWT: PyJWKClient usage&lt;/a&gt;, and &lt;a href="https://fastapi.tiangolo.com/tutorial/security/oauth2-jwt/"&gt;FastAPI security&lt;/a&gt;.&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://developers.facebook.com/docs/permissions/reference/user_friends/"&gt;Facebook &lt;code&gt;user_friends&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://developers.facebook.com/docs/apps/review/"&gt;Facebook App Review&lt;/a&gt;.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;</description></item></channel></rss>