|
23 | 23 | import eu.openanalytics.containerproxy.ContainerProxyApplication; |
24 | 24 | import eu.openanalytics.containerproxy.auth.IAuthenticationBackend; |
25 | 25 | import eu.openanalytics.containerproxy.auth.UserLogoutHandler; |
| 26 | +import eu.openanalytics.containerproxy.auth.impl.OpenIDAuthenticationBackend; |
26 | 27 | import eu.openanalytics.containerproxy.util.AppRecoveryFilter; |
27 | 28 | import eu.openanalytics.containerproxy.util.EnvironmentUtils; |
28 | 29 | import eu.openanalytics.containerproxy.util.OverridingHeaderWriter; |
|
32 | 33 | import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; |
33 | 34 | import org.springframework.context.annotation.Bean; |
34 | 35 | import org.springframework.context.annotation.Configuration; |
| 36 | +import org.springframework.core.convert.converter.Converter; |
35 | 37 | import org.springframework.core.env.Environment; |
36 | 38 | import org.springframework.security.access.AccessDeniedException; |
| 39 | +import org.springframework.security.authentication.AbstractAuthenticationToken; |
37 | 40 | import org.springframework.security.authentication.AuthenticationManager; |
38 | 41 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; |
39 | 42 | import org.springframework.security.config.annotation.web.builders.WebSecurity; |
40 | 43 | import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; |
41 | 44 | import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; |
42 | 45 | import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer; |
43 | 46 | 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; |
44 | 57 | import org.springframework.security.web.access.AccessDeniedHandler; |
45 | 58 | import org.springframework.security.web.access.AccessDeniedHandlerImpl; |
46 | 59 | import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; |
|
56 | 69 | import javax.servlet.http.HttpServletResponse; |
57 | 70 | import java.io.IOException; |
58 | 71 | import java.util.ArrayList; |
| 72 | +import java.util.Arrays; |
| 73 | +import java.util.HashSet; |
59 | 74 | import java.util.List; |
| 75 | +import java.util.Set; |
60 | 76 |
|
61 | 77 | import static eu.openanalytics.containerproxy.ui.TemplateResolverConfig.PROP_CORS_ALLOWED_ORIGINS; |
62 | 78 |
|
@@ -85,6 +101,10 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter { |
85 | 101 | public static final String PROP_DISABLE_HSTS_HEADER = "proxy.api-security.disable-hsts-header"; |
86 | 102 | public static final String PROP_DISABLE_XSS_PROTECTION_HEADER = "proxy.api-security.disable-xss-protection-header"; |
87 | 103 | 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"; |
88 | 108 |
|
89 | 109 | @Override |
90 | 110 | public void configure(WebSecurity web) { |
@@ -233,6 +253,63 @@ public void handle(HttpServletRequest request, HttpServletResponse response, Acc |
233 | 253 |
|
234 | 254 | // create session cookie even if there is no Authentication in order to support the None authentication backend |
235 | 255 | 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 | + }; |
236 | 313 | } |
237 | 314 |
|
238 | 315 | @Bean(name="authenticationManager") |
|
0 commit comments