Skip to content

Commit 57b3adb

Browse files
authored
Added OAuth and few other bug fixes (#11)
* Added OAuth WIP. * Formatting * Gradle cleanup
1 parent ebe1924 commit 57b3adb

File tree

7 files changed

+342
-9
lines changed

7 files changed

+342
-9
lines changed
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*
2+
* This file is part of the Tansen project.
3+
*
4+
* For the full copyright and license information, please view the LICENSE
5+
* file that was distributed with this source code.
6+
*/
7+
package com.ideasbucket.tansen.configuration;
8+
9+
import static jakarta.validation.constraints.Pattern.Flag.CASE_INSENSITIVE;
10+
11+
import com.fasterxml.jackson.annotation.JsonCreator;
12+
import com.fasterxml.jackson.annotation.JsonProperty;
13+
import jakarta.validation.Valid;
14+
import jakarta.validation.constraints.Pattern;
15+
import org.springframework.boot.context.properties.ConfigurationProperties;
16+
import org.springframework.validation.annotation.Validated;
17+
18+
@ConfigurationProperties(prefix = "auth")
19+
@Validated
20+
public class AuthenticationProperties {
21+
22+
@JsonProperty("type")
23+
@Pattern(regexp = "^(?:OAUTH2|oauth2)$", message = "Only OAuth2 is supported currently.", flags = CASE_INSENSITIVE)
24+
private final String type;
25+
26+
@JsonProperty("oauth2")
27+
@Valid
28+
private final OAuth2 oauth2;
29+
30+
@JsonCreator
31+
public AuthenticationProperties(@JsonProperty("type") String type, @JsonProperty("oauth2") OAuth2 oauth2) {
32+
this.type = type == null ? null : type.toLowerCase();
33+
this.oauth2 = oauth2;
34+
}
35+
36+
public String getType() {
37+
return type;
38+
}
39+
40+
public OAuth2 getOauth2() {
41+
return oauth2;
42+
}
43+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/*
2+
* This file is part of the Tansen project.
3+
*
4+
* For the full copyright and license information, please view the LICENSE
5+
* file that was distributed with this source code.
6+
*/
7+
package com.ideasbucket.tansen.configuration;
8+
9+
import com.fasterxml.jackson.annotation.*;
10+
import jakarta.validation.Valid;
11+
import jakarta.validation.constraints.AssertTrue;
12+
import java.util.Map;
13+
import java.util.stream.Collectors;
14+
import javax.validation.constraints.NotNull;
15+
16+
@JsonInclude(JsonInclude.Include.NON_NULL)
17+
@JsonPropertyOrder({ "client" })
18+
public class OAuth2 {
19+
20+
@JsonProperty("client")
21+
@Valid
22+
private final Map<String, @Valid @NotNull(
23+
message = "OAuth2 properties cannot be null."
24+
) OAuthClientProperties> client;
25+
26+
@JsonCreator
27+
public OAuth2(@JsonProperty("client") Map<String, OAuthClientProperties> client) {
28+
this.client =
29+
client
30+
.entrySet()
31+
.stream()
32+
.collect(
33+
Collectors.toMap(
34+
it -> it.getKey().toLowerCase(),
35+
it -> {
36+
var result = OAuthClientProperties.getInstanceWithProvider(
37+
it.getValue(),
38+
it.getKey().toLowerCase()
39+
);
40+
OAuthClientProperties.validate(result);
41+
return result;
42+
}
43+
)
44+
);
45+
}
46+
47+
public Map<String, OAuthClientProperties> getClient() {
48+
return client;
49+
}
50+
51+
@AssertTrue(message = "Invalid OAuth provider currently we only support Okta.")
52+
@JsonIgnore
53+
private boolean hasValidSupportedOAuthProvider() {
54+
if (client.isEmpty()) {
55+
return true;
56+
}
57+
58+
return client.containsKey("okta");
59+
}
60+
}
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
/*
2+
* This file is part of the Tansen project.
3+
*
4+
* For the full copyright and license information, please view the LICENSE
5+
* file that was distributed with this source code.
6+
*/
7+
package com.ideasbucket.tansen.configuration;
8+
9+
import com.fasterxml.jackson.annotation.*;
10+
import jakarta.validation.constraints.NotBlank;
11+
import jakarta.validation.constraints.NotEmpty;
12+
import jakarta.validation.constraints.NotNull;
13+
import java.util.HashSet;
14+
import java.util.List;
15+
import java.util.Map;
16+
import org.springframework.beans.factory.BeanInitializationException;
17+
18+
@JsonInclude(JsonInclude.Include.NON_NULL)
19+
@JsonPropertyOrder(
20+
{
21+
"provider",
22+
"clientId",
23+
"clientSecret",
24+
"authorizationUri",
25+
"tokenUri",
26+
"userInfoUri",
27+
"jwkSetUri",
28+
"scope",
29+
"customParams",
30+
}
31+
)
32+
public class OAuthClientProperties {
33+
34+
private final String provider;
35+
36+
@JsonProperty("clientId")
37+
@NotNull(message = "Client ID cannot be null.")
38+
@NotBlank(message = "Client ID cannot be blank.")
39+
private final String clientId;
40+
41+
@JsonProperty("clientSecret")
42+
@NotNull(message = "Client Secret cannot be null.")
43+
@NotBlank(message = "Client Secret cannot be blank.")
44+
private final String clientSecret;
45+
46+
@JsonProperty("authorizationUri")
47+
private final String authorizationUri;
48+
49+
@JsonProperty("tokenUri")
50+
private final String tokenUri;
51+
52+
@JsonProperty("userInfoUri")
53+
private final String userInfoUri;
54+
55+
@JsonProperty("jwkSetUri")
56+
private final String jwkSetUri;
57+
58+
@JsonProperty("scope")
59+
@NotNull(message = "Scope cannot be null.")
60+
@NotEmpty(message = "Scope cannot be empty.")
61+
private final List<@NotNull(message = "Scope cannot be null.") @NotBlank(
62+
message = "Scope cannot be blank."
63+
) String> scope;
64+
65+
@JsonProperty("customParams")
66+
private final Map<String, String> customParameters;
67+
68+
@JsonCreator
69+
public OAuthClientProperties(
70+
@JsonProperty("provider") String provider,
71+
@JsonProperty("clientId") String clientId,
72+
@JsonProperty("clientSecret") String clientSecret,
73+
@JsonProperty("authorizationUri") String authorizationUri,
74+
@JsonProperty("tokenUri") String tokenUri,
75+
@JsonProperty("userInfoUri") String userInfoUri,
76+
@JsonProperty("jwkSetUri") String jwkSetUri,
77+
@JsonProperty("scope") List<String> scope,
78+
@JsonProperty("customParams") Map<String, String> customParameters
79+
) {
80+
this.provider = provider;
81+
this.clientId = clientId;
82+
this.clientSecret = clientSecret;
83+
this.authorizationUri = authorizationUri;
84+
this.tokenUri = tokenUri;
85+
this.userInfoUri = userInfoUri;
86+
this.jwkSetUri = jwkSetUri;
87+
this.scope = scope;
88+
this.customParameters = customParameters == null ? Map.of() : Map.copyOf(customParameters);
89+
}
90+
91+
public static OAuthClientProperties getInstanceWithProvider(OAuthClientProperties original, String provider) {
92+
return new OAuthClientProperties(
93+
provider,
94+
original.clientId,
95+
original.clientSecret,
96+
original.authorizationUri,
97+
original.tokenUri,
98+
original.userInfoUri,
99+
original.jwkSetUri,
100+
original.scope,
101+
original.customParameters
102+
);
103+
}
104+
105+
public static void validate(OAuthClientProperties target) throws BeanInitializationException {
106+
if (target.getProvider() == null) {
107+
return;
108+
}
109+
110+
if (target.getProvider().equalsIgnoreCase("okta")) {
111+
if (target.getJwkSetUri() == null || target.getJwkSetUri().isBlank()) {
112+
throw new BeanInitializationException(
113+
"Okta requires JWK set URI. Properties path 'auth.oauth2.client.okta.jwkSetUri'"
114+
);
115+
}
116+
117+
if (target.getAuthorizationUri() == null || target.getAuthorizationUri().isBlank()) {
118+
throw new BeanInitializationException(
119+
"Okta requires Authorization URI. Properties path 'auth.oauth2.client.okta.authorizationUri'"
120+
);
121+
}
122+
123+
if (target.getTokenUri() == null || target.getTokenUri().isBlank()) {
124+
throw new BeanInitializationException(
125+
"Okta requires token URI. Properties path 'auth.oauth2.client.okta.tokenUri'"
126+
);
127+
}
128+
129+
if (!new HashSet<>(target.scope).containsAll(List.of("openid", "profile", "email"))) {
130+
throw new BeanInitializationException(
131+
"Okta requires following scopes openid, profile and email at minimum. Properties path 'auth.oauth2.client.okta.scope'"
132+
);
133+
}
134+
}
135+
}
136+
137+
public String getProvider() {
138+
return provider;
139+
}
140+
141+
public String getClientId() {
142+
return clientId;
143+
}
144+
145+
public String getClientSecret() {
146+
return clientSecret;
147+
}
148+
149+
public String getAuthorizationUri() {
150+
return authorizationUri;
151+
}
152+
153+
public String getTokenUri() {
154+
return tokenUri;
155+
}
156+
157+
public String getUserInfoUri() {
158+
return userInfoUri;
159+
}
160+
161+
public String getJwkSetUri() {
162+
return jwkSetUri;
163+
}
164+
165+
public List<String> getScope() {
166+
return scope;
167+
}
168+
}

backend/main/java/com/ideasbucket/tansen/configuration/RbacProperties.java

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,26 +9,20 @@
99
import com.fasterxml.jackson.annotation.JsonCreator;
1010
import com.fasterxml.jackson.annotation.JsonProperty;
1111
import jakarta.validation.Valid;
12-
import jakarta.validation.constraints.NotNull;
1312
import java.util.List;
1413
import org.springframework.boot.context.properties.ConfigurationProperties;
15-
import org.springframework.boot.context.properties.EnableConfigurationProperties;
16-
import org.springframework.context.annotation.PropertySource;
1714
import org.springframework.validation.annotation.Validated;
1815

19-
@PropertySource(value = "classpath:roles.yml", factory = YamlPropertySourceFactory.class)
2016
@ConfigurationProperties(prefix = "rbac")
21-
@EnableConfigurationProperties(RbacProperties.class)
2217
@Validated
2318
public class RbacProperties {
2419

2520
@JsonProperty("roles")
26-
@NotNull(message = "Roles cannot be null.")
2721
private final List<@Valid Role> roles;
2822

2923
@JsonCreator
3024
public RbacProperties(@JsonProperty("roles") List<Role> roles) {
31-
this.roles = roles;
25+
this.roles = roles == null ? List.of() : List.copyOf(roles);
3226
}
3327

3428
public List<Role> getRoles() {
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/*
2+
* This file is part of the Tansen project.
3+
*
4+
* For the full copyright and license information, please view the LICENSE
5+
* file that was distributed with this source code.
6+
*/
7+
package com.ideasbucket.tansen.configuration;
8+
9+
import org.springframework.beans.factory.annotation.Autowired;
10+
import org.springframework.context.annotation.Bean;
11+
import org.springframework.context.annotation.Configuration;
12+
import org.springframework.http.HttpMethod;
13+
import org.springframework.http.HttpStatus;
14+
import org.springframework.security.config.annotation.method.configuration.EnableReactiveMethodSecurity;
15+
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
16+
import org.springframework.security.config.web.server.ServerHttpSecurity;
17+
import org.springframework.security.web.server.SecurityWebFilterChain;
18+
import org.springframework.security.web.server.context.NoOpServerSecurityContextRepository;
19+
import reactor.core.publisher.Mono;
20+
21+
@Configuration
22+
@EnableWebFluxSecurity
23+
@EnableReactiveMethodSecurity
24+
public class SecurityConfiguration {
25+
26+
private final AuthenticationProperties authenticationProperties;
27+
28+
@Autowired
29+
public SecurityConfiguration(AuthenticationProperties authenticationProperties) {
30+
this.authenticationProperties = authenticationProperties;
31+
}
32+
33+
@Bean
34+
public SecurityWebFilterChain configure(ServerHttpSecurity http) {
35+
if (this.authenticationProperties == null || this.authenticationProperties.getType() == null) {
36+
return disabledSecurity(http);
37+
}
38+
return disabledSecurity(http);
39+
}
40+
41+
private SecurityWebFilterChain disabledSecurity(ServerHttpSecurity http) {
42+
return http
43+
.exceptionHandling(exceptionHandlingSpec -> {
44+
exceptionHandlingSpec.authenticationEntryPoint((swe, e) ->
45+
Mono.fromRunnable(() -> swe.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED))
46+
);
47+
exceptionHandlingSpec.accessDeniedHandler((swe, e) ->
48+
Mono.fromRunnable(() -> swe.getResponse().setStatusCode(HttpStatus.FORBIDDEN))
49+
);
50+
})
51+
.logout((ServerHttpSecurity.LogoutSpec::disable))
52+
.httpBasic((ServerHttpSecurity.HttpBasicSpec::disable))
53+
.csrf(ServerHttpSecurity.CsrfSpec::disable)
54+
.cors(corsSpec -> {})
55+
.securityContextRepository(NoOpServerSecurityContextRepository.getInstance())
56+
.authorizeExchange(authorizeExchangeSpec -> {
57+
authorizeExchangeSpec.pathMatchers(HttpMethod.OPTIONS).permitAll();
58+
authorizeExchangeSpec.pathMatchers(HttpMethod.GET, "/**").permitAll();
59+
authorizeExchangeSpec.pathMatchers(HttpMethod.POST, "/**").permitAll();
60+
authorizeExchangeSpec.pathMatchers(HttpMethod.PUT, "/**").permitAll();
61+
authorizeExchangeSpec.pathMatchers(HttpMethod.PATCH, "/**").permitAll();
62+
authorizeExchangeSpec.pathMatchers(HttpMethod.DELETE, "/**").permitAll();
63+
authorizeExchangeSpec.anyExchange().permitAll();
64+
})
65+
.build();
66+
}
67+
}

backend/main/resources/roles.yml

Lines changed: 0 additions & 2 deletions
This file was deleted.

build.gradle.kts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ dependencies {
4141
implementation("org.springframework.boot:spring-boot-starter-thymeleaf")
4242
implementation("org.springframework.boot:spring-boot-starter-validation")
4343
implementation("org.springframework.boot:spring-boot-starter-webflux")
44+
implementation("org.springframework.boot:spring-boot-starter-oauth2-client")
45+
implementation("org.springframework.boot:spring-boot-starter-security")
4446
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
4547
implementation("io.projectreactor.kotlin:reactor-kotlin-extensions")
4648
implementation("org.jetbrains.kotlin:kotlin-reflect")
@@ -55,6 +57,7 @@ dependencies {
5557
implementation("org.springdoc:springdoc-openapi-starter-webflux-ui:2.1.0")
5658
annotationProcessor("org.springframework.boot:spring-boot-configuration-processor")
5759
testImplementation("org.springframework.boot:spring-boot-starter-test")
60+
testImplementation("org.springframework.security:spring-security-test")
5861
testImplementation("io.projectreactor:reactor-test")
5962
testImplementation("org.springframework.kafka:spring-kafka-test")
6063
testImplementation("org.testcontainers:junit-jupiter")

0 commit comments

Comments
 (0)