Skip to content

Commit

Permalink
Merge pull request #9326 from cbornet/reactive-session
Browse files Browse the repository at this point in the history
Support for session auth in reactive apps
  • Loading branch information
jdubois authored Mar 4, 2019
2 parents 485d0a2 + 4a7ec61 commit 5c0c2fc
Show file tree
Hide file tree
Showing 14 changed files with 124 additions and 103 deletions.
17 changes: 10 additions & 7 deletions generators/server/files.js
Original file line number Diff line number Diff line change
Expand Up @@ -422,7 +422,8 @@ const serverFiles = {
]
},
{
condition: generator => !shouldSkipUserManagement(generator) && generator.authenticationType === 'session',
condition: generator =>
!shouldSkipUserManagement(generator) && generator.authenticationType === 'session' && !generator.reactive,
path: SERVER_MAIN_SRC_DIR,
templates: [
{
Expand Down Expand Up @@ -802,10 +803,12 @@ const serverFiles = {
serverJavaApp: [
{
path: SERVER_MAIN_SRC_DIR,
templates: [
{ file: 'package/Application.java', renameTo: generator => `${generator.javaDir}${generator.mainClass}.java` },
{ file: 'package/ApplicationWebXml.java', renameTo: generator => `${generator.javaDir}ApplicationWebXml.java` }
]
templates: [{ file: 'package/Application.java', renameTo: generator => `${generator.javaDir}${generator.mainClass}.java` }]
},
{
condition: generator => !generator.reactive,
path: SERVER_MAIN_SRC_DIR,
templates: [{ file: 'package/ApplicationWebXml.java', renameTo: generator => `${generator.javaDir}ApplicationWebXml.java` }]
}
],
serverJavaConfig: [
Expand Down Expand Up @@ -1151,7 +1154,7 @@ const serverFiles = {
]
},
{
condition: generator => !generator.skipClient,
condition: generator => !generator.skipClient && !generator.reactive,
path: SERVER_MAIN_SRC_DIR,
templates: [
{
Expand Down Expand Up @@ -1230,7 +1233,7 @@ const serverFiles = {
]
},
{
condition: generator => !generator.skipClient,
condition: generator => !generator.skipClient && !generator.reactive,
path: SERVER_TEST_SRC_DIR,
templates: [
{
Expand Down
14 changes: 7 additions & 7 deletions generators/server/prompts.js
Original file line number Diff line number Diff line change
Expand Up @@ -117,18 +117,18 @@ function askForServerSideOpts(meta) {
name: 'JWT authentication (stateless, with a token)'
}
];
if (applicationType === 'monolith' && response.serviceDiscoveryType !== 'eureka') {
opts.push({
value: 'session',
name: 'HTTP Session Authentication (stateful, default Spring Security mechanism)'
});
}
if (!reactive) {
opts.push({
value: 'oauth2',
name: 'OAuth 2.0 / OIDC Authentication (stateful, works with Keycloak and Okta)'
});

if (applicationType === 'monolith' && response.serviceDiscoveryType !== 'eureka') {
opts.push({
value: 'session',
name: 'HTTP Session Authentication (stateful, default Spring Security mechanism)'
});
} else if (['gateway', 'microservice'].includes(applicationType)) {
if (['gateway', 'microservice'].includes(applicationType)) {
opts.push({
value: 'uaa',
name: 'Authentication with JHipster UAA server (the server must be generated separately)'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,20 @@
package <%= packageName %>.config;

import <%= packageName %>.security.AuthoritiesConstants;
<%_ if (authenticationType === 'jwt') { _%>
import <%= packageName %>.security.jwt.JWTFilter;
import <%= packageName %>.security.jwt.TokenProvider;

<%_ } _%>
<%_ if (authenticationType === 'session') { _%>
import io.github.jhipster.web.filter.reactive.CookieCsrfFilter;
<%_ } _%>
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.http.HttpMethod;
<%_ if (authenticationType === 'session') { _%>
import org.springframework.http.HttpStatus;
<%_ } _%>
<%_ if (!skipUserManagement) { _%>
import org.springframework.security.authentication.ReactiveAuthenticationManager;
import org.springframework.security.authentication.UserDetailsRepositoryReactiveAuthenticationManager;
Expand All @@ -40,11 +47,20 @@ import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
<%_ } _%>
import org.springframework.security.web.server.SecurityWebFilterChain;
import org.springframework.web.cors.reactive.CorsWebFilter;
<%_ if (!skipUserManagement) { _%>
import org.springframework.security.web.server.authentication.AuthenticationWebFilter;
<%_ if (authenticationType === 'session') { _%>
import org.springframework.security.web.server.WebFilterExchange;
import org.springframework.security.web.server.authentication.HttpStatusServerEntryPoint;
import org.springframework.security.web.server.authentication.logout.HttpStatusReturningServerLogoutSuccessHandler;
import org.springframework.security.web.server.csrf.CookieServerCsrfTokenRepository;
<%_ } _%>
import org.springframework.security.web.server.util.matcher.NegatedServerWebExchangeMatcher;
import org.springframework.security.web.server.util.matcher.OrServerWebExchangeMatcher;
import org.zalando.problem.spring.webflux.advice.security.SecurityProblemSupport;
<%_ if (authenticationType === 'session') { _%>
import reactor.core.publisher.Mono;
<%_ } _%>

import static org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers.pathMatchers;

@Configuration
@EnableWebFluxSecurity
Expand All @@ -56,18 +72,19 @@ public class SecurityConfiguration {
private final ReactiveUserDetailsService userDetailsService;

<%_ } _%>
<%_ if (authenticationType === 'jwt') { _%>
private final TokenProvider tokenProvider;

private final CorsWebFilter corsWebFilter;

<%_ } _%>
private final SecurityProblemSupport problemSupport;

public SecurityConfiguration(<% if (!skipUserManagement) { %>ReactiveUserDetailsService userDetailsService, <% } %>TokenProvider tokenProvider, CorsWebFilter corsWebFilter, SecurityProblemSupport problemSupport) {
public SecurityConfiguration(<% if (!skipUserManagement) { %>ReactiveUserDetailsService userDetailsService, <% } %><% if (authenticationType === 'jwt') { %>TokenProvider tokenProvider, <% } %>SecurityProblemSupport problemSupport) {
<%_ if (!skipUserManagement) { _%>
this.userDetailsService = userDetailsService;
<%_ } _%>
<%_ if (authenticationType === 'jwt') { _%>
this.tokenProvider = tokenProvider;
this.corsWebFilter = corsWebFilter;
<%_ } _%>
this.problemSupport = problemSupport;
}

Expand All @@ -88,31 +105,49 @@ public class SecurityConfiguration {
@Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
http
.securityMatcher(new NegatedServerWebExchangeMatcher(new OrServerWebExchangeMatcher(
pathMatchers("/app/**", "/i18n/**", "/content/**", "/swagger-ui/index.html", "/test/**"),
pathMatchers(HttpMethod.OPTIONS, "/**")
)))
.csrf()
.disable()
.headers()
.frameOptions()
.disable()
<%_ if (['session','oauth2'].includes(authenticationType) && applicationType !== 'microservice') { _%>
.csrfTokenRepository(CookieServerCsrfTokenRepository.withHttpOnlyFalse())
.and()
.addFilterAt(corsWebFilter, SecurityWebFiltersOrder.HTTP_HEADERS_WRITER)
// See https://github.com/spring-projects/spring-security/issues/5766
.addFilterAt(new CookieCsrfFilter(), SecurityWebFiltersOrder.REACTOR_CONTEXT)
<%_ } else{ _%>
.disable()
<%_ } _%>
<%_ if (authenticationType === 'jwt') { _%>
.addFilterAt(new JWTFilter(tokenProvider), SecurityWebFiltersOrder.HTTP_BASIC)
<%_ } _%>
<%_ if (!skipUserManagement) { _%>
.addFilterAt(new AuthenticationWebFilter(reactiveAuthenticationManager()), SecurityWebFiltersOrder.AUTHENTICATION)
.authenticationManager(reactiveAuthenticationManager())
<%_ } _%>
.exceptionHandling()
.accessDeniedHandler(problemSupport)
.authenticationEntryPoint(problemSupport)
<%_ if (authenticationType === 'session') { _%>
.and()
.formLogin()
.requiresAuthenticationMatcher(pathMatchers(HttpMethod.POST, "/api/authentication"))
.authenticationEntryPoint(new HttpStatusServerEntryPoint(HttpStatus.OK))
.authenticationSuccessHandler((exchange, authentication) -> setStatusCode(exchange, HttpStatus.OK))
.authenticationFailureHandler((exchange, exception) -> setStatusCode(exchange, HttpStatus.UNAUTHORIZED))
.and()
.logout()
.logoutUrl("/api/logout")
.logoutSuccessHandler(new HttpStatusReturningServerLogoutSuccessHandler())
<%_ } _%>
.and()
.headers()
.frameOptions()
.disable()
.and()
.authorizeExchange()
.pathMatchers(HttpMethod.OPTIONS, "/**").permitAll()
<%_ if (!skipClient) { _%>
.pathMatchers("/").permitAll()
.pathMatchers("/*.*").permitAll()
.pathMatchers("/app/**").permitAll()
.pathMatchers("/i18n/**").permitAll()
.pathMatchers("/content/**").permitAll()
.pathMatchers("/swagger-ui/index.html").permitAll()
.pathMatchers("/test/**").permitAll()
<%_ } _%>
<%_ if (!skipUserManagement) { _%>
.pathMatchers("/api/register").permitAll()
Expand All @@ -128,4 +163,10 @@ public class SecurityConfiguration {
.pathMatchers("/management/**").hasAuthority(AuthoritiesConstants.ADMIN);
return http.build();
}
<%_ if (authenticationType === 'session') { _%>

private static Mono<Void> setStatusCode(WebFilterExchange exchange, HttpStatus status) {
return Mono.fromRunnable(() -> exchange.getExchange().getResponse().setStatusCode(status));
}
<%_ } _%>
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ import io.github.jhipster.web.filter.CachingHttpHeadersFilter;
<%_ } _%>
import io.undertow.UndertowOptions;
<%_ } _%>
<%_ if (reactive) { _%>
import io.github.jhipster.web.filter.reactive.CachingHttpHeadersFilter;
<%_ } _%>
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
<%_ if (!reactive) { _%>
Expand All @@ -55,11 +58,6 @@ import org.springframework.core.env.Environment;
import org.springframework.core.env.Profiles;
import org.springframework.http.MediaType;
<%_ } _%>
<%_ if (reactive) { _%>
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher;
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers;
<%_ } _%>
import org.springframework.web.cors.CorsConfiguration;
<%_ if (!reactive) { _%>
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
Expand All @@ -72,13 +70,9 @@ import org.springframework.web.reactive.config.WebFluxConfigurer;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.ServerResponse;
import org.springframework.web.reactive.result.method.annotation.ArgumentResolverConfigurer;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebExceptionHandler;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import org.zalando.problem.spring.webflux.advice.ProblemExceptionHandler;
import org.zalando.problem.spring.webflux.advice.ProblemHandling;
import reactor.core.publisher.Mono;
<%_ } _%>
<%_ if (!reactive) { _%>

Expand Down Expand Up @@ -278,33 +272,6 @@ public class WebConfigurer implements <% if (!reactive) { %>ServletContextInitia
// Use a cache filter that only match selected paths
return new CachingHttpHeadersFilter(TimeUnit.DAYS.toMillis(jHipsterProperties.getHttp().getCache().getTimeToLiveInDays()));
}

/**
* This filter is used in production, to put HTTP cache headers with a long expiration time.
*/
public static class CachingHttpHeadersFilter implements WebFilter {

private final long cacheTimeToLive;

public CachingHttpHeadersFilter(Long cacheTimeToLive) {
this.cacheTimeToLive = cacheTimeToLive;
}

@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
return ServerWebExchangeMatchers.pathMatchers("/i18n/**", "/content/**", "/app/**")
.matches(exchange)
.filter(ServerWebExchangeMatcher.MatchResult::isMatch)
.doOnNext(matchResult -> {
ServerHttpResponse response = exchange.getResponse();
response.getHeaders().setCacheControl("max-age=" + cacheTimeToLive + ", public");
response.getHeaders().setPragma("cache");
response.getHeaders().setExpires(cacheTimeToLive + System.currentTimeMillis());

})
.then(chain.filter(exchange));
}
}
<%_ } _%>
<%_ } _%>
<%_ if (devDatabaseType === 'h2Disk' || devDatabaseType === 'h2Memory') { _%>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import <%=packageName%>.domain.Authority;
import <%=packageName%>.domain.<%= asEntity('User') %>;
<%_ if (databaseType === 'sql' || databaseType === 'mongodb' || databaseType === 'couchbase') { _%>
import <%=packageName%>.repository<% if (reactive) { %>.reactive<% } %>.AuthorityRepository;
<%_ if (authenticationType === 'session') { _%>
<%_ if (authenticationType === 'session' && !reactive) { _%>
import <%=packageName%>.repository.PersistentTokenRepository;
<%_ } _%>
<%_ } _%>
Expand Down Expand Up @@ -77,7 +77,7 @@ import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
<%_ } _%>

<%_ if ((databaseType === 'sql' || databaseType === 'mongodb' || databaseType === 'couchbase') && authenticationType === 'session') { _%>
<%_ if ((databaseType === 'sql' || databaseType === 'mongodb' || databaseType === 'couchbase') && authenticationType === 'session' && !reactive) { _%>
import java.time.LocalDate;
<%_ } _%>
import java.time.Instant;
Expand Down Expand Up @@ -116,7 +116,7 @@ public class UserService {
private final UserSearchRepository userSearchRepository;
<%_ } _%>
<%_ if (databaseType === 'sql' || databaseType === 'mongodb' || databaseType === 'couchbase') { _%>
<%_ if (authenticationType === 'session') { _%>
<%_ if (authenticationType === 'session' && !reactive) { _%>

private final PersistentTokenRepository persistentTokenRepository;
<%_ } _%>
Expand All @@ -128,7 +128,7 @@ public class UserService {
private final CacheManager cacheManager;
<%_ } _%>

public UserService(UserRepository userRepository<% if (authenticationType !== 'oauth2') { %>, PasswordEncoder passwordEncoder<% } %><% if (searchEngine === 'elasticsearch') { %>, UserSearchRepository userSearchRepository<% } %><% if (databaseType === 'sql' || databaseType === 'mongodb' || databaseType === 'couchbase') { %><% if (authenticationType === 'session') { %>, PersistentTokenRepository persistentTokenRepository<% } %>, AuthorityRepository authorityRepository<% } %><% if (cacheManagerIsAvailable === true) { %>, CacheManager cacheManager<% } %>) {
public UserService(UserRepository userRepository<% if (authenticationType !== 'oauth2') { %>, PasswordEncoder passwordEncoder<% } %><% if (searchEngine === 'elasticsearch') { %>, UserSearchRepository userSearchRepository<% } %><% if (databaseType === 'sql' || databaseType === 'mongodb' || databaseType === 'couchbase') { %><% if (authenticationType === 'session' && !reactive) { %>, PersistentTokenRepository persistentTokenRepository<% } %>, AuthorityRepository authorityRepository<% } %><% if (cacheManagerIsAvailable === true) { %>, CacheManager cacheManager<% } %>) {
this.userRepository = userRepository;
<%_ if (authenticationType !== 'oauth2') { _%>
this.passwordEncoder = passwordEncoder;
Expand All @@ -137,7 +137,7 @@ public class UserService {
this.userSearchRepository = userSearchRepository;
<%_ } _%>
<%_ if (databaseType === 'sql' || databaseType === 'mongodb' || databaseType === 'couchbase') { _%>
<%_ if (authenticationType === 'session') { _%>
<%_ if (authenticationType === 'session' && !reactive) { _%>
this.persistentTokenRepository = persistentTokenRepository;
<%_ } _%>
this.authorityRepository = authorityRepository;
Expand Down Expand Up @@ -652,7 +652,7 @@ public class UserService {
public <% if (reactive) { %>Mono<% } else { %>Optional<% } %><<%= asEntity('User') %>> getUserWithAuthorities() {
return SecurityUtils.getCurrentUserLogin().flatMap(userRepository::findOne<% if (databaseType === 'sql') { %>WithAuthorities<% } %>ByLogin);
}
<%_ if ((databaseType === 'sql' || databaseType === 'mongodb' || databaseType === 'couchbase') && authenticationType === 'session') { _%>
<%_ if ((databaseType === 'sql' || databaseType === 'mongodb' || databaseType === 'couchbase') && authenticationType === 'session' && !reactive) { _%>

/**
* Persistent Token are used for providing automatic authentication, they should be automatically deleted after
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ public final class RandomUtil {
public static String generateResetKey() {
return RandomStringUtils.randomNumeric(DEF_COUNT);
}
<%_ if (authenticationType === 'session') { _%>
<%_ if (authenticationType === 'session' && !reactive) { _%>

/**
* Generate a unique series to validate a persistent token, used in the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ public class AccountResource {
}
<%_ } else { _%>

<%_ if (authenticationType === 'session') { _%>
<%_ if (authenticationType === 'session' && !reactive) { _%>
import <%=packageName%>.domain.PersistentToken;
import <%=packageName%>.repository.PersistentTokenRepository;
<%_ } _%>
Expand Down Expand Up @@ -117,7 +117,7 @@ import reactor.core.publisher.Mono;
import javax.servlet.http.HttpServletRequest;
<%_ } _%>
import javax.validation.Valid;
<%_ if (authenticationType === 'session') { _%>
<%_ if (authenticationType === 'session' && !reactive) { _%>
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
<%_ } _%>
Expand All @@ -142,17 +142,17 @@ public class AccountResource {
private final UserService userService;

private final MailService mailService;
<%_ if (authenticationType === 'session') { _%>
<%_ if (authenticationType === 'session' && !reactive) { _%>

private final PersistentTokenRepository persistentTokenRepository;
<%_ } _%>

public AccountResource(UserRepository userRepository, UserService userService, MailService mailService<% if (authenticationType === 'session') { %>, PersistentTokenRepository persistentTokenRepository<% } %>) {
public AccountResource(UserRepository userRepository, UserService userService, MailService mailService<% if (authenticationType === 'session' && !reactive) { %>, PersistentTokenRepository persistentTokenRepository<% } %>) {

this.userRepository = userRepository;
this.userService = userService;
this.mailService = mailService;
<%_ if (authenticationType === 'session') { _%>
<%_ if (authenticationType === 'session' && !reactive) { _%>
this.persistentTokenRepository = persistentTokenRepository;
<%_ } _%>
}
Expand Down Expand Up @@ -288,7 +288,7 @@ public class AccountResource {
throw new InvalidPasswordException();
}
<% if (reactive) { %>return <% } %>userService.changePassword(passwordChangeDto.getCurrentPassword(), passwordChangeDto.getNewPassword());
}<% if (authenticationType === 'session') { %>
}<% if (authenticationType === 'session' && !reactive) { %>

/**
* {@code GET /account/sessions} : get the current open sessions.
Expand Down
Loading

0 comments on commit 5c0c2fc

Please sign in to comment.