Companion code for the OAuth 2.1 / OIDC beginner series (19 articles). All modules share a single sample app — "Memo", a note-sharing service where third-party clients access the Memo API through OAuth authorization.
- Versions (resolved): Spring Boot 4.1.0 (pulls Spring Security / Authorization Server 7.1.0) / Java 25 LTS / Node 24 / Next.js 16.2.10 / React 19.2.7
- Maven: use the wrapper (
./mvnw) in each Spring module — no separate Maven install needed - Sample data is fictional only (
alice@example.com/bob@example.com). No real secrets or personal data are committed.
| Element | Value |
|---|---|
| Resource | notes (id / owner / title / body) |
| Scopes | notes.read (view) / notes.write (create·update) |
| Roles (RBAC) | ROLE_USER (own notes) / ROLE_ADMIN (all notes) |
| Clients | memo-web (Next.js) / memo-portal (Spring MVC, SSO) / memo-batch (client credentials) / memo-agent (MCP) |
| Sample users | alice / bob (@example.com, dummy) |
| Authorization Server | Spring Authorization Server (default) / Keycloak (art. 014) |
| Module | Status | Article(s) | Purpose |
|---|---|---|---|
auth-server/ |
✅ base | (all) · 007 (OIDC) · 017 (roles) | Spring Authorization Server (SS7.1) — local AS; 007 OIDC test (discovery / ID token / UserInfo); 017 adds a roles claim to access tokens (OAuth2TokenCustomizer) |
resource-server/ |
✅ base | 009 · 017 | Memo notes API (JWT protected). 017 adds RBAC (roles claim → ROLE_ via JwtAuthenticationConverter, hasRole('ADMIN')) and ownership (@PostFilter / @PostAuthorize — a user sees only their own notes) |
client-next/ |
✅ base | 008 | memo-web (Next.js, PKCE authorization code flow) |
e2e/ |
✅ base | 008/010 | Playwright headless happy-path (008) + SSO (010) |
client-spring/ |
✅ | 010 | memo-portal — Spring oauth2Login() client; SSO with memo-web |
keycloak/ |
✅ | 014 | Real IdP — Memo realm import + docker compose (Keycloak 26.6.3). Resource-server's keycloak profile points at it via issuer-uri; verified by KeycloakResourceServerTest (Testcontainers) |
batch/ |
✅ | 015 | memo-batch — client-credentials machine-to-machine client; AuthorizedClientServiceOAuth2AuthorizedClientManager + RestClient; verified by MemoAggregatorTest |
agent/ |
✅ | 018 | memo-agent (MCP) — a plain oauth2Login() client (no Spring AI / MCP starter) that acts as an MCP client: authorization code + PKCE on behalf of the user, plus RFC 8707 audience binding (sends the resource parameter so the token's aud is pinned to the notes API). Verified by ResourceIndicatorAuthorizationRequestResolverTest + context test. The resource-server side (018) adds RFC 9728 Protected Resource Metadata (Spring Security 7.1 native) and RFC 8707 aud validation (AudienceValidator) |
flows/ |
✅ | 012/015/016/017/018 | curl flow scripts — revoke-introspect.sh (012), client-credentials.sh (015), device-grant.sh (016), authorization-scope.sh (017 — scope/role/ownership with real tokens), mcp-agent.sh (018 — RFC 9728 discovery + RFC 8707 audience rejection). Run against a live auth-server + resource-server |
Minimal base =
auth-server+resource-server+client-next+e2e+ CI. Later branches are added incrementally per article.
# Java 25 (via SDKMAN)
source "$HOME/.sdkman/bin/sdkman-init.sh" && sdk use java 25.0.3-tem
# Spring modules
(cd auth-server && ./mvnw -B test && ./mvnw spring-boot:run)
(cd resource-server && ./mvnw -B test)
# Next client
(cd client-next && npm ci && npm run dev)
# e2e
(cd e2e && npm ci && npx playwright test)
# Real IdP (article 014): run Keycloak and point the resource-server at it
docker compose -f keycloak/docker-compose.yml up -d # http://localhost:8081 (admin/admin)
(cd resource-server && SPRING_PROFILES_ACTIVE=keycloak ./mvnw spring-boot:run)CI (.github/workflows/ci.yml) runs the same on ubuntu-latest; a green run is the hard gate for the corresponding code articles.