Skip to content

Commit a4b0374

Browse files
committed
Fix #27809: re-implement OAuth Resource Server
1 parent a847c82 commit a4b0374

3 files changed

Lines changed: 81 additions & 119 deletions

File tree

pom.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,10 @@
149149
<groupId>org.springframework.security</groupId>
150150
<artifactId>spring-security-oauth2-jose</artifactId>
151151
</dependency>
152+
<dependency>
153+
<groupId>org.springframework.security</groupId>
154+
<artifactId>spring-security-oauth2-resource-server</artifactId>
155+
</dependency>
152156
<dependency>
153157
<groupId>org.springframework.security</groupId>
154158
<artifactId>spring-security-test</artifactId>

src/main/java/eu/openanalytics/containerproxy/security/APISecurityConfig.java

Lines changed: 0 additions & 119 deletions
This file was deleted.

src/main/java/eu/openanalytics/containerproxy/security/WebSecurityConfig.java

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import eu.openanalytics.containerproxy.ContainerProxyApplication;
2424
import eu.openanalytics.containerproxy.auth.IAuthenticationBackend;
2525
import eu.openanalytics.containerproxy.auth.UserLogoutHandler;
26+
import eu.openanalytics.containerproxy.auth.impl.OpenIDAuthenticationBackend;
2627
import eu.openanalytics.containerproxy.util.AppRecoveryFilter;
2728
import eu.openanalytics.containerproxy.util.EnvironmentUtils;
2829
import eu.openanalytics.containerproxy.util.OverridingHeaderWriter;
@@ -32,15 +33,27 @@
3233
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
3334
import org.springframework.context.annotation.Bean;
3435
import org.springframework.context.annotation.Configuration;
36+
import org.springframework.core.convert.converter.Converter;
3537
import org.springframework.core.env.Environment;
3638
import org.springframework.security.access.AccessDeniedException;
39+
import org.springframework.security.authentication.AbstractAuthenticationToken;
3740
import org.springframework.security.authentication.AuthenticationManager;
3841
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
3942
import org.springframework.security.config.annotation.web.builders.WebSecurity;
4043
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
4144
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
4245
import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer;
4346
import org.springframework.security.config.http.SessionCreationPolicy;
47+
import org.springframework.security.core.GrantedAuthority;
48+
import org.springframework.security.core.authority.SimpleGrantedAuthority;
49+
import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator;
50+
import org.springframework.security.oauth2.core.OAuth2Error;
51+
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
52+
import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult;
53+
import org.springframework.security.oauth2.jwt.Jwt;
54+
import org.springframework.security.oauth2.jwt.JwtTimestampValidator;
55+
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
56+
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
4457
import org.springframework.security.web.access.AccessDeniedHandler;
4558
import org.springframework.security.web.access.AccessDeniedHandlerImpl;
4659
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
@@ -56,7 +69,10 @@
5669
import javax.servlet.http.HttpServletResponse;
5770
import java.io.IOException;
5871
import java.util.ArrayList;
72+
import java.util.Arrays;
73+
import java.util.HashSet;
5974
import java.util.List;
75+
import java.util.Set;
6076

6177
import static eu.openanalytics.containerproxy.ui.TemplateResolverConfig.PROP_CORS_ALLOWED_ORIGINS;
6278

@@ -85,6 +101,10 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
85101
public static final String PROP_DISABLE_HSTS_HEADER = "proxy.api-security.disable-hsts-header";
86102
public static final String PROP_DISABLE_XSS_PROTECTION_HEADER = "proxy.api-security.disable-xss-protection-header";
87103
public static final String PROP_CUSTOM_HEADERS = "proxy.api-security.custom-headers";
104+
public static final String PROP_OAUTH2_RESOURCE_ID = "proxy.oauth2.resource-id";
105+
public static final String PROP_OAUTH2_JWKS_URL = "proxy.oauth2.jwks-url";
106+
public static final String PROP_OAUTH2_ROLES_CLAIM = "proxy.oauth2.roles-claim";
107+
public static final String PROP_OAUTH2_USERNAME_ATTRIBUTE = "proxy.oauth2.username-attribute";
88108

