Skip to content

Commit

Permalink
feat: internal authorization service
Browse files Browse the repository at this point in the history
Core authorization token
  • Loading branch information
matteo-s authored Aug 5, 2024
2 parents 2cd56e0 + 51a24be commit 5365353
Show file tree
Hide file tree
Showing 21 changed files with 1,421 additions and 21 deletions.
7 changes: 6 additions & 1 deletion application/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,12 @@
<groupId>it.smartcommunitylabdhub</groupId>
<artifactId>dh-files</artifactId>
<version>${revision}</version>
</dependency>
</dependency>
<dependency>
<groupId>it.smartcommunitylabdhub</groupId>
<artifactId>dh-authorization</artifactId>
<version>${revision}</version>
</dependency>
</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package it.smartcommunitylabdhub.core.components.run;

import it.smartcommunitylabdhub.authorization.services.JwtTokenService;
import it.smartcommunitylabdhub.commons.accessors.fields.StatusFieldAccessor;
import it.smartcommunitylabdhub.commons.accessors.spec.RunSpecAccessor;
import it.smartcommunitylabdhub.commons.config.SecurityProperties;
import it.smartcommunitylabdhub.commons.events.RunnableChangedEvent;
import it.smartcommunitylabdhub.commons.events.RunnableMonitorObject;
import it.smartcommunitylabdhub.commons.exceptions.NoSuchEntityException;
Expand Down Expand Up @@ -78,6 +80,12 @@ public class RunManager {
@Autowired
ProcessorRegistry processorRegistry;

@Autowired
JwtTokenService jwtTokenService;

@Autowired
SecurityProperties securityProperties;

public Run build(@NotNull Run run) throws NoSuchEntityException {
// GET state machine, init state machine with status
RunBaseSpec runBaseSpec = new RunBaseSpec();
Expand Down Expand Up @@ -196,9 +204,13 @@ public Run run(@NotNull Run run) throws NoSuchEntityException, InvalidTransactio
//extract auth from security context to inflate secured credentials
//TODO refactor properly
if (r instanceof SecuredRunnable) {
// check that auth is enabled via securityProperties
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth != null) {
((SecuredRunnable) r).setCredentials(auth);
if (auth != null && securityProperties.isRequired()) {
Serializable credentials = jwtTokenService.generateCredentials(auth);
if (credentials != null) {
((SecuredRunnable) r).setCredentials(credentials);
}
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,24 @@
package it.smartcommunitylabdhub.core.config;

import com.nimbusds.jose.JOSEException;
import com.nimbusds.jose.jwk.JWK;
import com.nimbusds.jose.jwk.RSAKey;
import it.smartcommunitylabdhub.authorization.config.KeyStoreConfig;
import it.smartcommunitylabdhub.commons.config.ApplicationProperties;
import it.smartcommunitylabdhub.commons.config.SecurityProperties;
import jakarta.servlet.http.HttpServletRequest;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.domain.AuditorAware;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
Expand All @@ -23,13 +30,15 @@
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator;
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
import org.springframework.security.oauth2.core.oidc.IdTokenClaimNames;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtClaimNames;
import org.springframework.security.oauth2.jwt.JwtClaimValidator;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.JwtValidators;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationProvider;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.access.AccessDeniedHandlerImpl;
Expand All @@ -48,6 +57,9 @@ public class SecurityConfig {

public static final String API_PREFIX = "/api";

@Autowired
ApplicationProperties applicationProperties;

@Autowired
SecurityProperties properties;

Expand All @@ -60,6 +72,15 @@ public class SecurityConfig {
@Value("${management.endpoints.web.base-path}")
private String managementBasePath;

@Value("${jwt.client-id}")
private String clientId;

@Value("${jwt.client-secret}")
private String clientSecret;

@Autowired
KeyStoreConfig keyStoreConfig;

@Bean("apiSecurityFilterChain")
public SecurityFilterChain apiSecurityFilterChain(HttpSecurity http) throws Exception {
HttpSecurity securityChain = http
Expand All @@ -85,18 +106,37 @@ public SecurityFilterChain apiSecurityFilterChain(HttpSecurity http) throws Exce

//authentication (when configured)
if (properties.isRequired()) {
if (properties.isBasicAuthEnabled()) {
securityChain
.httpBasic(basic -> basic.authenticationEntryPoint(new Http403ForbiddenEntryPoint()))
.userDetailsService(userDetailsService());
}
//always enable internal jwt auth provider
JwtAuthenticationProvider coreJwtAuthProvider = new JwtAuthenticationProvider(coreJwtDecoder());
coreJwtAuthProvider.setJwtAuthenticationConverter(coreJwtAuthenticationConverter());

// Create authentication Manager
securityChain.oauth2ResourceServer(oauth2 ->
oauth2.jwt(jwt -> jwt.authenticationManager(new ProviderManager(coreJwtAuthProvider)))
);

if (properties.isJwtAuthEnabled()) {
// rebuild auth manager to include external jwt provider
JwtAuthenticationProvider externalJwtAuthProvider = new JwtAuthenticationProvider(externalJwtDecoder());

externalJwtAuthProvider.setJwtAuthenticationConverter(externalJwtAuthenticationConverter());

securityChain.oauth2ResourceServer(oauth2 ->
oauth2.jwt(jwt -> jwt.decoder(jwtDecoder()).jwtAuthenticationConverter(jwtAuthenticationConverter())
oauth2.jwt(jwt ->
jwt.authenticationManager(new ProviderManager(coreJwtAuthProvider, externalJwtAuthProvider))
)
);
}

//enable basic if required
if (properties.isBasicAuthEnabled()) {
securityChain
.httpBasic(basic -> basic.authenticationEntryPoint(new Http403ForbiddenEntryPoint()))
.userDetailsService(
userDetailsService(properties.getBasic().getUsername(), properties.getBasic().getPassword())
);
}

//disable anonymous
securityChain.anonymous(anon -> anon.disable());
} else {
Expand All @@ -116,19 +156,88 @@ public SecurityFilterChain apiSecurityFilterChain(HttpSecurity http) throws Exce
return securityChain.build();
}

public UserDetailsService userDetailsService() {
/**
* Internal basic auth
*/
public UserDetailsService userDetailsService(String username, String password) {
//create admin user with full permissions
UserDetails admin = User
.withDefaultPasswordEncoder()
.username(properties.getBasic().getUsername())
.password(properties.getBasic().getPassword())
.username(username)
.password(password)
.roles("ADMIN", "USER")
.build();

return new InMemoryUserDetailsManager(admin);
}

private JwtDecoder jwtDecoder() {
/**
* Internal auth via JWT
*/

private JwtDecoder coreJwtDecoder() throws JOSEException {
JWK jwk = keyStoreConfig.getJWKSetKeyStore().getJwk();

//we support only RSA keys
if (!(jwk instanceof RSAKey)) {
throw new IllegalArgumentException("the provided key is not suitable for token authentication");
}

NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withPublicKey(jwk.toRSAKey().toRSAPublicKey()).build();

OAuth2TokenValidator<Jwt> withIssuer = JwtValidators.createDefaultWithIssuer(
applicationProperties.getEndpoint()
);

OAuth2TokenValidator<Jwt> audienceValidator = new JwtClaimValidator<List<String>>(
JwtClaimNames.AUD,
(aud -> aud != null && aud.contains(applicationProperties.getName()))
);

//access tokens *do not contain* at_hash, those are refresh
OAuth2TokenValidator<Jwt> accessTokenValidator = new JwtClaimValidator<String>(
IdTokenClaimNames.AT_HASH,
(Objects::isNull)
);

OAuth2TokenValidator<Jwt> validator = new DelegatingOAuth2TokenValidator<>(
withIssuer,
audienceValidator,
accessTokenValidator
);
jwtDecoder.setJwtValidator(validator);

return jwtDecoder;
}

private JwtAuthenticationConverter coreJwtAuthenticationConverter() {
String claim = "authorities";
JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
converter.setJwtGrantedAuthoritiesConverter((Jwt source) -> {
if (source == null) return null;

List<GrantedAuthority> authorities = new ArrayList<>();
authorities.add(new SimpleGrantedAuthority("ROLE_USER"));

if (StringUtils.hasText(claim) && source.hasClaim(claim)) {
List<String> roles = source.getClaimAsStringList(claim);
if (roles != null) {
roles.forEach(r -> {
//use as is
authorities.add(new SimpleGrantedAuthority(r));
});
}
}

return authorities;
});
return converter;
}

/**
* External auth via JWT
*/
private JwtDecoder externalJwtDecoder() {
SecurityProperties.JwtAuthenticationProperties jwtProps = properties.getJwt();
NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withIssuerLocation(jwtProps.getIssuerUri()).build();

Expand All @@ -144,7 +253,7 @@ private JwtDecoder jwtDecoder() {
return jwtDecoder;
}

private JwtAuthenticationConverter jwtAuthenticationConverter() {
private JwtAuthenticationConverter externalJwtAuthenticationConverter() {
SecurityProperties.JwtAuthenticationProperties jwtProps = properties.getJwt();
String claim = jwtProps.getClaim();
JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
Expand Down Expand Up @@ -174,6 +283,55 @@ private JwtAuthenticationConverter jwtAuthenticationConverter() {
return converter;
}

@Bean("authSecurityFilterChain")
public SecurityFilterChain authSecurityFilterChain(HttpSecurity http) throws Exception {
HttpSecurity securityChain = http
.securityMatcher(getAuthRequestMatcher())
.authorizeHttpRequests(auth -> {
auth.requestMatchers(getAuthRequestMatcher()).hasRole("USER").anyRequest().authenticated();
})
// disable request cache
.requestCache(requestCache -> requestCache.disable())
//disable csrf
.csrf(csrf -> csrf.disable())
// we don't want a session for these endpoints, each request should be evaluated
.sessionManagement(management -> management.sessionCreationPolicy(SessionCreationPolicy.STATELESS));

// allow cors
securityChain.cors(cors -> {
if (StringUtils.hasText(corsOrigins)) {
cors.configurationSource(corsConfigurationSource(corsOrigins));
} else {
cors.disable();
}
});

//authentication (when configured)
if (StringUtils.hasText(clientId) && StringUtils.hasText(clientSecret)) {
//enable basic
securityChain
.httpBasic(basic -> basic.authenticationEntryPoint(new Http403ForbiddenEntryPoint()))
.userDetailsService(userDetailsService(clientId, clientSecret));

//disable anonymous
securityChain.anonymous(anon -> anon.disable());
} else {
//assign both USER and ADMIN to anon user to bypass all scoped permission checks
securityChain.anonymous(anon -> {
anon.authorities("ROLE_USER", "ROLE_ADMIN");
anon.principal("anonymous");
});
}

securityChain.exceptionHandling(handling -> {
handling
.authenticationEntryPoint(new Http403ForbiddenEntryPoint())
.accessDeniedHandler(new AccessDeniedHandlerImpl()); // use 403
});

return securityChain.build();
}

@Bean("h2SecurityFilterChain")
public SecurityFilterChain h2SecurityFilterChain(HttpSecurity http) throws Exception {
return http
Expand Down Expand Up @@ -222,6 +380,10 @@ public RequestMatcher getApiRequestMatcher() {
return new AntPathRequestMatcher(API_PREFIX + "/**");
}

public RequestMatcher getAuthRequestMatcher() {
return new AntPathRequestMatcher("/auth/**");
}

private CorsConfigurationSource corsConfigurationSource(String origins) {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOriginPatterns(new ArrayList<>(StringUtils.commaDelimitedListToSet(origins)));
Expand Down
16 changes: 15 additions & 1 deletion application/src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,8 @@ kubernetes:
results: ${K8S_ENABLE_RESULTS:default}
security:
disable-root: ${K8S_SEC_DISABLE_ROOT:false}
envs:
prefix: ${K8S_ENVS_PREFIX:${application.name}}
image-pull-policy: ${K8S_IMAGE_PULL_POLICY:IfNotPresent}
init-image: ${K8S_INIT_IMAGE:ghcr.io/scc-digitalhub/digitalhub-core-builder-tool:latest}
resources:
Expand Down Expand Up @@ -193,4 +195,16 @@ files:
secret-key: ${AWS_SECRET_KEY:}
endpoint: ${S3_ENDPOINT:}
bucket: ${S3_BUCKET:}


# JWT configuration
jwt:
keystore:
path: ${JWT_KEYSTORE_PATH:classpath:/keystore.jwks}
kid: ${JWT_KEYSTORE_KID:}
access-token:
duration: ${JWT_ACCESS_TOKEN_DURATION:}
refresh-token:
duration: ${JWT_REFRESH_TOKEN_DURATION:}
client-id: ${JWT_CLIENT_ID:${security.basic.username}}
client-secret: ${JWT_CLIENT_SECRET:${security.basic.password}}
cache-control: ${JWKS_CACHE_CONTROL:public, max-age=900, must-revalidate, no-transform}
Loading

0 comments on commit 5365353

Please sign in to comment.