Add Spring Authorization Server project: OAuth2/OIDC provider, client and resource server
Three modules on Spring Boot 4.1.1 with Spring Authorization Server 7.1.1: the provider
itself, a relying party, and an API that trusts its tokens. Client registration, PKCE,
a custom consent page and token customisation, with profiles that make each failure
reproducible.
Every claim is backed by captured output in docs/output/as-*.txt, regenerated by
authorization-server/scripts/run-all.sh. Notable findings, verified against the jars:
- OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(HttpSecurity) was deleted
in 7.0, and both configuration classes moved into spring-security-config
- ClientSettings.requireProofKey flipped from false to true, on the authorization server
(1.5.8 -> 7.1.1) and on the OAuth2 client (6.5.1 -> 7.1.1)
- requireProofKey(false) does not make PKCE optional for a public client; the code
verifier is that client's only authentication at the token endpoint
- MediaTypeRequestMatcher(TEXT_HTML) matches Accept: */*, so the token endpoint answers
API callers with 302 -> /login unless setIgnoredMediaTypes(ALL) is called
Also renames the repository to spring-auth-demo and cross-links the new chapter set from
the existing documentation.
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
# The entry-point matcher is left as MediaTypeRequestMatcher(TEXT_HTML) with no ignored
|
||||
# types. `Accept: */*` then matches text/html and the token endpoint redirects API callers
|
||||
# to the HTML login page. See docs/authorization-server/09-entry-point.md.
|
||||
demo:
|
||||
ignore-accept-all: false
|
||||
@@ -0,0 +1,3 @@
|
||||
# The OAuth2TokenCustomizer bean is not registered (see TokenClaimsCustomizer's @Profile).
|
||||
# Diff a token from this profile against a default one to see exactly what the default
|
||||
# access token does and does not carry.
|
||||
@@ -0,0 +1,4 @@
|
||||
# Consent off. The authorization endpoint issues the code immediately after login.
|
||||
# Correct for a first-party client you own; wrong the moment a third party registers.
|
||||
demo:
|
||||
require-consent: false
|
||||
@@ -0,0 +1,4 @@
|
||||
# The public client no longer requires a code_verifier. The code exchange then succeeds
|
||||
# with nothing but the authorization code - which is the whole attack PKCE prevents.
|
||||
demo:
|
||||
require-pkce: false
|
||||
@@ -0,0 +1,4 @@
|
||||
# demo-service gets REFERENCE (opaque) access tokens instead of self-contained JWTs.
|
||||
# The string is meaningless to a resource server without a call to /oauth2/introspect.
|
||||
demo:
|
||||
service-token-format: reference
|
||||
@@ -0,0 +1,5 @@
|
||||
logging:
|
||||
level:
|
||||
org.springframework.security: TRACE
|
||||
org.springframework.security.oauth2.server.authorization: TRACE
|
||||
org.springframework.security.web.FilterChainProxy: DEBUG
|
||||
@@ -0,0 +1,19 @@
|
||||
server:
|
||||
port: 9000
|
||||
|
||||
spring:
|
||||
application:
|
||||
name: auth-server
|
||||
thymeleaf:
|
||||
cache: false
|
||||
|
||||
# Flags the RegisteredClientRepository reads. Every variant profile below flips exactly
|
||||
# one of them, so the resulting failure is attributable to one line.
|
||||
demo:
|
||||
require-consent: true
|
||||
require-pkce: true
|
||||
service-token-format: jwt
|
||||
|
||||
logging:
|
||||
level:
|
||||
org.springframework.security: INFO
|
||||
@@ -0,0 +1,71 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns:th="http://www.thymeleaf.org">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Approve access</title>
|
||||
<style>
|
||||
body { font-family: system-ui, sans-serif; max-width: 34rem; margin: 4rem auto; color: #222; }
|
||||
.card { border: 1px solid #e2e5ea; border-radius: 8px; padding: 1.5rem 1.75rem; }
|
||||
.scope { display: block; margin: .4rem 0; }
|
||||
code { background: #f4f5f7; padding: .1rem .3rem; border-radius: 3px; }
|
||||
button { padding: .5rem 1rem; border-radius: 6px; border: 1px solid #b7bec9; cursor: pointer; }
|
||||
.primary { background: #2f6fdb; color: #fff; border-color: #2f6fdb; }
|
||||
.muted { color: #667; font-size: .9rem; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="card">
|
||||
<h2>Approve access</h2>
|
||||
<p>
|
||||
Signed in as <b th:text="${principalName}">user</b>.
|
||||
The application <code th:text="${clientId}">client</code> wants to act on your behalf.
|
||||
</p>
|
||||
|
||||
<!--
|
||||
The form POSTs to /oauth2/authorize, NOT to /oauth2/consent. The authorization
|
||||
endpoint owns both halves of the exchange; the consent page is only a renderer.
|
||||
-->
|
||||
<form method="post" th:action="@{${requestURI}}">
|
||||
<!-- Echoed back verbatim. Drop `state` and the endpoint cannot correlate this
|
||||
approval with the pending authorization request, and you get a redirect loop. -->
|
||||
<input type="hidden" name="client_id" th:value="${clientId}">
|
||||
<input type="hidden" name="state" th:value="${state}">
|
||||
<input type="hidden" name="user_code" th:value="${userCode}" th:if="${userCode}">
|
||||
|
||||
<p><b>This application will be able to:</b></p>
|
||||
<label class="scope" th:each="s : ${scopes}">
|
||||
<!-- One `scope` parameter per approved scope. A single space-joined value is
|
||||
accepted by the parameter binder and then silently treated as one unknown
|
||||
scope name, so consent appears to succeed and the token comes back short. -->
|
||||
<input type="checkbox" name="scope" th:value="${s}" checked>
|
||||
<code th:text="${s}">scope</code>
|
||||
</label>
|
||||
|
||||
<div th:if="${!previouslyApprovedScopes.isEmpty()}">
|
||||
<p class="muted">Already approved previously:</p>
|
||||
<label class="scope" th:each="s : ${previouslyApprovedScopes}">
|
||||
<input type="checkbox" disabled checked>
|
||||
<code th:text="${s}">scope</code>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<p class="muted">
|
||||
<code>openid</code> is requested implicitly and is not listed — the
|
||||
authorization server never asks for consent on it.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<button class="primary" type="submit" id="approve">Approve</button>
|
||||
</p>
|
||||
</form>
|
||||
|
||||
<form method="post" th:action="@{${requestURI}}">
|
||||
<input type="hidden" name="client_id" th:value="${clientId}">
|
||||
<input type="hidden" name="state" th:value="${state}">
|
||||
<!-- No `scope` parameters at all is how you say "denied". The endpoint then redirects
|
||||
back to the client with error=access_denied. -->
|
||||
<button type="submit" id="deny">Deny</button>
|
||||
</form>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user