Skip to content

Commit 8fcbc59

Browse files
authored
Added Okta OAuth Authentication (#14)
* Added Okta authentication integration. * Added Date picker for message browser
1 parent ad3e477 commit 8fcbc59

32 files changed

+1125
-153
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
* View Consumer Groups — view per-partition parked offsets, combined and per-partition lag
2828
* Browse Messages — browse messages with JSON, plain text, Protobuf, and Avro encoding
2929
* Topic Configuration — create and configure new topics and edit existing one
30+
* Support for Okta OAuth Authentication
3031

3132
---
3233

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,12 @@ public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
2020
final String basePath = exchange.getRequest().getPath().contextPath().value();
2121
final String path = exchange.getRequest().getPath().pathWithinApplication().value();
2222

23-
if (!path.startsWith("/api") && !path.startsWith("/assets")) {
23+
if (
24+
!path.startsWith("/api") &&
25+
!path.startsWith("/assets") &&
26+
!path.startsWith("/logout") &&
27+
!path.startsWith("/authentication")
28+
) {
2429
return chain.filter(
2530
exchange
2631
.mutate()
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.auth;
8+
9+
import static jakarta.validation.constraints.Pattern.Flag.CASE_INSENSITIVE;
10+
11+
import com.ideasbucket.tansen.entity.OAuth2;
12+
import jakarta.validation.Valid;
13+
import jakarta.validation.constraints.Pattern;
14+
import org.springframework.boot.context.properties.ConfigurationProperties;
15+
import org.springframework.validation.annotation.Validated;
16+
17+
@ConfigurationProperties(prefix = "auth")
18+
@Validated
19+
public class AuthenticationProperties {
20+
21+
@Pattern(
22+
regexp = "^(?:disabled|oauth2|DISABLED|OAUTH2)$",
23+
message = "Only OAUTH2 and DISABLED is supported.",
24+
flags = CASE_INSENSITIVE
25+
)
26+
private final String type;
27+
28+
@Valid
29+
private final OAuth2 oauth2;
30+
31+
public AuthenticationProperties(String type, OAuth2 oauth2) {
32+
this.type = type == null ? "disabled" : 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: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
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.auth;
8+
9+
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
10+
import org.springframework.context.annotation.Bean;
11+
import org.springframework.context.annotation.Configuration;
12+
import org.springframework.http.HttpMethod;
13+
import org.springframework.security.config.Customizer;
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+
20+
@Configuration
21+
@EnableWebFluxSecurity
22+
@EnableReactiveMethodSecurity
23+
@ConditionalOnProperty(value = "auth.type", havingValue = "disabled")
24+
public class DisabledSecurityConfiguration {
25+
26+
@Bean
27+
public SecurityWebFilterChain configure(ServerHttpSecurity http) {
28+
return http
29+
.logout((ServerHttpSecurity.LogoutSpec::disable))
30+
.httpBasic((ServerHttpSecurity.HttpBasicSpec::disable))
31+
.csrf(ServerHttpSecurity.CsrfSpec::disable)
32+
.cors(Customizer.withDefaults())
33+
.securityContextRepository(NoOpServerSecurityContextRepository.getInstance())
34+
.authorizeExchange(authorizeExchangeSpec -> {
35+
authorizeExchangeSpec.pathMatchers(HttpMethod.OPTIONS).permitAll();
36+
authorizeExchangeSpec.pathMatchers(HttpMethod.GET, "/**").permitAll();
37+
authorizeExchangeSpec.pathMatchers(HttpMethod.POST, "/**").permitAll();
38+
authorizeExchangeSpec.pathMatchers(HttpMethod.PUT, "/**").permitAll();
39+
authorizeExchangeSpec.pathMatchers(HttpMethod.PATCH, "/**").permitAll();
40+
authorizeExchangeSpec.pathMatchers(HttpMethod.DELETE, "/**").permitAll();
41+
authorizeExchangeSpec.anyExchange().permitAll();
42+
})
43+
.build();
44+
}
45+
}
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
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.auth;
8+
9+
import static com.ideasbucket.tansen.util.JsonConverter.createObjectNode;
10+
import static com.ideasbucket.tansen.util.RequestResponseUtil.*;
11+
12+
import java.net.URI;
13+
import java.util.Map;
14+
import org.springframework.beans.factory.annotation.Autowired;
15+
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
16+
import org.springframework.context.annotation.Bean;
17+
import org.springframework.context.annotation.Configuration;
18+
import org.springframework.http.HttpMethod;
19+
import org.springframework.http.HttpStatus;
20+
import org.springframework.http.server.reactive.ServerHttpResponse;
21+
import org.springframework.security.config.Customizer;
22+
import org.springframework.security.config.annotation.method.configuration.EnableReactiveMethodSecurity;
23+
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
24+
import org.springframework.security.config.oauth2.client.CommonOAuth2Provider;
25+
import org.springframework.security.config.web.server.ServerHttpSecurity;
26+
import org.springframework.security.oauth2.client.oidc.web.server.logout.OidcClientInitiatedServerLogoutSuccessHandler;
27+
import org.springframework.security.oauth2.client.registration.ClientRegistration;
28+
import org.springframework.security.oauth2.client.registration.InMemoryReactiveClientRegistrationRepository;
29+
import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository;
30+
import org.springframework.security.oauth2.core.oidc.IdTokenClaimNames;
31+
import org.springframework.security.web.server.SecurityWebFilterChain;
32+
import org.springframework.security.web.server.authentication.logout.ServerLogoutSuccessHandler;
33+
import org.springframework.security.web.server.csrf.CookieServerCsrfTokenRepository;
34+
import org.springframework.security.web.server.csrf.CsrfToken;
35+
import org.springframework.security.web.server.csrf.ServerCsrfTokenRequestHandler;
36+
import org.springframework.security.web.server.csrf.XorServerCsrfTokenRequestAttributeHandler;
37+
import org.springframework.web.server.WebFilter;
38+
import reactor.core.publisher.Mono;
39+
40+
@Configuration
41+
@EnableWebFluxSecurity
42+
@EnableReactiveMethodSecurity
43+
@ConditionalOnProperty(value = "auth.type", havingValue = "oauth2")
44+
public class OAuth2SecurityConfiguration {
45+
46+
private final AuthenticationProperties authenticationProperties;
47+
private final ReactiveClientRegistrationRepository clientRegistrationRepository;
48+
49+
@Autowired
50+
public OAuth2SecurityConfiguration(AuthenticationProperties authenticationProperties) {
51+
this.authenticationProperties = authenticationProperties;
52+
this.clientRegistrationRepository = new InMemoryReactiveClientRegistrationRepository(oktaClientRegistration());
53+
}
54+
55+
@Bean
56+
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
57+
CookieServerCsrfTokenRepository csrfTokenRepository = CookieServerCsrfTokenRepository.withHttpOnlyFalse();
58+
XorServerCsrfTokenRequestAttributeHandler delegate = new XorServerCsrfTokenRequestAttributeHandler();
59+
// Use only the handle() method of XorServerCsrfTokenRequestAttributeHandler and the
60+
// default implementation of resolveCsrfTokenValue() from ServerCsrfTokenRequestHandler
61+
ServerCsrfTokenRequestHandler csrfTokenRequestHandler = delegate::handle;
62+
63+
return http
64+
.authorizeExchange(authorizeExchangeSpec -> {
65+
authorizeExchangeSpec.pathMatchers(HttpMethod.OPTIONS).permitAll();
66+
authorizeExchangeSpec.pathMatchers(HttpMethod.GET, "/insight/health/readiness").permitAll();
67+
authorizeExchangeSpec.pathMatchers(HttpMethod.GET, "/insight/health/liveness").permitAll();
68+
authorizeExchangeSpec.pathMatchers(HttpMethod.GET, "/authentication").permitAll();
69+
authorizeExchangeSpec.pathMatchers(HttpMethod.GET, "/login**").permitAll();
70+
authorizeExchangeSpec.pathMatchers(HttpMethod.GET, "/oauth2/**").permitAll();
71+
authorizeExchangeSpec.pathMatchers(HttpMethod.GET, "/favicon.ico").permitAll();
72+
authorizeExchangeSpec.pathMatchers(HttpMethod.GET, "/assets/**").permitAll();
73+
authorizeExchangeSpec.anyExchange().authenticated();
74+
})
75+
.formLogin(formLoginSpec -> formLoginSpec.loginPage("/login"))
76+
.csrf(csrfSpec -> {
77+
csrfSpec.csrfTokenRepository(csrfTokenRepository);
78+
csrfSpec.csrfTokenRequestHandler(csrfTokenRequestHandler);
79+
})
80+
.logout(logoutSpec -> {
81+
logoutSpec.logoutUrl("/logout");
82+
logoutSpec.logoutSuccessHandler(oidcLogoutSuccessHandler(this.clientRegistrationRepository));
83+
})
84+
.oauth2Login(Customizer.withDefaults())
85+
.exceptionHandling(exceptionHandlingSpec -> {
86+
exceptionHandlingSpec.authenticationEntryPoint((serverWebExchange, ex) -> {
87+
if (isAjaxRequest(serverWebExchange.getRequest().getHeaders())) {
88+
var rootNode = createObjectNode();
89+
rootNode.put("success", false);
90+
var errorsNode = createObjectNode();
91+
errorsNode.put("loginUrl", getLoginPath(authenticationProperties));
92+
errorsNode.put("message", "Login required.");
93+
rootNode.replace("errors", errorsNode);
94+
95+
return setJsonResponse(rootNode, serverWebExchange, HttpStatus.UNAUTHORIZED);
96+
}
97+
98+
return Mono.fromRunnable(() -> {
99+
ServerHttpResponse response = serverWebExchange.getResponse();
100+
response.setStatusCode(HttpStatus.TEMPORARY_REDIRECT);
101+
response.getHeaders().set("Referer", serverWebExchange.getRequest().getPath().value());
102+
response.getHeaders().setLocation(URI.create(getLoginPath(authenticationProperties)));
103+
});
104+
});
105+
106+
exceptionHandlingSpec.accessDeniedHandler((serverWebExchange, ex) -> {
107+
var rootNode = createObjectNode();
108+
rootNode.put("success", false);
109+
var errorsNode = createObjectNode();
110+
errorsNode.put("access", "You do not have access to this action.");
111+
rootNode.replace("errors", errorsNode);
112+
113+
return setJsonResponse(rootNode, serverWebExchange, HttpStatus.FORBIDDEN);
114+
});
115+
})
116+
.build();
117+
}
118+
119+
@Bean
120+
public ReactiveClientRegistrationRepository clientRegistrationRepository() {
121+
return this.clientRegistrationRepository;
122+
}
123+
124+
@Bean
125+
WebFilter csrfCookieWebFilter() {
126+
return (exchange, chain) -> {
127+
Mono<CsrfToken> csrfToken = exchange.getAttributeOrDefault(CsrfToken.class.getName(), Mono.empty());
128+
return csrfToken
129+
.doOnSuccess(token -> {
130+
/* Ensures the token is subscribed to. */
131+
})
132+
.then(chain.filter(exchange));
133+
};
134+
}
135+
136+
private ServerLogoutSuccessHandler oidcLogoutSuccessHandler(final ReactiveClientRegistrationRepository repository) {
137+
var successHandler = new OidcClientInitiatedServerLogoutSuccessHandler(repository);
138+
successHandler.setPostLogoutRedirectUri("{baseUrl}");
139+
140+
return successHandler;
141+
}
142+
143+
private ClientRegistration oktaClientRegistration() {
144+
return CommonOAuth2Provider.OKTA
145+
.getBuilder("okta")
146+
.clientId(authenticationProperties.getOauth2().getClient("okta").getClientId())
147+
.clientSecret(authenticationProperties.getOauth2().getClient("okta").getClientSecret())
148+
.authorizationUri(authenticationProperties.getOauth2().getClient("okta").getAuthorizationUri())
149+
.redirectUri("{baseUrl}/login/oauth2/code/{registrationId}")
150+
.scope(authenticationProperties.getOauth2().getClient("okta").getScope())
151+
.tokenUri(authenticationProperties.getOauth2().getClient("okta").getTokenUri())
152+
.userInfoUri(authenticationProperties.getOauth2().getClient("okta").getUserInfoUri())
153+
.jwkSetUri(authenticationProperties.getOauth2().getClient("okta").getJwkSetUri())
154+
.userNameAttributeName(IdTokenClaimNames.SUB)
155+
.clientName("Okta")
156+
.providerConfigurationMetadata(
157+
Map.of(
158+
"end_session_endpoint",
159+
authenticationProperties.getOauth2().getClient("okta").getSessionEndPointUri()
160+
)
161+
)
162+
.build();
163+
}
164+
}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
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.entity;
8+
9+
import com.fasterxml.jackson.annotation.*;
10+
import java.util.List;
11+
12+
@JsonInclude(JsonInclude.Include.NON_NULL)
13+
@JsonPropertyOrder({ "loginRequired", "loggedIn", "firstName", "lastName", "initials" })
14+
public class AuthStatus {
15+
16+
@JsonProperty("loginRequired")
17+
private final Boolean loginRequired;
18+
19+
@JsonProperty("loggedIn")
20+
private final Boolean loggedIn;
21+
22+
@JsonProperty("firstName")
23+
private final String firstName;
24+
25+
@JsonProperty("lastName")
26+
private final String lastName;
27+
28+
@JsonProperty("initials")
29+
private final String initials;
30+
31+
@JsonProperty("loginOptions")
32+
private final List<LoginOptions> loginOptions;
33+
34+
@JsonCreator
35+
public AuthStatus(
36+
@JsonProperty("loginRequired") Boolean loginRequired,
37+
@JsonProperty("loggedIn") Boolean loggedIn,
38+
@JsonProperty("firstName") String firstName,
39+
@JsonProperty("lastName") String lastName,
40+
@JsonProperty("loginOptions") List<LoginOptions> loginOptions
41+
) {
42+
this.loginRequired = loginRequired;
43+
this.loggedIn = loggedIn;
44+
this.firstName = firstName;
45+
this.lastName = lastName;
46+
this.initials = getNameInitials(firstName, lastName);
47+
this.loginOptions = loginOptions;
48+
}
49+
50+
public Boolean getLoginRequired() {
51+
return loginRequired;
52+
}
53+
54+
public Boolean getLoggedIn() {
55+
return loggedIn;
56+
}
57+
58+
public String getFirstName() {
59+
return firstName;
60+
}
61+
62+
public String getLastName() {
63+
return lastName;
64+
}
65+
66+
public String getInitials() {
67+
return initials;
68+
}
69+
70+
public List<LoginOptions> getLoginOptions() {
71+
return loginOptions;
72+
}
73+
74+
@JsonIgnore
75+
public static AuthStatus noLoginRequired() {
76+
return new AuthStatus(false, true, null, null, null);
77+
}
78+
79+
@JsonIgnore
80+
public static AuthStatus notLoggedIn(List<LoginOptions> loginOptions) {
81+
return new AuthStatus(true, false, null, null, loginOptions);
82+
}
83+
84+
@JsonIgnore
85+
public static AuthStatus loggedIn(String firstName, String lastName) {
86+
return new AuthStatus(true, true, firstName, lastName, null);
87+
}
88+
89+
@JsonIgnore
90+
private String getNameInitials(String firstName, String lastName) {
91+
if ((firstName == null) && (lastName == null)) {
92+
return null;
93+
}
94+
95+
if (lastName == null) {
96+
return firstName.substring(0, 1).toUpperCase();
97+
}
98+
99+
if (firstName == null) {
100+
return lastName.substring(0, 1).toUpperCase();
101+
}
102+
103+
return firstName.substring(0, 1).toUpperCase() + lastName.substring(0, 1).toUpperCase();
104+
}
105+
}

0 commit comments

Comments
 (0)