Skip to content

Commit fba2561

Browse files
jzheauxrwinch
authored andcommitted
Reactive Opaque Token Support
Fixes: gh-6513
1 parent 43587b4 commit fba2561

File tree

4 files changed

+838
-86
lines changed

4 files changed

+838
-86
lines changed

config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java

+155-72
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,24 @@
1616

1717
package org.springframework.security.config.web.server;
1818

19+
import java.io.IOException;
20+
import java.io.PrintWriter;
21+
import java.io.StringWriter;
22+
import java.security.interfaces.RSAPublicKey;
23+
import java.time.Duration;
24+
import java.util.ArrayList;
25+
import java.util.Arrays;
26+
import java.util.Collections;
27+
import java.util.HashMap;
28+
import java.util.List;
29+
import java.util.Map;
30+
import java.util.Optional;
31+
import java.util.UUID;
32+
import java.util.function.Function;
33+
34+
import reactor.core.publisher.Mono;
35+
import reactor.util.context.Context;
36+
1937
import org.springframework.beans.BeansException;
2038
import org.springframework.context.ApplicationContext;
2139
import org.springframework.core.Ordered;
@@ -65,6 +83,7 @@
6583
import org.springframework.security.oauth2.jwt.ReactiveJwtDecoderFactory;
6684
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
6785
import org.springframework.security.oauth2.server.resource.authentication.JwtReactiveAuthenticationManager;
86+
import org.springframework.security.oauth2.server.resource.authentication.OAuth2IntrospectionReactiveAuthenticationManager;
6887
import org.springframework.security.oauth2.server.resource.authentication.ReactiveJwtAuthenticationConverterAdapter;
6988
import org.springframework.security.oauth2.server.resource.web.access.server.BearerTokenServerAccessDeniedHandler;
7089
import org.springframework.security.oauth2.server.resource.web.server.BearerTokenServerAuthenticationEntryPoint;
@@ -143,23 +162,6 @@
143162
import org.springframework.web.server.ServerWebExchange;
144163
import org.springframework.web.server.WebFilter;
145164
import org.springframework.web.server.WebFilterChain;
146-
import reactor.core.publisher.Mono;
147-
import reactor.util.context.Context;
148-
149-
import java.io.IOException;
150-
import java.io.PrintWriter;
151-
import java.io.StringWriter;
152-
import java.security.interfaces.RSAPublicKey;
153-
import java.time.Duration;
154-
import java.util.ArrayList;
155-
import java.util.Arrays;
156-
import java.util.Collections;
157-
import java.util.HashMap;
158-
import java.util.List;
159-
import java.util.Map;
160-
import java.util.Optional;
161-
import java.util.UUID;
162-
import java.util.function.Function;
163165

