Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ dependencyManagement {

dependencies {
api("com.fasterxml.jackson.core:jackson-databind")

api("org.springframework.boot:spring-boot-starter-oauth2-client")
api("org.springframework.boot:spring-boot-starter-oauth2-resource-server")
api("org.springframework.boot:spring-boot-starter-security")
api("org.springframework.boot:spring-boot-starter-web")
Expand Down Expand Up @@ -106,7 +108,8 @@ tasks.jacocoTestReport {
files(classDirectories.files.map {
fileTree(it) {
exclude(
"**/*AutoConfiguration.class", // Spring auto-config classes
"**/*Configuration.class", // Spring configuration classes
"app/quickcase/sdk/spring/auth/QuickcaseOAuth2ResourceServerCustomizer.class" // Spring customizer
)
}
})
Expand All @@ -125,7 +128,7 @@ tasks.jacocoTestCoverageVerification {
files(classDirectories.files.map {
fileTree(it) {
exclude(
"**/*AutoConfiguration.class", // Spring auto-config classes
"**/*Configuration.class", // Spring auto-config classes
"app/quickcase/sdk/spring/auth/QuickcaseOAuth2ResourceServerCustomizer.class" // Spring customizer
)
}
Expand Down
42 changes: 42 additions & 0 deletions src/main/java/app/quickcase/sdk/spring/auth/OidcConfig.java
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
package app.quickcase.sdk.spring.auth;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import lombok.Builder;
import lombok.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.bind.ConstructorBinding;
import org.springframework.boot.context.properties.bind.DefaultValue;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;

import static app.quickcase.sdk.spring.auth.OidcConfigDefault.*;
import static app.quickcase.sdk.spring.auth.OidcConfigDefault.Claims.*;
Expand All @@ -25,6 +32,8 @@ public class OidcConfig {
@Deprecated(forRemoval = true)
String authorisationStrategy;
@Nullable
Client client;
@Nullable
String issuer;
ProviderMetadata metadata;
String openidScope;
Expand All @@ -37,23 +46,56 @@ public class OidcConfig {
String mode;
Claims claims;

@Builder
@ConstructorBinding
public OidcConfig(
@DefaultValue(AUTHORISATION_STRATEGY) String authorisationStrategy,
Client client,
@NonNull String issuer,
@Nullable ProviderMetadata metadata,
@DefaultValue(OPENID_SCOPE) String openidScope,
@DefaultValue(MODE) String mode,
@DefaultValue Claims claims
) {
this.authorisationStrategy = authorisationStrategy;
this.client = client;
this.issuer = issuer;
this.metadata = metadata;
this.openidScope = openidScope;
this.mode = mode;
this.claims = claims;
}

public Map<String, Object> toOidcConfiguration() {
Assert.notNull(issuer, "OIDC issuer must be provided");
Assert.notNull(metadata, "OIDC metadata must be provided");
Assert.notNull(metadata.jwksUri(), "OIDC JWK Set URI must be provided");
Assert.notNull(metadata.tokenEndpoint(), "OIDC token endpoint must be explicitly provided");

var oidcConfiguration = new HashMap<String, Object>();

oidcConfiguration.put("issuer", issuer);
oidcConfiguration.put("jwks_uri", metadata.jwksUri());
oidcConfiguration.put("token_endpoint", metadata.tokenEndpoint());

if (metadata.userInfoEndpoint() != null)
oidcConfiguration.put("userinfo_endpoint", metadata.userInfoEndpoint());

// Required by Spring Security
oidcConfiguration.put("subject_types_supported", List.of("public"));

return oidcConfiguration;
}

@Builder
public record Client(
String id,
String secret,
Set<String> scope
) {
}

@Builder
public record ProviderMetadata(
String jwksUri,
String tokenEndpoint,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package app.quickcase.sdk.spring.auth;

import java.util.List;

import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.util.Assert;

import static org.springframework.security.oauth2.client.registration.ClientRegistrations.fromIssuerLocation;
import static org.springframework.security.oauth2.client.registration.ClientRegistrations.fromOidcConfiguration;

@Configuration
@ConditionalOnProperty(prefix = "quickcase.oidc.client", name = "id")
public class QuickcaseOAuth2ClientConfiguration {
@Bean
public ClientRegistration defaultOAuth2Client(
OidcConfig oidcConfig
) {
var issuer = oidcConfig.getIssuer();
var client = oidcConfig.getClient();

Assert.notNull(client, "OIDC client must be defined");
Assert.notNull(client.id(), "OIDC client ID must be defined");
Assert.notNull(client.secret(), "OIDC client secret must be defined");

Assert.notNull(issuer, "OIDC issuer is required for client registration");

var clientRegistrationBuilder = oidcConfig.getMetadata() != null
? fromOidcConfiguration(oidcConfig.toOidcConfiguration())
: fromIssuerLocation(issuer);

return clientRegistrationBuilder
.registrationId("default")
.clientName("S2S Client")
.clientId(client.id())
.clientSecret(client.secret())
.scope(client.scope())
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
.build();
}

@Bean
@ConditionalOnMissingBean(ClientRegistrationRepository.class)
public ClientRegistrationRepository clientRegistrationRepository(List<ClientRegistration> registrations) {
return new InMemoryClientRegistrationRepository(registrations);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
package app.quickcase.sdk.spring.auth;

import java.net.URI;
import java.net.URISyntaxException;

import app.quickcase.sdk.spring.auth.claims.ClaimNamesProvider;
import app.quickcase.sdk.spring.auth.converters.*;
import app.quickcase.sdk.spring.auth.userinfo.UserInfoAuthenticationConverter;
import app.quickcase.sdk.spring.auth.userinfo.UserInfoGateway;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.util.Assert;
import org.springframework.web.client.RestTemplate;

public class QuickcaseOAuth2ResourceServerConfiguration {
@Bean
@ConditionalOnMissingBean(ClaimNamesProvider.class)
public ClaimNamesProvider createClaimNamesProvider(OidcConfig oidcConfig) {
return new ClaimNamesProvider(oidcConfig.getClaims());
}

/**
* @deprecated UserInfo parsing deprecated; scheduled for removal in v2.0.0
*/
@Deprecated(forRemoval = true)
@Bean
@ConditionalOnMissingBean(UserInfoGateway.class)
@ConditionalOnProperty(prefix = "quickcase.oidc", name = "mode", havingValue = "user-info", matchIfMissing = true)
public UserInfoGateway createUserInfoGateway(OidcConfig oidcConfig) throws URISyntaxException {
var metadata = oidcConfig.getMetadata();
Assert.notNull(metadata, "OIDC metadata must be defined");
Assert.notNull(metadata.userInfoEndpoint(), "OIDC userinfo endpoint must be defined");

return new UserInfoGateway(new URI(metadata.userInfoEndpoint()), new RestTemplate());
}

@Bean
@ConditionalOnMissingBean(JwtClientIdConverter.class)
public JwtClientIdConverter jwtClientIdConverter() {
return new JwtClientIdConverter();
}

@Bean
@ConditionalOnMissingBean(JwtAccountConverter.class)
public JwtAccountConverter jwtAccountConverter(ClaimNamesProvider claimNames) {
return new JwtAccountConverter(claimNames.account());
}

@Bean
@ConditionalOnMissingBean(JwtScopesConverter.class)
public JwtScopesConverter jwtScopesConverter() {
return new JwtScopesConverter();
}

@Bean
@ConditionalOnMissingBean(JwtRolesConverter.class)
public JwtRolesConverter jwtRolesConverter(ClaimNamesProvider claimNames) {
return new JwtRolesConverter(claimNames.roles());
}

@Bean
@ConditionalOnMissingBean(JwtGroupsConverter.class)
public JwtGroupsConverter jwtGroupsConverter(ClaimNamesProvider claimNames) {
return new JwtGroupsConverter(claimNames.groups());
}

@Bean
@ConditionalOnMissingBean(JwtUserInfoConverter.class)
public JwtUserInfoConverter jwtUserInfoConverter(
ClaimNamesProvider claimNames,
JwtAccountConverter accountConverter,
JwtRolesConverter rolesConverter,
JwtGroupsConverter groupsConverter
) {
return new JwtUserInfoConverter(claimNames, accountConverter, rolesConverter, groupsConverter);
}

@Bean
@ConditionalOnMissingBean(JwtClientInfoConverter.class)
public JwtClientInfoConverter jwtClientInfoConverter(
ClaimNamesProvider claimNames,
JwtAccountConverter accountConverter,
JwtScopesConverter scopesConverter,
JwtRolesConverter rolesConverter,
JwtGroupsConverter groupsConverter
) {
return new JwtClientInfoConverter(claimNames, accountConverter, scopesConverter, rolesConverter, groupsConverter);
}

/**
* @deprecated UserInfo parsing deprecated; scheduled for removal in v2.0.0
*/
@Deprecated(forRemoval = true)
@Bean
@ConditionalOnMissingBean(JsonUserInfoConverter.class)
public JsonUserInfoConverter jsonUserInfoConverter(ClaimNamesProvider claimNames) {
return new JsonUserInfoConverter(claimNames);
}

/**
* @deprecated UserInfo parsing deprecated; scheduled for removal in v2.0.0
*/
@Deprecated(forRemoval = true)
@Bean
@ConditionalOnMissingBean(AbstractAuthenticationConverter.class)
@ConditionalOnProperty(prefix = "quickcase.oidc", name = "mode", havingValue = "user-info", matchIfMissing = true)
public UserInfoAuthenticationConverter createUserInfoAuthenticationConverter(
JwtClientIdConverter clientIdConverter,
JwtScopesConverter scopesConverter,
JwtClientInfoConverter clientInfoConverter,
OidcConfig oidcConfig,
UserInfoGateway userInfoGateway,
JsonUserInfoConverter userInfoConverter
) {
return new UserInfoAuthenticationConverter(
clientIdConverter,
scopesConverter,
clientInfoConverter,
oidcConfig.getOpenidScope(),
userInfoGateway,
userInfoConverter
);
}

@Bean
@ConditionalOnMissingBean(AbstractAuthenticationConverter.class)
@ConditionalOnProperty(prefix = "quickcase.oidc", name = "mode", havingValue = "jwt-access-token")
public JwtAuthenticationConverter createAccessTokenAuthenticationConverter(
JwtClientIdConverter clientIdConverter,
JwtScopesConverter scopesConverter,
JwtUserInfoConverter userInfoConverter,
JwtClientInfoConverter clientInfoConverter,
OidcConfig oidcConfig
) {
return new JwtAuthenticationConverter(
clientIdConverter,
scopesConverter,
userInfoConverter,
clientInfoConverter,
oidcConfig.getOpenidScope()
);
}

@Bean
public QuickcaseOAuth2ResourceServerCustomizer oauth2ResourceServerCustomizer(
OidcConfig oidcConfig,
AbstractAuthenticationConverter authenticationConverter
) {
return new QuickcaseOAuth2ResourceServerCustomizer(oidcConfig, authenticationConverter);
}
}
Loading