-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
62933d6
commit b2d2f36
Showing
10 changed files
with
299 additions
and
72 deletions.
There are no files selected for viewing
57 changes: 57 additions & 0 deletions
57
playground-backend/src/main/java/ch/sbb/playgroundbackend/auth/TenantConfig.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
package ch.sbb.playgroundbackend.auth; | ||
|
||
import java.util.ArrayList; | ||
import java.util.List; | ||
import org.springframework.boot.context.properties.ConfigurationProperties; | ||
import org.springframework.boot.context.properties.EnableConfigurationProperties; | ||
import org.springframework.context.annotation.Configuration; | ||
|
||
/** | ||
* Contains the tenant configurations from the application.yml | ||
*/ | ||
@Configuration | ||
@EnableConfigurationProperties | ||
@ConfigurationProperties("auth") | ||
public class TenantConfig { | ||
|
||
private List<Tenant> tenants = new ArrayList<>(); | ||
|
||
public List<Tenant> getTenants() { | ||
return tenants; | ||
} | ||
|
||
public void setTenants(List<Tenant> tenants) { | ||
this.tenants = tenants; | ||
} | ||
|
||
public static class Tenant { | ||
|
||
private String name; | ||
private String jwkSetUri; | ||
private String issuerUri; | ||
|
||
public String getName() { | ||
return name; | ||
} | ||
|
||
public void setName(String name) { | ||
this.name = name; | ||
} | ||
|
||
public String getJwkSetUri() { | ||
return jwkSetUri; | ||
} | ||
|
||
public void setJwkSetUri(String jwkSetUri) { | ||
this.jwkSetUri = jwkSetUri; | ||
} | ||
|
||
public String getIssuerUri() { | ||
return issuerUri; | ||
} | ||
|
||
public void setIssuerUri(String issuerUri) { | ||
this.issuerUri = issuerUri; | ||
} | ||
} | ||
} |
59 changes: 59 additions & 0 deletions
59
playground-backend/src/main/java/ch/sbb/playgroundbackend/auth/TenantJwsKeySelector.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
package ch.sbb.playgroundbackend.auth; | ||
|
||
import ch.sbb.playgroundbackend.auth.TenantConfig.Tenant; | ||
import com.nimbusds.jose.JWSHeader; | ||
import com.nimbusds.jose.KeySourceException; | ||
import com.nimbusds.jose.proc.JWSAlgorithmFamilyJWSKeySelector; | ||
import com.nimbusds.jose.proc.JWSKeySelector; | ||
import com.nimbusds.jose.proc.SecurityContext; | ||
import com.nimbusds.jwt.JWTClaimsSet; | ||
import com.nimbusds.jwt.proc.JWTClaimsSetAwareJWSKeySelector; | ||
import java.net.URL; | ||
import java.security.Key; | ||
import java.util.List; | ||
import java.util.Map; | ||
import java.util.concurrent.ConcurrentHashMap; | ||
import org.springframework.security.oauth2.jwt.JwtClaimNames; | ||
import org.springframework.stereotype.Component; | ||
|
||
/** | ||
* This class (which is used by the bean jwtProcessor, see SecurityConfig.java) provides the functionality to choose which key selector to use based on the iss claim in the JWT. It uses a cache for | ||
* JWKKeySelectors, keyed by tenant identifier. Looking up the tenant is more secure than simply calculating the JWK Set endpoint on the fly - the lookup acts as a list of allowed tenants. | ||
* <p> | ||
* For a more detailed description see <a href="https://docs.spring.io/spring-security/reference/servlet/oauth2/resource-server/multitenancy.html#_parsing_the_claim_only_once">Spring Security | ||
* Documentation</a>. | ||
*/ | ||
@Component | ||
public class TenantJwsKeySelector implements JWTClaimsSetAwareJWSKeySelector<SecurityContext> { | ||
|
||
private final TenantService tenantService; | ||
private final Map<String, JWSKeySelector<SecurityContext>> selectors = new ConcurrentHashMap<>(); | ||
|
||
public TenantJwsKeySelector(TenantService tenantService) { | ||
this.tenantService = tenantService; | ||
} | ||
|
||
@Override | ||
public List<? extends Key> selectKeys(JWSHeader jwsHeader, JWTClaimsSet jwtClaimsSet, SecurityContext securityContext) | ||
throws KeySourceException { | ||
return this.selectors.computeIfAbsent(toTenant(jwtClaimsSet), this::fromTenant) | ||
.selectJWSKeys(jwsHeader, securityContext); | ||
} | ||
|
||
private String toTenant(JWTClaimsSet claimSet) { | ||
return (String) claimSet.getClaim(JwtClaimNames.ISS); | ||
} | ||
|
||
private JWSKeySelector<SecurityContext> fromTenant(String issuerUri) { | ||
final Tenant tenant = tenantService.getByIssuerUri(issuerUri); | ||
return fromUri(tenant.getJwkSetUri()); | ||
} | ||
|
||
private JWSKeySelector<SecurityContext> fromUri(String uri) { | ||
try { | ||
return JWSAlgorithmFamilyJWSKeySelector.fromJWKSetURL(new URL(uri)); | ||
} catch (Exception ex) { | ||
throw new IllegalArgumentException(ex); | ||
} | ||
} | ||
} |
32 changes: 32 additions & 0 deletions
32
playground-backend/src/main/java/ch/sbb/playgroundbackend/auth/TenantService.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
package ch.sbb.playgroundbackend.auth; | ||
|
||
import ch.sbb.playgroundbackend.auth.TenantConfig.Tenant; | ||
import org.apache.logging.log4j.LogManager; | ||
import org.apache.logging.log4j.Logger; | ||
import org.springframework.stereotype.Service; | ||
|
||
/** | ||
* Service providing tenant information based on the iss claim of the JWT token. | ||
*/ | ||
@Service | ||
public class TenantService { | ||
|
||
private static final Logger logger = LogManager.getLogger(TenantService.class); | ||
|
||
private final TenantConfig tenantConfig; | ||
|
||
public TenantService(TenantConfig tenantConfig) { | ||
this.tenantConfig = tenantConfig; | ||
} | ||
|
||
public Tenant getByIssuerUri(String issuerUri) { | ||
Tenant tenant = tenantConfig.getTenants().stream().filter(t -> | ||
issuerUri.equals(t.getIssuerUri()) | ||
).findAny() | ||
.orElseThrow(() -> new IllegalArgumentException("unknown tenant")); | ||
|
||
logger.info(String.format("Got tenant '%s' with issuer URI '%s'", tenant.getName(), tenant.getIssuerUri())); | ||
|
||
return tenant; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
35 changes: 35 additions & 0 deletions
35
...nd/src/main/java/ch/sbb/playgroundbackend/config/DynamicClientRegistrationRepository.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
package ch.sbb.playgroundbackend.config; | ||
|
||
import org.springframework.beans.factory.annotation.Value; | ||
import org.springframework.context.annotation.Configuration; | ||
import org.springframework.security.core.context.SecurityContextHolder; | ||
import org.springframework.security.oauth2.client.registration.ClientRegistration; | ||
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; | ||
import org.springframework.security.oauth2.client.registration.ClientRegistrations; | ||
import org.springframework.security.oauth2.core.AuthorizationGrantType; | ||
import org.springframework.security.oauth2.jwt.Jwt; | ||
import org.springframework.security.oauth2.jwt.JwtClaimNames; | ||
|
||
@Configuration | ||
public class DynamicClientRegistrationRepository implements ClientRegistrationRepository { | ||
|
||
@Value("${auth.exchange.client-id}") | ||
private String clientId; | ||
|
||
@Value("${auth.exchange.client-secret}") | ||
private String clientSecret; | ||
|
||
@Value("${auth.exchange.scope}") | ||
private String scope; | ||
|
||
@Override | ||
public ClientRegistration findByRegistrationId(String registrationId) { | ||
String issuerUri = (String) ((Jwt) (SecurityContextHolder.getContext().getAuthentication().getPrincipal())).getClaims().get(JwtClaimNames.ISS); | ||
return ClientRegistrations.fromIssuerLocation(issuerUri) | ||
.authorizationGrantType(AuthorizationGrantType.JWT_BEARER) | ||
.clientId(clientId) | ||
.clientSecret(clientSecret) | ||
.scope(scope) | ||
.build(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
52 changes: 36 additions & 16 deletions
52
...round-backend/src/main/java/ch/sbb/playgroundbackend/config/WebSecurityConfiguration.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,37 +1,57 @@ | ||
package ch.sbb.playgroundbackend.config; | ||
|
||
import static org.springframework.security.config.Customizer.withDefaults; | ||
|
||
import org.springframework.context.annotation.Bean; | ||
import org.springframework.context.annotation.Configuration; | ||
import org.springframework.context.annotation.Profile; | ||
import org.springframework.security.config.annotation.web.builders.HttpSecurity; | ||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; | ||
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; | ||
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter; | ||
import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter; | ||
import org.springframework.security.web.SecurityFilterChain; | ||
|
||
import static org.springframework.security.config.Customizer.withDefaults; | ||
|
||
@Configuration | ||
@EnableWebSecurity | ||
@Profile("!test") | ||
public class WebSecurityConfiguration { | ||
|
||
private static final String ROLES_KEY = "roles"; | ||
private static final String ROLE_PREFIX = "ROLE_"; | ||
|
||
@Bean | ||
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { | ||
http | ||
.cors(withDefaults()) | ||
.authorizeHttpRequests(authConfig -> { | ||
authConfig.requestMatchers("/swagger-ui/**").permitAll(); | ||
authConfig.requestMatchers("/v3/api-docs/**").permitAll(); | ||
authConfig.requestMatchers("/actuator/health/*").permitAll(); | ||
authConfig.requestMatchers("/actuator/info").permitAll(); | ||
authConfig.requestMatchers("/**").authenticated(); | ||
} | ||
) | ||
// Disable csrf for now as it makes unauthenticated requests return 401/403 | ||
.csrf(AbstractHttpConfigurer::disable) | ||
.oauth2ResourceServer(oauth2 -> | ||
oauth2.jwt(withDefaults()) | ||
); | ||
.cors(withDefaults()) | ||
.authorizeHttpRequests(authConfig -> { | ||
authConfig.requestMatchers("/swagger-ui/**").permitAll(); | ||
authConfig.requestMatchers("/v3/api-docs/**").permitAll(); | ||
authConfig.requestMatchers("/actuator/health/*").permitAll(); | ||
authConfig.requestMatchers("/actuator/info").permitAll(); | ||
authConfig.requestMatchers("/admin/**").hasRole("admin"); | ||
authConfig.requestMatchers("/**").authenticated(); | ||
} | ||
) | ||
// Disable csrf for now as it makes unauthenticated requests return 401/403 | ||
.csrf(AbstractHttpConfigurer::disable) | ||
.oauth2ResourceServer(oauth2 -> | ||
oauth2.jwt(jwtConfigurer -> jwtConfigurer.jwtAuthenticationConverter(jwtAuthenticationConverter())) | ||
); | ||
return http.build(); | ||
} | ||
|
||
@Bean | ||
public JwtAuthenticationConverter jwtAuthenticationConverter() { | ||
// We define a custom role converter to extract the roles from the Entra ID's JWT token and convert them to granted authorities. | ||
// This allows us to do role-based access control on our endpoints. | ||
JwtGrantedAuthoritiesConverter roleConverter = new JwtGrantedAuthoritiesConverter(); | ||
roleConverter.setAuthoritiesClaimName(ROLES_KEY); | ||
roleConverter.setAuthorityPrefix(ROLE_PREFIX); | ||
|
||
JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter(); | ||
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(roleConverter); | ||
|
||
return jwtAuthenticationConverter; | ||
} | ||
} |
Oops, something went wrong.