89109
@Override
90110
public void configure(WebSecurity web) {
@@ -233,6 +253,63 @@ public void handle(HttpServletRequest request, HttpServletResponse response, Acc
233253

234254
// create session cookie even if there is no Authentication in order to support the None authentication backend
235255
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.ALWAYS);
256+
257+
258+
String oauth2JwksUri = environment.getProperty(PROP_OAUTH2_JWKS_URL);
259+
String resourceId = environment.getProperty(PROP_OAUTH2_RESOURCE_ID);
260+
if (oauth2JwksUri != null && resourceId != null) {
261+
http.oauth2ResourceServer()
262+
.jwt()
263+
.decoder(jwtDecoder(oauth2JwksUri, resourceId))
264+
.jwtAuthenticationConverter(jwtAuthenticationConverter());
265+
}
266+
}
267+
268+
private NimbusJwtDecoder jwtDecoder(String oauth2JwksUri, String resourceId) {
269+
String usernameClaim = environment.getProperty(PROP_OAUTH2_USERNAME_ATTRIBUTE, "sub");
270+
OAuth2TokenValidator<Jwt> audienceValidator = token -> {
271+
if (token.getAudience().contains(resourceId)) {
272+
return OAuth2TokenValidatorResult.success();
273+
} else {
274+
return OAuth2TokenValidatorResult.failure(new OAuth2Error("custom_code", "Invalid audience", null));
275+
}
276+
};
277+
278+
OAuth2TokenValidator<Jwt> usernameValidator = token -> {
279+
if (token.hasClaim(usernameClaim)) {
280+
return OAuth2TokenValidatorResult.success();
281+
} else {
282+
return OAuth2TokenValidatorResult.failure(new OAuth2Error("custom_code", "Username claim missing", null));
283+
}
284+
};
285+
286+
DelegatingOAuth2TokenValidator<Jwt> validators = new DelegatingOAuth2TokenValidator<>(Arrays.asList(new JwtTimestampValidator(), audienceValidator, usernameValidator));
287+
288+
NimbusJwtDecoder decoder = NimbusJwtDecoder.withJwkSetUri(oauth2JwksUri).build();
289+
decoder.setJwtValidator(validators);
290+
291+
return decoder;
292+
}
293+
294+
private Converter<Jwt, AbstractAuthenticationToken> jwtAuthenticationConverter() {
295+
String rolesClaim = environment.getProperty(PROP_OAUTH2_ROLES_CLAIM);
296+
String usernameClaim = environment.getProperty(PROP_OAUTH2_USERNAME_ATTRIBUTE, "sub");
297+
return source -> {
298+
Set<GrantedAuthority> mappedAuthorities = new HashSet<>();
299+
if (rolesClaim != null) {
300+
Object claimValue = source.getClaim(rolesClaim);
301+
for (String role : OpenIDAuthenticationBackend.parseRolesClaim(logger, rolesClaim, claimValue)) {
302+
String mappedRole = role.toUpperCase().startsWith("ROLE_") ? role : "ROLE_" + role;
303+
mappedAuthorities.add(new SimpleGrantedAuthority(mappedRole.toUpperCase()));
304+
}
305+
}
306+
307+
String principalClaimValue = source.getClaimAsString(usernameClaim);
308+
if (principalClaimValue == null) {
309+
throw new IllegalArgumentException(String.format("Cannot extract username from OAuth token, no claim %s found", usernameClaim));
310+
}
311+
return new JwtAuthenticationToken(source, mappedAuthorities, principalClaimValue);
312+
};
236313
}
237314

238315
@Bean(name="authenticationManager")

0 commit comments

Comments
 (0)