Skip to content

Commit

Permalink
OpenidProviderPropertiesResolver
Browse files Browse the repository at this point in the history
  • Loading branch information
ch4mpy committed Feb 9, 2024
1 parent d533ee8 commit 6148894
Show file tree
Hide file tree
Showing 41 changed files with 1,115 additions and 605 deletions.
8 changes: 7 additions & 1 deletion README.MD
Original file line number Diff line number Diff line change
Expand Up @@ -482,7 +482,7 @@ These starters are designed to push auto-configuration one step further. In most
I could forget to update README before releasing, so please refer to [maven central](https://repo1.maven.org/maven2/com/c4-soft/springaddons/spring-addons/) to pick latest available release
```xml
<properties>
<springaddons.version>7.4.1</springaddons.version>
<springaddons.version>7.5.0</springaddons.version>
</properties>
<dependencies>
Expand Down Expand Up @@ -518,6 +518,12 @@ I could forget to update README before releasing, so please refer to [maven cent
### 5.1. <a name="release-notes-7"/>`7.x` Branch
### `7.5.0`
- Create [spring-addons-starter-oidc README](https://github.com/ch4mpy/spring-addons/tree/master/spring-addons-starter-oidc)
- Replace `AuthoritiesMappingPropertiesResolver` with `OpenidProviderPropertiesResolver`
- `OpenidProviderPropertiesResolver` makes multi-tenancy much simpler to implement, including in "dynamic" scenarios (see [spring-addons-starter-oidc README](https://github.com/ch4mpy/spring-addons/tree/master/spring-addons-starter-oidc#1-1-4))
- Fix names of `(Server)HttpSecurityPostProcessor` (synchronised impl where prefixed with `Server` which it shouldn't and reactive weren't when it should)
### `7.4.1`
- [gh-183](https://github.com/ch4mpy/spring-addons/issues/183) Allow anonymous CORS preflight requests (`OPTIONS` requests to a path configured with CORS)
- [gh-184](https://github.com/ch4mpy/spring-addons/issues/184) Configuration properties to add parameters to token requests (necessary for instance to add an `audience` when using client-credentials with Auth0)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,128 +1,44 @@
package com.c4soft.springaddons.tutorials;

import java.net.URI;
import java.net.URISyntaxException;
import java.util.Collection;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
import java.util.Optional;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.convert.converter.Converter;
import org.springframework.http.HttpStatus;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationManagerResolver;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtClaimNames;
import org.springframework.security.oauth2.jwt.JwtDecoders;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationProvider;
import org.springframework.security.oauth2.server.resource.authentication.JwtIssuerAuthenticationManagerResolver;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.util.StringUtils;

import com.c4_soft.springaddons.security.oidc.OAuthentication;
import com.c4_soft.springaddons.security.oidc.OpenidClaimSet;
import com.c4_soft.springaddons.security.oidc.starter.properties.MissingAuthorizationServerConfigurationException;
import com.c4_soft.springaddons.security.oidc.starter.OpenidProviderPropertiesResolver;
import com.c4_soft.springaddons.security.oidc.starter.properties.OpenidProviderProperties;
import com.c4_soft.springaddons.security.oidc.starter.properties.SpringAddonsOidcProperties;
import com.c4_soft.springaddons.security.oidc.starter.synchronised.resourceserver.JwtAbstractAuthenticationTokenConverter;

import jakarta.servlet.http.HttpServletRequest;

@Configuration
@EnableMethodSecurity
public class WebSecurityConfig {
@Bean
JwtAbstractAuthenticationTokenConverter authenticationConverter(
Converter<Map<String, Object>, Collection<? extends GrantedAuthority>> authoritiesConverter,
DynamicTenantProperties addonsProperties) {
return jwt -> {
final var issProperties = addonsProperties.getOpProperties(jwt.getClaims().get(JwtClaimNames.ISS).toString());
return new OAuthentication<>(
new OpenidClaimSet(jwt.getClaims(), issProperties.getUsernameClaim()),
authoritiesConverter.convert(jwt.getClaims()),
jwt.getTokenValue());
};
}

private static URI baseUri(URI uri) {
if (uri == null) {
return null;
}
try {
return new URI(uri.getScheme(), null, uri.getHost(), uri.getPort(), null, null, null);
} catch (URISyntaxException e) {
throw new InvalidIssuerException(uri.toString());
}
}

@Primary
@Component
static class DynamicTenantProperties extends SpringAddonsOidcProperties {
public class IssuerStartsWithOpenidProviderPropertiesResolver implements OpenidProviderPropertiesResolver {
private final SpringAddonsOidcProperties properties;

@Override
public OpenidProviderProperties getOpProperties(String issOrJwks) throws MissingAuthorizationServerConfigurationException {
return super.getOpProperties(baseUri(URI.create(issOrJwks)).toString());
}

}

@Component
static class DynamicTenantsAuthenticationManagerResolver implements AuthenticationManagerResolver<HttpServletRequest> {
private final Set<String> issuerBaseUris;
private final Converter<Jwt, ? extends AbstractAuthenticationToken> jwtAuthenticationConverter;
private final Map<String, JwtAuthenticationProvider> jwtManagers = new ConcurrentHashMap<>();
private final JwtIssuerAuthenticationManagerResolver delegate = new JwtIssuerAuthenticationManagerResolver(
(AuthenticationManagerResolver<String>) this::getAuthenticationManager);

public DynamicTenantsAuthenticationManagerResolver(
SpringAddonsOidcProperties addonsProperties,
Converter<Jwt, ? extends AbstractAuthenticationToken> jwtAuthenticationConverter) {
this.issuerBaseUris = addonsProperties
.getOps()
.stream()
.map(OpenidProviderProperties::getIss)
.map(WebSecurityConfig::baseUri)
.map(URI::toString)
.collect(Collectors.toSet());
this.jwtAuthenticationConverter = jwtAuthenticationConverter;
public IssuerStartsWithOpenidProviderPropertiesResolver(SpringAddonsOidcProperties properties) {
this.properties = properties;
}

@Override
public AuthenticationManager resolve(HttpServletRequest context) {
return delegate.resolve(context);
}

public AuthenticationManager getAuthenticationManager(String issuerUriString) {
final var issuerBaseUri = baseUri(URI.create(issuerUriString)).toString();
if (!issuerBaseUris.contains(issuerBaseUri)) {
throw new InvalidIssuerException(issuerUriString);
}
if (!this.jwtManagers.containsKey(issuerUriString)) {
this.jwtManagers.put(issuerUriString, getProvider(issuerUriString));
}
return jwtManagers.get(issuerUriString)::authenticate;
}

private JwtAuthenticationProvider getProvider(String issuerUriString) {
var provider = new JwtAuthenticationProvider(JwtDecoders.fromIssuerLocation(issuerUriString));
provider.setJwtAuthenticationConverter(jwtAuthenticationConverter);
return provider;
}
}

@ResponseStatus(code = HttpStatus.UNAUTHORIZED)
static class InvalidIssuerException extends RuntimeException {
private static final long serialVersionUID = 4431133205219303797L;

public InvalidIssuerException(String issuerUriString) {
super("Issuer %s is not trusted".formatted(issuerUriString));
public Optional<OpenidProviderProperties> resolve(Map<String, Object> claimSet) {
final var tokenIss = Optional
.ofNullable(claimSet.get(JwtClaimNames.ISS))
.map(Object::toString)
.orElseThrow(() -> new RuntimeException("Invalid token: missing issuer"));
return properties.getOps().stream().filter(opProps -> {
final var opBaseHref = Optional.ofNullable(opProps.getIss()).map(URI::toString).orElse(null);
if (StringUtils.isEmpty(opBaseHref)) {
return false;
}
return tokenIss.startsWith(opBaseHref);
}).findAny();
}
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
scheme: http
origins: ${scheme}://localhost:4200
keycloak-port: 8442
keycloak-port: 8080

server:
error:
include-message: always
port: 7084
ssl:
enabled: false

Expand All @@ -22,19 +22,8 @@ com:
authorities:
- path: $.realm_access.roles
- path: $.resource_access.*.roles
- iss: https://cognito-idp.us-west-2.amazonaws.com
username-claim: username
authorities:
- path: cognito:groups
- iss: https://dev-ch4mpy.eu.auth0.com
username-claim: $['https://c4-soft.com/user']['name']
authorities:
- path: $['https://c4-soft.com/user']['roles']
- path: $.permissions
resourceserver:
cors:
- path: /**
allowed-origin-patterns: ${origins}
enabled: false
permit-all:
- "/actuator/health/readiness"
- "/actuator/health/liveness"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,12 @@
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.AuthorizeHttpRequestsConfigurer;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.oauth2.jwt.JwtClaimNames;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

import com.c4_soft.springaddons.security.oidc.OAuthentication;
import com.c4_soft.springaddons.security.oidc.OpenidClaimSet;
import com.c4_soft.springaddons.security.oidc.starter.properties.SpringAddonsOidcProperties;
import com.c4_soft.springaddons.security.oidc.starter.OpenidProviderPropertiesResolver;
import com.c4_soft.springaddons.security.oidc.starter.properties.NotAConfiguredOpenidProviderException;
import com.c4_soft.springaddons.security.oidc.starter.synchronised.resourceserver.JwtAbstractAuthenticationTokenConverter;
import com.c4_soft.springaddons.security.oidc.starter.synchronised.resourceserver.ResourceServerExpressionInterceptUrlRegistryPostProcessor;

Expand All @@ -28,43 +28,42 @@
import io.swagger.v3.oas.annotations.security.OAuthScope;
import io.swagger.v3.oas.annotations.security.SecurityScheme;

@SecurityScheme(
name = "authorization-code",
type = SecuritySchemeType.OAUTH2,
flows = @OAuthFlows(
authorizationCode = @OAuthFlow(
authorizationUrl = "https://oidc.c4-soft.com/auth/realms/master/protocol/openid-connect/auth",
tokenUrl = "https://oidc.c4-soft.com/auth/realms/master/protocol/openid-connect/token",
scopes = { @OAuthScope(name = "openid"), @OAuthScope(name = "profile") })))
@SecurityScheme(name = "authorization-code", type = SecuritySchemeType.OAUTH2, flows = @OAuthFlows(authorizationCode = @OAuthFlow(authorizationUrl = "https://oidc.c4-soft.com/auth/realms/master/protocol/openid-connect/auth", tokenUrl = "https://oidc.c4-soft.com/auth/realms/master/protocol/openid-connect/token", scopes = {
@OAuthScope(name = "openid"),
@OAuthScope(name = "profile") })))
@SpringBootApplication
public class ResourceServerWithOAuthenticationApplication {

public static void main(String[] args) {
SpringApplication.run(ResourceServerWithOAuthenticationApplication.class, args);
}
public static void main(String[] args) {
SpringApplication.run(ResourceServerWithOAuthenticationApplication.class, args);
}

@Configuration
@EnableMethodSecurity
public static class SecurityConfig {
@Bean
JwtAbstractAuthenticationTokenConverter authenticationConverter(
Converter<Map<String, Object>, Collection<? extends GrantedAuthority>> authoritiesConverter,
SpringAddonsOidcProperties addonsProperties) {
return jwt -> new OAuthentication<>(
new OpenidClaimSet(jwt.getClaims(), addonsProperties.getOpProperties(jwt.getClaims().get(JwtClaimNames.ISS)).getUsernameClaim()),
authoritiesConverter.convert(jwt.getClaims()),
jwt.getTokenValue());
}
@Configuration
@EnableMethodSecurity
public static class SecurityConfig {
@Bean
JwtAbstractAuthenticationTokenConverter authenticationConverter(
Converter<Map<String, Object>, Collection<? extends GrantedAuthority>> authoritiesConverter,
OpenidProviderPropertiesResolver opPropertiesResolver) {
return jwt -> {
final var opProperties = opPropertiesResolver
.resolve(jwt.getClaims())
.orElseThrow(() -> new NotAConfiguredOpenidProviderException(jwt.getClaims()));
final var claims = new OpenidClaimSet(jwt.getClaims(), opProperties.getUsernameClaim());
final var authorities = authoritiesConverter.convert(jwt.getClaims());
return new OAuthentication<>(claims, authorities, jwt.getTokenValue());
};
}

@Bean
ResourceServerExpressionInterceptUrlRegistryPostProcessor expressionInterceptUrlRegistryPostProcessor() {
// @formatter:off
@Bean
ResourceServerExpressionInterceptUrlRegistryPostProcessor expressionInterceptUrlRegistryPostProcessor() {
// @formatter:off
return (AuthorizeHttpRequestsConfigurer<HttpSecurity>.AuthorizationManagerRequestMatcherRegistry registry) -> registry
.requestMatchers(AntPathRequestMatcher.antMatcher(HttpMethod.GET, "/actuator/**")).hasAuthority("OBSERVABILITY:read")
.requestMatchers(new AntPathRequestMatcher("/actuator/**")).hasAuthority("OBSERVABILITY:write")
.anyRequest().authenticated();
// @formatter:on
}
}
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,12 @@
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.oauth2.core.OAuth2AuthenticatedPrincipal;
import org.springframework.security.oauth2.jwt.JwtClaimNames;
import org.springframework.security.oauth2.server.resource.introspection.ReactiveOpaqueTokenAuthenticationConverter;

import com.c4_soft.springaddons.security.oidc.OAuthentication;
import com.c4_soft.springaddons.security.oidc.OpenidClaimSet;
import com.c4_soft.springaddons.security.oidc.starter.properties.SpringAddonsOidcProperties;
import com.c4_soft.springaddons.security.oidc.starter.OpenidProviderPropertiesResolver;
import com.c4_soft.springaddons.security.oidc.starter.properties.NotAConfiguredOpenidProviderException;
import com.c4_soft.springaddons.security.oidc.starter.reactive.resourceserver.ResourceServerAuthorizeExchangeSpecPostProcessor;

import reactor.core.publisher.Mono;
Expand All @@ -24,30 +24,30 @@
@Configuration
public class SecurityConfig {

@Bean
ReactiveOpaqueTokenAuthenticationConverter authenticationConverter(
Converter<Map<String, Object>, Collection<? extends GrantedAuthority>> authoritiesConverter,
SpringAddonsOidcProperties addonsProperties) {
return (
String introspectedToken,
OAuth2AuthenticatedPrincipal authenticatedPrincipal) -> Mono
.just(
new OAuthentication<>(
new OpenidClaimSet(
authenticatedPrincipal.getAttributes(),
addonsProperties.getOpProperties(authenticatedPrincipal.getAttributes().get(JwtClaimNames.ISS))
.getUsernameClaim()),
authoritiesConverter.convert(authenticatedPrincipal.getAttributes()),
introspectedToken));
}

@Bean
ResourceServerAuthorizeExchangeSpecPostProcessor authorizeExchangeSpecPostProcessor() {
// @formatter:off
@Bean
ReactiveOpaqueTokenAuthenticationConverter authenticationConverter(
Converter<Map<String, Object>, Collection<? extends GrantedAuthority>> authoritiesConverter,
OpenidProviderPropertiesResolver opPropertiesResolver) {
return (String introspectedToken, OAuth2AuthenticatedPrincipal authenticatedPrincipal) -> Mono
.just(
new OAuthentication<>(
new OpenidClaimSet(
authenticatedPrincipal.getAttributes(),
opPropertiesResolver
.resolve(authenticatedPrincipal.getAttributes())
.orElseThrow(() -> new NotAConfiguredOpenidProviderException(authenticatedPrincipal.getAttributes()))
.getUsernameClaim()),
authoritiesConverter.convert(authenticatedPrincipal.getAttributes()),
introspectedToken));
}

@Bean
ResourceServerAuthorizeExchangeSpecPostProcessor authorizeExchangeSpecPostProcessor() {
// @formatter:off
return (ServerHttpSecurity.AuthorizeExchangeSpec spec) -> spec
.pathMatchers("/secured-route").hasRole("AUTHORIZED_PERSONNEL")
.anyExchange().authenticated();
// @formatter:on
}
}

}
}
Loading

0 comments on commit 6148894

Please sign in to comment.