Log in from the frontend

Log in from the frontend

Keep the IdP and the backend running, and in a third terminal:

$ cd bikeshare-security/frontend
$ python3 -m http.server 5500

Open http://localhost:5500/index.html (not 127.0.0.1: the IdP and the backend were both told to expect the origin http://localhost:5500 exactly, and browsers treat the two hostnames as different origins).

There is no framework and no build step: index.html and app.js are the whole frontend, written by hand so every step of the flow is visible.

Walking through the code

Open app.js. Clicking Login calls login():

  • It generates a random verifier, and a challenge that is its SHA-256 hash: this is PKCE. The verifier is kept (in sessionStorage, only long enough to survive the redirect) and never sent until the very last step; only its hash is sent now.

  • It builds the /oauth2/authorize URL from the IdP with the challenge, and navigates the whole page there with window.location.href, the same full-page redirect a classic server-rendered login uses.

After the operator logs in on the IdP’s page, it redirects back to http://localhost:5500/index.html?code=…​&state=…​;. DOMContentLoaded notices the code parameter and calls exchangeCodeForToken(), which POST`s to `/oauth2/token with the original verifier: the IdP checks that hashing it reproduces the challenge from the first request, proving this is the same client that started the flow, and only then returns a token.

The access token is kept in a plain JavaScript variable, nothing more: reload the page, and it is gone, by design (see OAuth2 and OIDC).

Why CORS shows up here, and only here

Log in, then open your browser’s network tab and reload the login once more. You will see OPTIONS preflight requests before the real ones.

The authorization redirect itself needed no CORS: it was a full-page navigation, like clicking any link. But the token exchange and every backend call are plain fetch calls from the page’s own origin (http://localhost:5500) to two different origins (http://localhost:9000 and http://localhost:8080). That is exactly what CORS exists to gate, which is why both idp/SecurityConfig.java and backend/SecurityConfig.java each register a CorsConfigurationSource naming http://localhost:5500 explicitly.

Predict, then try: comment out the .cors(Customizer.withDefaults()) line in the backend’s SecurityConfig.java, then restart only the backend (the frontend stays logged in, since nothing there changed). Click Refresh bike list again. The request still reaches the backend (CORS is enforced by the browser, reading headers the server chooses to send), but the browser refuses to let the page read the response, and app.js reports a network error. Restore the line and restart the backend afterward.

Using it

Click Refresh bike list, then Rent a bike. Both calls carry Authorization: Bearer <token>, and both require a scope this token does not have unless the previous page’s checks passed: bikes:read to list, bikes:write to rent.

Reload the page: you are logged out, exactly as decided in OAuth2 and OIDC. Log in again to confirm it is the IdP’s login form you see, not anything the frontend itself drew.