164166
import static org.springframework.security.web.server.DelegatingServerAuthenticationEntryPoint.DelegateEntry;
165167
import static org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher.MatchResult.match;
@@ -994,8 +996,11 @@ public class OAuth2ResourceServerSpec {
994996
private ServerAuthenticationEntryPoint entryPoint = new BearerTokenServerAuthenticationEntryPoint();
995997
private ServerAccessDeniedHandler accessDeniedHandler = new BearerTokenServerAccessDeniedHandler();
996998
private ServerAuthenticationConverter bearerTokenConverter = new ServerBearerTokenAuthenticationConverter();
999+
private BearerTokenServerWebExchangeMatcher bearerTokenServerWebExchangeMatcher =
1000+
new BearerTokenServerWebExchangeMatcher();
9971001

9981002
private JwtSpec jwt;
1003+
private OpaqueTokenSpec opaqueToken;
9991004

10001005
/**
10011006
* Configures the {@link ServerAccessDeniedHandler} to use for requests authenticating with
@@ -1047,10 +1052,94 @@ public JwtSpec jwt() {
10471052
return this.jwt;
10481053
}
10491054

1055+
public OpaqueTokenSpec opaqueToken() {
1056+
if (this.opaqueToken == null) {
1057+
this.opaqueToken = new OpaqueTokenSpec();
1058+
}
1059+
return this.opaqueToken;
1060+
}
1061+
10501062
protected void configure(ServerHttpSecurity http) {
1063+
this.bearerTokenServerWebExchangeMatcher
1064+
.setBearerTokenConverter(this.bearerTokenConverter);
1065+
1066+
registerDefaultAccessDeniedHandler(http);
1067+
registerDefaultAuthenticationEntryPoint(http);
1068+
registerDefaultCsrfOverride(http);
1069+
1070+
if (this.jwt != null && this.opaqueToken != null) {
1071+
throw new IllegalStateException("Spring Security only supports JWTs or Opaque Tokens, not both at the " +
1072+
"same time");
1073+
}
1074+
1075+
if (this.jwt == null && this.opaqueToken == null) {
1076+
throw new IllegalStateException("Jwt and Opaque Token are the only supported formats for bearer tokens " +
1077+
"in Spring Security and neither was found. Make sure to configure JWT " +
1078+
"via http.oauth2ResourceServer().jwt() or Opaque Tokens via " +
1079+
"http.oauth2ResourceServer().opaqueToken().");
1080+
}
1081+
10511082
if (this.jwt != null) {
10521083
this.jwt.configure(http);
10531084
}
1085+
1086+
if (this.opaqueToken != null) {
1087+
this.opaqueToken.configure(http);
1088+
}
1089+
}
1090+
1091+
private void registerDefaultAccessDeniedHandler(ServerHttpSecurity http) {
1092+
if ( http.exceptionHandling != null ) {
1093+
http.defaultAccessDeniedHandlers.add(
1094+
new ServerWebExchangeDelegatingServerAccessDeniedHandler.DelegateEntry(
1095+
this.bearerTokenServerWebExchangeMatcher,
1096+
OAuth2ResourceServerSpec.this.accessDeniedHandler
1097+
)
1098+
);
1099+
}
1100+
}
1101+
1102+
private void registerDefaultAuthenticationEntryPoint(ServerHttpSecurity http) {
1103+
if (http.exceptionHandling != null) {
1104+
http.defaultEntryPoints.add(
1105+
new DelegateEntry(
1106+
this.bearerTokenServerWebExchangeMatcher,
1107+
OAuth2ResourceServerSpec.this.entryPoint
1108+
)
1109+
);
1110+
}
1111+
}
1112+
1113+
private void registerDefaultCsrfOverride(ServerHttpSecurity http) {
1114+
if ( http.csrf != null && !http.csrf.specifiedRequireCsrfProtectionMatcher ) {
1115+
http
1116+
.csrf()
1117+
.requireCsrfProtectionMatcher(
1118+
new AndServerWebExchangeMatcher(
1119+
CsrfWebFilter.DEFAULT_CSRF_MATCHER,
1120+
new NegatedServerWebExchangeMatcher(
1121+
this.bearerTokenServerWebExchangeMatcher)));
1122+
}
1123+
}
1124+
1125+
private class BearerTokenServerWebExchangeMatcher implements ServerWebExchangeMatcher {
1126+
ServerAuthenticationConverter bearerTokenConverter;
1127+
1128+
@Override
1129+
public Mono<MatchResult> matches(ServerWebExchange exchange) {
1130+
return this.bearerTokenConverter.convert(exchange)
1131+
.flatMap(this::nullAuthentication)
1132+
.onErrorResume(e -> notMatch());
1133+
}
1134+
1135+
public void setBearerTokenConverter(ServerAuthenticationConverter bearerTokenConverter) {
1136+
Assert.notNull(bearerTokenConverter, "bearerTokenConverter cannot be null");
1137+
this.bearerTokenConverter = bearerTokenConverter;
1138+
}
1139+
1140+
private Mono<MatchResult> nullAuthentication(Authentication authentication) {
1141+
return authentication == null ? notMatch() : match();
1142+
}
10541143
}
10551144

10561145
/**
@@ -1062,9 +1151,6 @@ public class JwtSpec {
10621151
private Converter<Jwt, ? extends Mono<? extends AbstractAuthenticationToken>> jwtAuthenticationConverter
10631152
= new ReactiveJwtAuthenticationConverterAdapter(new JwtAuthenticationConverter());
10641153

1065-
private BearerTokenServerWebExchangeMatcher bearerTokenServerWebExchangeMatcher =
1066-
new BearerTokenServerWebExchangeMatcher();
1067-
10681154
/**
10691155
* Configures the {@link ReactiveAuthenticationManager} to use
10701156
* @param authenticationManager the authentication manager to use
@@ -1128,17 +1214,10 @@ public OAuth2ResourceServerSpec and() {
11281214
}
11291215

11301216
protected void configure(ServerHttpSecurity http) {
1131-
this.bearerTokenServerWebExchangeMatcher.setBearerTokenConverter(bearerTokenConverter);
1132-
1133-
registerDefaultAccessDeniedHandler(http);
1134-
registerDefaultAuthenticationEntryPoint(http);
1135-
registerDefaultCsrfOverride(http);
1136-
11371217
ReactiveAuthenticationManager authenticationManager = getAuthenticationManager();
11381218
AuthenticationWebFilter oauth2 = new AuthenticationWebFilter(authenticationManager);
11391219
oauth2.setServerAuthenticationConverter(bearerTokenConverter);
11401220
oauth2.setAuthenticationFailureHandler(new ServerAuthenticationEntryPointFailureHandler(entryPoint));
1141-
11421221
http
11431222
.addFilterAt(oauth2, SecurityWebFiltersOrder.AUTHENTICATION);
11441223
}
@@ -1170,59 +1249,63 @@ private ReactiveAuthenticationManager getAuthenticationManager() {
11701249

11711250
return authenticationManager;
11721251
}
1252+
}
11731253

1174-
private void registerDefaultAccessDeniedHandler(ServerHttpSecurity http) {
1175-
if ( http.exceptionHandling != null ) {
1176-
http.defaultAccessDeniedHandlers.add(
1177-
new ServerWebExchangeDelegatingServerAccessDeniedHandler.DelegateEntry(
1178-
this.bearerTokenServerWebExchangeMatcher,
1179-
OAuth2ResourceServerSpec.this.accessDeniedHandler
1180-
)
1181-
);
1182-
}
1183-
}
1254+
/**
1255+
* Configures Opaque Token Resource Server support
1256+
*
1257+
* @author Josh Cummings
1258+
* @since 5.2
1259+
*/
1260+
public class OpaqueTokenSpec {
1261+
private String introspectionUri;
1262+
private String introspectionClientId;
1263+
private String introspectionClientSecret;
11841264

1185-
private void registerDefaultAuthenticationEntryPoint(ServerHttpSecurity http) {
1186-
if (http.exceptionHandling != null) {
1187-
http.defaultEntryPoints.add(
1188-
new DelegateEntry(
1189-
this.bearerTokenServerWebExchangeMatcher,
1190-
OAuth2ResourceServerSpec.this.entryPoint
1191-
)
1192-
);
1193-
}
1265+
/**
1266+
* Configures the URI of the Introspection endpoint
1267+
* @param introspectionUri The URI of the Introspection endpoint
1268+
* @return the {@code OpaqueTokenSpec} for additional configuration
1269+
*/
1270+
public OpaqueTokenSpec introspectionUri(String introspectionUri) {
1271+
Assert.hasText(introspectionUri, "introspectionUri cannot be empty");
1272+
this.introspectionUri = introspectionUri;
1273+
return this;
11941274
}
11951275

1196-
private void registerDefaultCsrfOverride(ServerHttpSecurity http) {
1197-
if ( http.csrf != null && !http.csrf.specifiedRequireCsrfProtectionMatcher ) {
1198-
http
1199-
.csrf()
1200-
.requireCsrfProtectionMatcher(
1201-
new AndServerWebExchangeMatcher(
1202-
CsrfWebFilter.DEFAULT_CSRF_MATCHER,
1203-
new NegatedServerWebExchangeMatcher(
1204-
this.bearerTokenServerWebExchangeMatcher)));
1205-
}
1276+
/**
1277+
* Configures the credentials for Introspection endpoint
1278+
* @param clientId The clientId part of the credentials
1279+
* @param clientSecret The clientSecret part of the credentials
1280+
* @return the {@code OpaqueTokenSpec} for additional configuration
1281+
*/
1282+
public OpaqueTokenSpec introspectionClientCredentials(String clientId, String clientSecret) {
1283+
Assert.hasText(clientId, "clientId cannot be empty");
1284+
Assert.notNull(clientSecret, "clientSecret cannot be null");
1285+
this.introspectionClientId = clientId;
1286+
this.introspectionClientSecret = clientSecret;
1287+
return this;
12061288
}
12071289

1208-
private class BearerTokenServerWebExchangeMatcher implements ServerWebExchangeMatcher {
1209-
ServerAuthenticationConverter bearerTokenConverter;
1210-
1211-
@Override
1212-
public Mono<MatchResult> matches(ServerWebExchange exchange) {
1213-
return this.bearerTokenConverter.convert(exchange)
1214-
.flatMap(this::nullAuthentication)
1215-
.onErrorResume(e -> notMatch());
1216-
}
1290+
/**
1291+
* Allows method chaining to continue configuring the {@link ServerHttpSecurity}
1292+
* @return the {@link ServerHttpSecurity} to continue configuring
1293+
*/
1294+
public OAuth2ResourceServerSpec and() {
1295+
return OAuth2ResourceServerSpec.this;
1296+
}
12171297

1218-
public void setBearerTokenConverter(ServerAuthenticationConverter bearerTokenConverter) {
1219-
Assert.notNull(bearerTokenConverter, "bearerTokenConverter cannot be null");
1220-
this.bearerTokenConverter = bearerTokenConverter;
1221-
}
1298+
protected ReactiveAuthenticationManager getAuthenticationManager() {
1299+
return new OAuth2IntrospectionReactiveAuthenticationManager(
1300+
this.introspectionUri, this.introspectionClientId, this.introspectionClientSecret);
1301+
}
12221302

1223-
private Mono<MatchResult> nullAuthentication(Authentication authentication) {
1224-
return authentication == null ? notMatch() : match();
1225-
}
1303+
protected void configure(ServerHttpSecurity http) {
1304+
ReactiveAuthenticationManager authenticationManager = getAuthenticationManager();
1305+
AuthenticationWebFilter oauth2 = new AuthenticationWebFilter(authenticationManager);
1306+
oauth2.setServerAuthenticationConverter(bearerTokenConverter);
1307+
oauth2.setAuthenticationFailureHandler(new ServerAuthenticationEntryPointFailureHandler(entryPoint));
1308+
http.addFilterAt(oauth2, SecurityWebFiltersOrder.AUTHENTICATION);
12261309
}
12271310
}
12281311

0 commit comments

Comments
 (0)