diff --git a/.yo-rc.json b/.yo-rc.json index b144e84bf..be5eb8064 100644 --- a/.yo-rc.json +++ b/.yo-rc.json @@ -12,7 +12,6 @@ "prodDatabaseType": "mysql", "useCompass": false, "buildTool": "maven", - "rememberMeKey": "5c37379956bd1242f5636c8cb322c2966ad81277", "searchEngine": false, "enableTranslation": true, "applicationType": "monolith", @@ -31,6 +30,7 @@ "messageBroker": false, "serviceDiscoveryType": false, "clientPackageManager": "yarn", - "clientFramework": "angular1" + "clientFramework": "angular1", + "jwtSecretKey": "07a00779cfd8d372c73b40631b62c81503e1b18e" } -} +} \ No newline at end of file diff --git a/README.md b/README.md index e5914f95e..fe2584b8c 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # jhipsterSampleApplication -This application was generated using JHipster 4.7.0, you can find documentation and help at [https://jhipster.github.io/documentation-archive/v4.7.0](https://jhipster.github.io/documentation-archive/v4.7.0). +This application was generated using JHipster 4.7.0, you can find documentation and help at [http://www.jhipster.tech/documentation-archive/v4.7.0](http://www.jhipster.tech/documentation-archive/v4.7.0). ## Development @@ -32,6 +32,7 @@ Add the `-h` flag on any command to see how you can use it. For example, `bower For further instructions on how to develop with JHipster, have a look at [Using JHipster in development][]. + ## Building for production To optimize the jhipsterSampleApplication application for production, run: @@ -83,7 +84,7 @@ To stop it and remove the container, run: You can also fully dockerize your application and all the services that it depends on. To achieve this, first build a docker image of your app by running: - ./mvnw package -Pprod docker:build + ./mvnw package -Pprod dockerfile:build Then run: @@ -95,14 +96,14 @@ For more information refer to [Using Docker and Docker-Compose][], this page als To configure CI for your project, run the ci-cd sub-generator (`jhipster ci-cd`), this will let you generate configuration files for a number of Continuous Integration systems. Consult the [Setting up Continuous Integration][] page for more information. -[JHipster Homepage and latest documentation]: https://jhipster.github.io -[JHipster 4.7.0 archive]: https://jhipster.github.io/documentation-archive/v4.7.0 +[JHipster Homepage and latest documentation]: http://www.jhipster.tech +[JHipster 4.7.0 archive]: http://www.jhipster.tech/documentation-archive/v4.7.0 -[Using JHipster in development]: https://jhipster.github.io/documentation-archive/v4.7.0/development/ -[Using Docker and Docker-Compose]: https://jhipster.github.io/documentation-archive/v4.7.0/docker-compose -[Using JHipster in production]: https://jhipster.github.io/documentation-archive/v4.7.0/production/ -[Running tests page]: https://jhipster.github.io/documentation-archive/v4.7.0/running-tests/ -[Setting up Continuous Integration]: https://jhipster.github.io/documentation-archive/v4.7.0/setting-up-ci/ +[Using JHipster in development]: http://www.jhipster.tech/documentation-archive/v4.7.0/development/ +[Using Docker and Docker-Compose]: http://www.jhipster.tech/documentation-archive/v4.7.0/docker-compose +[Using JHipster in production]: http://www.jhipster.tech/documentation-archive/v4.7.0/production/ +[Running tests page]: http://www.jhipster.tech/documentation-archive/v4.7.0/running-tests/ +[Setting up Continuous Integration]: http://www.jhipster.tech/documentation-archive/v4.7.0/setting-up-ci/ [Gatling]: http://gatling.io/ [Node.js]: https://nodejs.org/ diff --git a/gulp/utils.js b/gulp/utils.js index abb80d3e3..f2379211f 100644 --- a/gulp/utils.js +++ b/gulp/utils.js @@ -18,6 +18,9 @@ function parseVersion() { var version = null; var pomXml = fs.readFileSync('pom.xml', 'utf8'); parseString(pomXml, function (err, result) { + if (err) { + throw new Error('Failed to parse pom.xml: ' + err); + } if (result.project.version && result.project.version[0]) { version = result.project.version[0]; } else if (result.project.parent && result.project.parent[0] && result.project.parent[0].version && result.project.parent[0].version[0]) { diff --git a/gulpfile.js b/gulpfile.js index 12fec5d5c..72f6a257f 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -1,4 +1,4 @@ -// Generated on 2017-08-23 using generator-jhipster 4.7.0 +// Generated on 2017-09-08 using generator-jhipster 4.7.0 'use strict'; var gulp = require('gulp'), diff --git a/pom.xml b/pom.xml index 113159fb2..b3657245d 100644 --- a/pom.xml +++ b/pom.xml @@ -21,19 +21,20 @@ 2.0.0 2.5 3.5 - 0.4.13 + 1.3.4 3.2.2 1.4 2.2.5 2.2.4 - 5.2.8.Final + 5.2.10.Final 2.6.0 0.7.9 1.8 1.0.0 1.1.3 1.1.9 + 0.7.0 3.6 2.0.0 4.11 @@ -342,6 +343,11 @@ + + io.jsonwebtoken + jjwt + ${jjwt.version} + net.logstash.logback @@ -480,6 +486,25 @@ + + docker-resources + validate + + copy-resources + + + target/ + + + src/main/docker/ + true + + **/*.yml + + + + + @@ -577,18 +602,24 @@ com.spotify - docker-maven-plugin - ${docker-maven-plugin.version} + dockerfile-maven-plugin + ${dockerfile-maven-plugin.version} + - jhipstersampleapplication - src/main/docker - - - / - ${project.build.directory} - ${project.build.finalName}.war - - + ${project.artifactId} + latest + ${project.build.directory} @@ -683,6 +714,14 @@ maven-war-plugin src/main/webapp/ + + + src/main/webapp + + WEB-INF/** + + + @@ -719,6 +758,14 @@ maven-war-plugin target/www/ + + + src/main/webapp + + WEB-INF/** + + + diff --git a/src/main/docker/.dockerignore b/src/main/docker/.dockerignore new file mode 100644 index 000000000..28e6b1387 --- /dev/null +++ b/src/main/docker/.dockerignore @@ -0,0 +1,5 @@ +# https://docs.docker.com/engine/reference/builder/#dockerignore-file +# by default ignore everything except the jar file +**/* +!*.jar +!*.war diff --git a/src/main/docker/Dockerfile b/src/main/docker/Dockerfile index 2dcafb4dc..6fae8c834 100644 --- a/src/main/docker/Dockerfile +++ b/src/main/docker/Dockerfile @@ -4,8 +4,7 @@ ENV SPRING_OUTPUT_ANSI_ENABLED=ALWAYS \ JHIPSTER_SLEEP=0 \ JAVA_OPTS="" -# add directly the war -ADD *.war /app.war +ADD @project.build.finalName@.war /app.war EXPOSE 8080 CMD echo "The application will start in ${JHIPSTER_SLEEP}s..." && \ diff --git a/src/main/java/io/github/jhipster/sample/JhipsterSampleApplicationApp.java b/src/main/java/io/github/jhipster/sample/JhipsterSampleApplicationApp.java index 0fc41a52a..0d866a092 100644 --- a/src/main/java/io/github/jhipster/sample/JhipsterSampleApplicationApp.java +++ b/src/main/java/io/github/jhipster/sample/JhipsterSampleApplicationApp.java @@ -39,7 +39,7 @@ public JhipsterSampleApplicationApp(Environment env) { *

* Spring profiles can be configured with a program arguments --spring.profiles.active=your-active-profile *

- * You can find more information on how profiles work with JHipster on https://jhipster.github.io/profiles/. + * You can find more information on how profiles work with JHipster on http://www.jhipster.tech/profiles/. */ @PostConstruct public void initApplication() { diff --git a/src/main/java/io/github/jhipster/sample/config/CacheConfiguration.java b/src/main/java/io/github/jhipster/sample/config/CacheConfiguration.java index 28f7eacdd..8ab26e3d6 100644 --- a/src/main/java/io/github/jhipster/sample/config/CacheConfiguration.java +++ b/src/main/java/io/github/jhipster/sample/config/CacheConfiguration.java @@ -37,11 +37,10 @@ public CacheConfiguration(JHipsterProperties jHipsterProperties) { @Bean public JCacheManagerCustomizer cacheManagerCustomizer() { return cm -> { + cm.createCache("users", jcacheConfiguration); cm.createCache(io.github.jhipster.sample.domain.User.class.getName(), jcacheConfiguration); cm.createCache(io.github.jhipster.sample.domain.Authority.class.getName(), jcacheConfiguration); cm.createCache(io.github.jhipster.sample.domain.User.class.getName() + ".authorities", jcacheConfiguration); - cm.createCache(io.github.jhipster.sample.domain.PersistentToken.class.getName(), jcacheConfiguration); - cm.createCache(io.github.jhipster.sample.domain.User.class.getName() + ".persistentTokens", jcacheConfiguration); cm.createCache(io.github.jhipster.sample.domain.BankAccount.class.getName(), jcacheConfiguration); cm.createCache(io.github.jhipster.sample.domain.BankAccount.class.getName() + ".operations", jcacheConfiguration); cm.createCache(io.github.jhipster.sample.domain.Label.class.getName(), jcacheConfiguration); diff --git a/src/main/java/io/github/jhipster/sample/config/SecurityConfiguration.java b/src/main/java/io/github/jhipster/sample/config/SecurityConfiguration.java index b300cf76d..87175d4b7 100644 --- a/src/main/java/io/github/jhipster/sample/config/SecurityConfiguration.java +++ b/src/main/java/io/github/jhipster/sample/config/SecurityConfiguration.java @@ -1,8 +1,8 @@ package io.github.jhipster.sample.config; import io.github.jhipster.sample.security.*; +import io.github.jhipster.sample.security.jwt.*; -import io.github.jhipster.config.JHipsterProperties; import io.github.jhipster.security.*; import org.springframework.beans.factory.BeanInitializationException; @@ -15,12 +15,11 @@ import org.springframework.security.config.annotation.web.builders.WebSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.data.repository.query.SecurityEvaluationContextExtension; -import org.springframework.security.web.authentication.RememberMeServices; -import org.springframework.security.web.csrf.CookieCsrfTokenRepository; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.web.filter.CorsFilter; @@ -35,20 +34,17 @@ public class SecurityConfiguration extends WebSecurityConfigurerAdapter { private final UserDetailsService userDetailsService; - private final JHipsterProperties jHipsterProperties; - - private final RememberMeServices rememberMeServices; + private final TokenProvider tokenProvider; private final CorsFilter corsFilter; public SecurityConfiguration(AuthenticationManagerBuilder authenticationManagerBuilder, UserDetailsService userDetailsService, - JHipsterProperties jHipsterProperties, RememberMeServices rememberMeServices, + TokenProvider tokenProvider, CorsFilter corsFilter) { this.authenticationManagerBuilder = authenticationManagerBuilder; this.userDetailsService = userDetailsService; - this.jHipsterProperties = jHipsterProperties; - this.rememberMeServices = rememberMeServices; + this.tokenProvider = tokenProvider; this.corsFilter = corsFilter; } @@ -63,21 +59,6 @@ public void init() { } } - @Bean - public AjaxAuthenticationSuccessHandler ajaxAuthenticationSuccessHandler() { - return new AjaxAuthenticationSuccessHandler(); - } - - @Bean - public AjaxAuthenticationFailureHandler ajaxAuthenticationFailureHandler() { - return new AjaxAuthenticationFailureHandler(); - } - - @Bean - public AjaxLogoutSuccessHandler ajaxLogoutSuccessHandler() { - return new AjaxLogoutSuccessHandler(); - } - @Bean public Http401UnauthorizedEntryPoint http401UnauthorizedEntryPoint() { return new Http401UnauthorizedEntryPoint(); @@ -104,49 +85,39 @@ public void configure(WebSecurity web) throws Exception { @Override protected void configure(HttpSecurity http) throws Exception { http - .csrf() - .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) - .and() .addFilterBefore(corsFilter, UsernamePasswordAuthenticationFilter.class) .exceptionHandling() .authenticationEntryPoint(http401UnauthorizedEntryPoint()) .and() - .rememberMe() - .rememberMeServices(rememberMeServices) - .rememberMeParameter("remember-me") - .key(jHipsterProperties.getSecurity().getRememberMe().getKey()) - .and() - .formLogin() - .loginProcessingUrl("/api/authentication") - .successHandler(ajaxAuthenticationSuccessHandler()) - .failureHandler(ajaxAuthenticationFailureHandler()) - .usernameParameter("j_username") - .passwordParameter("j_password") - .permitAll() - .and() - .logout() - .logoutUrl("/api/logout") - .logoutSuccessHandler(ajaxLogoutSuccessHandler()) - .permitAll() - .and() + .csrf() + .disable() .headers() .frameOptions() .disable() + .and() + .sessionManagement() + .sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() .antMatchers("/api/register").permitAll() .antMatchers("/api/activate").permitAll() .antMatchers("/api/authenticate").permitAll() - .antMatchers("/api/account/reset_password/init").permitAll() - .antMatchers("/api/account/reset_password/finish").permitAll() + .antMatchers("/api/account/reset-password/init").permitAll() + .antMatchers("/api/account/reset-password/finish").permitAll() .antMatchers("/api/profile-info").permitAll() .antMatchers("/api/**").authenticated() .antMatchers("/management/health").permitAll() .antMatchers("/management/**").hasAuthority(AuthoritiesConstants.ADMIN) .antMatchers("/v2/api-docs/**").permitAll() .antMatchers("/swagger-resources/configuration/ui").permitAll() - .antMatchers("/swagger-ui/index.html").hasAuthority(AuthoritiesConstants.ADMIN); + .antMatchers("/swagger-ui/index.html").hasAuthority(AuthoritiesConstants.ADMIN) + .and() + .apply(securityConfigurerAdapter()); + + } + private JWTConfigurer securityConfigurerAdapter() { + return new JWTConfigurer(tokenProvider); } @Bean diff --git a/src/main/java/io/github/jhipster/sample/domain/PersistentToken.java b/src/main/java/io/github/jhipster/sample/domain/PersistentToken.java deleted file mode 100644 index e96cdfc63..000000000 --- a/src/main/java/io/github/jhipster/sample/domain/PersistentToken.java +++ /dev/null @@ -1,135 +0,0 @@ -package io.github.jhipster.sample.domain; - -import com.fasterxml.jackson.annotation.JsonIgnore; -import org.hibernate.annotations.Cache; -import org.hibernate.annotations.CacheConcurrencyStrategy; -import java.time.LocalDate; - -import javax.persistence.*; -import javax.validation.constraints.NotNull; -import javax.validation.constraints.Size; -import java.io.Serializable; - -/** - * Persistent tokens are used by Spring Security to automatically log in users. - * - * @see io.github.jhipster.sample.security.PersistentTokenRememberMeServices - */ -@Entity -@Table(name = "jhi_persistent_token") -@Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE) -public class PersistentToken implements Serializable { - - private static final long serialVersionUID = 1L; - - private static final int MAX_USER_AGENT_LEN = 255; - - @Id - private String series; - - @JsonIgnore - @NotNull - @Column(name = "token_value", nullable = false) - private String tokenValue; - - @Column(name = "token_date") - private LocalDate tokenDate; - - //an IPV6 address max length is 39 characters - @Size(min = 0, max = 39) - @Column(name = "ip_address", length = 39) - private String ipAddress; - - @Column(name = "user_agent") - private String userAgent; - - @JsonIgnore - @ManyToOne - private User user; - - public String getSeries() { - return series; - } - - public void setSeries(String series) { - this.series = series; - } - - public String getTokenValue() { - return tokenValue; - } - - public void setTokenValue(String tokenValue) { - this.tokenValue = tokenValue; - } - - public LocalDate getTokenDate() { - return tokenDate; - } - - public void setTokenDate(LocalDate tokenDate) { - this.tokenDate = tokenDate; - } - - public String getIpAddress() { - return ipAddress; - } - - public void setIpAddress(String ipAddress) { - this.ipAddress = ipAddress; - } - - public String getUserAgent() { - return userAgent; - } - - public void setUserAgent(String userAgent) { - if (userAgent.length() >= MAX_USER_AGENT_LEN) { - this.userAgent = userAgent.substring(0, MAX_USER_AGENT_LEN - 1); - } else { - this.userAgent = userAgent; - } - } - - public User getUser() { - return user; - } - - public void setUser(User user) { - this.user = user; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - - PersistentToken that = (PersistentToken) o; - - if (!series.equals(that.series)) { - return false; - } - - return true; - } - - @Override - public int hashCode() { - return series.hashCode(); - } - - @Override - public String toString() { - return "PersistentToken{" + - "series='" + series + '\'' + - ", tokenValue='" + tokenValue + '\'' + - ", tokenDate=" + tokenDate + - ", ipAddress='" + ipAddress + '\'' + - ", userAgent='" + userAgent + '\'' + - "}"; - } -} diff --git a/src/main/java/io/github/jhipster/sample/domain/User.java b/src/main/java/io/github/jhipster/sample/domain/User.java index 20c2dc491..879d2a7bf 100644 --- a/src/main/java/io/github/jhipster/sample/domain/User.java +++ b/src/main/java/io/github/jhipster/sample/domain/User.java @@ -94,11 +94,6 @@ public class User extends AbstractAuditingEntity implements Serializable { @BatchSize(size = 20) private Set authorities = new HashSet<>(); - @JsonIgnore - @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true, mappedBy = "user") - @Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE) - private Set persistentTokens = new HashSet<>(); - public Long getId() { return id; } @@ -203,14 +198,6 @@ public void setAuthorities(Set authorities) { this.authorities = authorities; } - public Set getPersistentTokens() { - return persistentTokens; - } - - public void setPersistentTokens(Set persistentTokens) { - this.persistentTokens = persistentTokens; - } - @Override public boolean equals(Object o) { if (this == o) { diff --git a/src/main/java/io/github/jhipster/sample/repository/PersistentTokenRepository.java b/src/main/java/io/github/jhipster/sample/repository/PersistentTokenRepository.java deleted file mode 100644 index dbafae69a..000000000 --- a/src/main/java/io/github/jhipster/sample/repository/PersistentTokenRepository.java +++ /dev/null @@ -1,19 +0,0 @@ -package io.github.jhipster.sample.repository; - -import io.github.jhipster.sample.domain.PersistentToken; -import io.github.jhipster.sample.domain.User; -import java.time.LocalDate; -import org.springframework.data.jpa.repository.JpaRepository; - -import java.util.List; - -/** - * Spring Data JPA repository for the PersistentToken entity. - */ -public interface PersistentTokenRepository extends JpaRepository { - - List findByUser(User user); - - List findByTokenDateBefore(LocalDate localDate); - -} diff --git a/src/main/java/io/github/jhipster/sample/repository/UserRepository.java b/src/main/java/io/github/jhipster/sample/repository/UserRepository.java index 8b59a74e0..aef36b970 100644 --- a/src/main/java/io/github/jhipster/sample/repository/UserRepository.java +++ b/src/main/java/io/github/jhipster/sample/repository/UserRepository.java @@ -1,6 +1,7 @@ package io.github.jhipster.sample.repository; import io.github.jhipster.sample.domain.User; +import org.springframework.cache.annotation.Cacheable; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.EntityGraph; @@ -30,6 +31,7 @@ public interface UserRepository extends JpaRepository { User findOneWithAuthoritiesById(Long id); @EntityGraph(attributePaths = "authorities") + @Cacheable(cacheNames="users") Optional findOneWithAuthoritiesByLogin(String login); Page findAllByLoginNot(Pageable pageable, String login); diff --git a/src/main/java/io/github/jhipster/sample/security/PersistentTokenRememberMeServices.java b/src/main/java/io/github/jhipster/sample/security/PersistentTokenRememberMeServices.java deleted file mode 100644 index 5b971e9d0..000000000 --- a/src/main/java/io/github/jhipster/sample/security/PersistentTokenRememberMeServices.java +++ /dev/null @@ -1,236 +0,0 @@ -package io.github.jhipster.sample.security; - -import io.github.jhipster.sample.domain.PersistentToken; -import io.github.jhipster.sample.repository.PersistentTokenRepository; -import io.github.jhipster.sample.repository.UserRepository; -import io.github.jhipster.sample.service.util.RandomUtil; - -import com.google.common.cache.Cache; -import com.google.common.cache.CacheBuilder; - -import io.github.jhipster.config.JHipsterProperties; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.dao.DataAccessException; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UsernameNotFoundException; -import org.springframework.security.web.authentication.rememberme.*; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import java.io.Serializable; -import java.time.LocalDate; -import java.util.concurrent.TimeUnit; -import java.util.Arrays; -import java.util.Date; - -/** - * Custom implementation of Spring Security's RememberMeServices. - *

- * Persistent tokens are used by Spring Security to automatically log in users. - *

- * This is a specific implementation of Spring Security's remember-me authentication, but it is much - * more powerful than the standard implementations: - *

- *

- * Please note that it allows the use of the same token for 5 seconds, and this value stored in a specific - * cache during that period. This is to allow concurrent requests from the same user: otherwise, two - * requests being sent at the same time could invalidate each other's token. - *

- * This is inspired by: - *

- *

- * The main algorithm comes from Spring Security's PersistentTokenBasedRememberMeServices, but this class - * couldn't be cleanly extended. - */ -@Service -public class PersistentTokenRememberMeServices extends - AbstractRememberMeServices { - - private final Logger log = LoggerFactory.getLogger(PersistentTokenRememberMeServices.class); - - // Token is valid for one month - private static final int TOKEN_VALIDITY_DAYS = 31; - - private static final int TOKEN_VALIDITY_SECONDS = 60 * 60 * 24 * TOKEN_VALIDITY_DAYS; - - private static final int UPGRADED_TOKEN_VALIDITY_SECONDS = 5; - - private Cache upgradedTokenCache = CacheBuilder.newBuilder() - .expireAfterWrite(UPGRADED_TOKEN_VALIDITY_SECONDS, TimeUnit.SECONDS) - .build(); - - private final PersistentTokenRepository persistentTokenRepository; - - private final UserRepository userRepository; - - public PersistentTokenRememberMeServices(JHipsterProperties jHipsterProperties, - org.springframework.security.core.userdetails.UserDetailsService userDetailsService, - PersistentTokenRepository persistentTokenRepository, UserRepository userRepository) { - - super(jHipsterProperties.getSecurity().getRememberMe().getKey(), userDetailsService); - this.persistentTokenRepository = persistentTokenRepository; - this.userRepository = userRepository; - } - - @Override - protected UserDetails processAutoLoginCookie(String[] cookieTokens, HttpServletRequest request, - HttpServletResponse response) { - - synchronized (this) { // prevent 2 authentication requests from the same user in parallel - String login = null; - UpgradedRememberMeToken upgradedToken = upgradedTokenCache.getIfPresent(cookieTokens[0]); - if (upgradedToken != null) { - login = upgradedToken.getUserLoginIfValidAndRecentUpgrade(cookieTokens); - log.debug("Detected previously upgraded login token for user '{}'", login); - } - - if (login == null) { - PersistentToken token = getPersistentToken(cookieTokens); - login = token.getUser().getLogin(); - - // Token also matches, so login is valid. Update the token value, keeping the *same* series number. - log.debug("Refreshing persistent login token for user '{}', series '{}'", login, token.getSeries()); - token.setTokenDate(LocalDate.now()); - token.setTokenValue(RandomUtil.generateTokenData()); - token.setIpAddress(request.getRemoteAddr()); - token.setUserAgent(request.getHeader("User-Agent")); - try { - persistentTokenRepository.saveAndFlush(token); - } catch (DataAccessException e) { - log.error("Failed to update token: ", e); - throw new RememberMeAuthenticationException("Autologin failed due to data access problem", e); - } - addCookie(token, request, response); - upgradedTokenCache.put(cookieTokens[0], new UpgradedRememberMeToken(cookieTokens, login)); - } - return getUserDetailsService().loadUserByUsername(login); - } - } - - @Override - protected void onLoginSuccess(HttpServletRequest request, HttpServletResponse response, Authentication - successfulAuthentication) { - - String login = successfulAuthentication.getName(); - - log.debug("Creating new persistent login for user {}", login); - PersistentToken token = userRepository.findOneByLogin(login).map(u -> { - PersistentToken t = new PersistentToken(); - t.setSeries(RandomUtil.generateSeriesData()); - t.setUser(u); - t.setTokenValue(RandomUtil.generateTokenData()); - t.setTokenDate(LocalDate.now()); - t.setIpAddress(request.getRemoteAddr()); - t.setUserAgent(request.getHeader("User-Agent")); - return t; - }).orElseThrow(() -> new UsernameNotFoundException("User " + login + " was not found in the database")); - try { - persistentTokenRepository.saveAndFlush(token); - addCookie(token, request, response); - } catch (DataAccessException e) { - log.error("Failed to save persistent token ", e); - } - } - - /** - * When logout occurs, only invalidate the current token, and not all user sessions. - *

- * The standard Spring Security implementations are too basic: they invalidate all tokens for the - * current user, so when he logs out from one browser, all his other sessions are destroyed. - */ - @Override - @Transactional - public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) { - String rememberMeCookie = extractRememberMeCookie(request); - if (rememberMeCookie != null && rememberMeCookie.length() != 0) { - try { - String[] cookieTokens = decodeCookie(rememberMeCookie); - PersistentToken token = getPersistentToken(cookieTokens); - persistentTokenRepository.delete(token); - } catch (InvalidCookieException ice) { - log.info("Invalid cookie, no persistent token could be deleted", ice); - } catch (RememberMeAuthenticationException rmae) { - log.debug("No persistent token found, so no token could be deleted", rmae); - } - } - super.logout(request, response, authentication); - } - - /** - * Validate the token and return it. - */ - private PersistentToken getPersistentToken(String[] cookieTokens) { - if (cookieTokens.length != 2) { - throw new InvalidCookieException("Cookie token did not contain " + 2 + - " tokens, but contained '" + Arrays.asList(cookieTokens) + "'"); - } - String presentedSeries = cookieTokens[0]; - String presentedToken = cookieTokens[1]; - PersistentToken token = persistentTokenRepository.findOne(presentedSeries); - - if (token == null) { - // No series match, so we can't authenticate using this cookie - throw new RememberMeAuthenticationException("No persistent token found for series id: " + presentedSeries); - } - - // We have a match for this user/series combination - log.info("presentedToken={} / tokenValue={}", presentedToken, token.getTokenValue()); - if (!presentedToken.equals(token.getTokenValue())) { - // Token doesn't match series value. Delete this session and throw an exception. - persistentTokenRepository.delete(token); - throw new CookieTheftException("Invalid remember-me token (Series/token) mismatch. Implies previous " + - "cookie theft attack."); - } - - if (token.getTokenDate().plusDays(TOKEN_VALIDITY_DAYS).isBefore(LocalDate.now())) { - persistentTokenRepository.delete(token); - throw new RememberMeAuthenticationException("Remember-me login has expired"); - } - return token; - } - - private void addCookie(PersistentToken token, HttpServletRequest request, HttpServletResponse response) { - setCookie( - new String[]{token.getSeries(), token.getTokenValue()}, - TOKEN_VALIDITY_SECONDS, request, response); - } - - private static class UpgradedRememberMeToken implements Serializable { - - private static final long serialVersionUID = 1L; - - private String[] upgradedToken; - - private Date upgradeTime; - - private String userLogin; - - UpgradedRememberMeToken(String[] upgradedToken, String userLogin) { - this.upgradedToken = upgradedToken; - this.userLogin = userLogin; - this.upgradeTime = new Date(); - } - - String getUserLoginIfValidAndRecentUpgrade(String[] currentToken) { - if (currentToken[0].equals(this.upgradedToken[0]) && - currentToken[1].equals(this.upgradedToken[1]) && - (upgradeTime.getTime() + UPGRADED_TOKEN_VALIDITY_SECONDS * 1000) > new Date().getTime()) { - return this.userLogin; - } - return null; - } - } -} diff --git a/src/main/java/io/github/jhipster/sample/security/SecurityUtils.java b/src/main/java/io/github/jhipster/sample/security/SecurityUtils.java index 03756ff76..e6ed57bc2 100644 --- a/src/main/java/io/github/jhipster/sample/security/SecurityUtils.java +++ b/src/main/java/io/github/jhipster/sample/security/SecurityUtils.java @@ -33,6 +33,20 @@ public static String getCurrentUserLogin() { return userName; } + /** + * Get the JWT of the current user. + * + * @return the JWT of the current user + */ + public static String getCurrentUserJWT() { + SecurityContext securityContext = SecurityContextHolder.getContext(); + Authentication authentication = securityContext.getAuthentication(); + if (authentication != null && authentication.getCredentials() instanceof String) { + return (String) authentication.getCredentials(); + } + return null; + } + /** * Check if a user is authenticated. * diff --git a/src/main/java/io/github/jhipster/sample/security/jwt/JWTConfigurer.java b/src/main/java/io/github/jhipster/sample/security/jwt/JWTConfigurer.java new file mode 100644 index 000000000..88da16d60 --- /dev/null +++ b/src/main/java/io/github/jhipster/sample/security/jwt/JWTConfigurer.java @@ -0,0 +1,23 @@ +package io.github.jhipster.sample.security.jwt; + +import org.springframework.security.config.annotation.SecurityConfigurerAdapter; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.web.DefaultSecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + +public class JWTConfigurer extends SecurityConfigurerAdapter { + + public static final String AUTHORIZATION_HEADER = "Authorization"; + + private TokenProvider tokenProvider; + + public JWTConfigurer(TokenProvider tokenProvider) { + this.tokenProvider = tokenProvider; + } + + @Override + public void configure(HttpSecurity http) throws Exception { + JWTFilter customFilter = new JWTFilter(tokenProvider); + http.addFilterBefore(customFilter, UsernamePasswordAuthenticationFilter.class); + } +} diff --git a/src/main/java/io/github/jhipster/sample/security/jwt/JWTFilter.java b/src/main/java/io/github/jhipster/sample/security/jwt/JWTFilter.java new file mode 100644 index 000000000..6daa2c4c4 --- /dev/null +++ b/src/main/java/io/github/jhipster/sample/security/jwt/JWTFilter.java @@ -0,0 +1,46 @@ +package io.github.jhipster.sample.security.jwt; + +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.util.StringUtils; +import org.springframework.web.filter.GenericFilterBean; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import java.io.IOException; + +/** + * Filters incoming requests and installs a Spring Security principal if a header corresponding to a valid user is + * found. + */ +public class JWTFilter extends GenericFilterBean { + + private TokenProvider tokenProvider; + + public JWTFilter(TokenProvider tokenProvider) { + this.tokenProvider = tokenProvider; + } + + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) + throws IOException, ServletException { + HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest; + String jwt = resolveToken(httpServletRequest); + if (StringUtils.hasText(jwt) && this.tokenProvider.validateToken(jwt)) { + Authentication authentication = this.tokenProvider.getAuthentication(jwt); + SecurityContextHolder.getContext().setAuthentication(authentication); + } + filterChain.doFilter(servletRequest, servletResponse); + } + + private String resolveToken(HttpServletRequest request){ + String bearerToken = request.getHeader(JWTConfigurer.AUTHORIZATION_HEADER); + if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) { + return bearerToken.substring(7, bearerToken.length()); + } + return null; + } +} diff --git a/src/main/java/io/github/jhipster/sample/security/jwt/TokenProvider.java b/src/main/java/io/github/jhipster/sample/security/jwt/TokenProvider.java new file mode 100644 index 000000000..5adb611a1 --- /dev/null +++ b/src/main/java/io/github/jhipster/sample/security/jwt/TokenProvider.java @@ -0,0 +1,109 @@ +package io.github.jhipster.sample.security.jwt; + +import io.github.jhipster.config.JHipsterProperties; + +import java.util.*; +import java.util.stream.Collectors; +import javax.annotation.PostConstruct; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.User; +import org.springframework.stereotype.Component; + +import io.jsonwebtoken.*; + +@Component +public class TokenProvider { + + private final Logger log = LoggerFactory.getLogger(TokenProvider.class); + + private static final String AUTHORITIES_KEY = "auth"; + + private String secretKey; + + private long tokenValidityInMilliseconds; + + private long tokenValidityInMillisecondsForRememberMe; + + private final JHipsterProperties jHipsterProperties; + + public TokenProvider(JHipsterProperties jHipsterProperties) { + this.jHipsterProperties = jHipsterProperties; + } + + @PostConstruct + public void init() { + this.secretKey = + jHipsterProperties.getSecurity().getAuthentication().getJwt().getSecret(); + + this.tokenValidityInMilliseconds = + 1000 * jHipsterProperties.getSecurity().getAuthentication().getJwt().getTokenValidityInSeconds(); + this.tokenValidityInMillisecondsForRememberMe = + 1000 * jHipsterProperties.getSecurity().getAuthentication().getJwt().getTokenValidityInSecondsForRememberMe(); + } + + public String createToken(Authentication authentication, Boolean rememberMe) { + String authorities = authentication.getAuthorities().stream() + .map(GrantedAuthority::getAuthority) + .collect(Collectors.joining(",")); + + long now = (new Date()).getTime(); + Date validity; + if (rememberMe) { + validity = new Date(now + this.tokenValidityInMillisecondsForRememberMe); + } else { + validity = new Date(now + this.tokenValidityInMilliseconds); + } + + return Jwts.builder() + .setSubject(authentication.getName()) + .claim(AUTHORITIES_KEY, authorities) + .signWith(SignatureAlgorithm.HS512, secretKey) + .setExpiration(validity) + .compact(); + } + + public Authentication getAuthentication(String token) { + Claims claims = Jwts.parser() + .setSigningKey(secretKey) + .parseClaimsJws(token) + .getBody(); + + Collection authorities = + Arrays.stream(claims.get(AUTHORITIES_KEY).toString().split(",")) + .map(SimpleGrantedAuthority::new) + .collect(Collectors.toList()); + + User principal = new User(claims.getSubject(), "", authorities); + + return new UsernamePasswordAuthenticationToken(principal, token, authorities); + } + + public boolean validateToken(String authToken) { + try { + Jwts.parser().setSigningKey(secretKey).parseClaimsJws(authToken); + return true; + } catch (SignatureException e) { + log.info("Invalid JWT signature."); + log.trace("Invalid JWT signature trace: {}", e); + } catch (MalformedJwtException e) { + log.info("Invalid JWT token."); + log.trace("Invalid JWT token trace: {}", e); + } catch (ExpiredJwtException e) { + log.info("Expired JWT token."); + log.trace("Expired JWT token trace: {}", e); + } catch (UnsupportedJwtException e) { + log.info("Unsupported JWT token."); + log.trace("Unsupported JWT token trace: {}", e); + } catch (IllegalArgumentException e) { + log.info("JWT token compact of handler are invalid."); + log.trace("JWT token compact of handler are invalid trace: {}", e); + } + return false; + } +} diff --git a/src/main/java/io/github/jhipster/sample/service/UserService.java b/src/main/java/io/github/jhipster/sample/service/UserService.java index 7cd824aaf..d24af9518 100644 --- a/src/main/java/io/github/jhipster/sample/service/UserService.java +++ b/src/main/java/io/github/jhipster/sample/service/UserService.java @@ -3,7 +3,6 @@ import io.github.jhipster.sample.domain.Authority; import io.github.jhipster.sample.domain.User; import io.github.jhipster.sample.repository.AuthorityRepository; -import io.github.jhipster.sample.repository.PersistentTokenRepository; import io.github.jhipster.sample.config.Constants; import io.github.jhipster.sample.repository.UserRepository; import io.github.jhipster.sample.security.AuthoritiesConstants; @@ -13,6 +12,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.cache.CacheManager; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.scheduling.annotation.Scheduled; @@ -20,7 +20,6 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.time.LocalDate; import java.time.Instant; import java.time.temporal.ChronoUnit; import java.util.*; @@ -39,15 +38,15 @@ public class UserService { private final PasswordEncoder passwordEncoder; - private final PersistentTokenRepository persistentTokenRepository; - private final AuthorityRepository authorityRepository; - public UserService(UserRepository userRepository, PasswordEncoder passwordEncoder, PersistentTokenRepository persistentTokenRepository, AuthorityRepository authorityRepository) { + private final CacheManager cacheManager; + + public UserService(UserRepository userRepository, PasswordEncoder passwordEncoder, AuthorityRepository authorityRepository, CacheManager cacheManager) { this.userRepository = userRepository; this.passwordEncoder = passwordEncoder; - this.persistentTokenRepository = persistentTokenRepository; this.authorityRepository = authorityRepository; + this.cacheManager = cacheManager; } public Optional activateRegistration(String key) { @@ -57,6 +56,7 @@ public Optional activateRegistration(String key) { // activate given user for the registration key. user.setActivated(true); user.setActivationKey(null); + cacheManager.getCache("users").evict(user.getLogin()); log.debug("Activated user: {}", user); return user; }); @@ -71,6 +71,7 @@ public Optional completePasswordReset(String newPassword, String key) { user.setPassword(passwordEncoder.encode(newPassword)); user.setResetKey(null); user.setResetDate(null); + cacheManager.getCache("users").evict(user.getLogin()); return user; }); } @@ -81,6 +82,7 @@ public Optional requestPasswordReset(String mail) { .map(user -> { user.setResetKey(RandomUtil.generateResetKey()); user.setResetDate(Instant.now()); + cacheManager.getCache("users").evict(user.getLogin()); return user; }); } @@ -156,6 +158,7 @@ public void updateUser(String firstName, String lastName, String email, String l user.setEmail(email); user.setLangKey(langKey); user.setImageUrl(imageUrl); + cacheManager.getCache("users").evict(user.getLogin()); log.debug("Changed Information for User: {}", user); }); } @@ -182,6 +185,7 @@ public Optional updateUser(UserDTO userDTO) { userDTO.getAuthorities().stream() .map(authorityRepository::findOne) .forEach(managedAuthorities::add); + cacheManager.getCache("users").evict(user.getLogin()); log.debug("Changed Information for User: {}", user); return user; }) @@ -191,6 +195,7 @@ public Optional updateUser(UserDTO userDTO) { public void deleteUser(String login) { userRepository.findOneByLogin(login).ifPresent(user -> { userRepository.delete(user); + cacheManager.getCache("users").evict(login); log.debug("Deleted User: {}", user); }); } @@ -199,6 +204,7 @@ public void changePassword(String password) { userRepository.findOneByLogin(SecurityUtils.getCurrentUserLogin()).ifPresent(user -> { String encryptedPassword = passwordEncoder.encode(password); user.setPassword(encryptedPassword); + cacheManager.getCache("users").evict(user.getLogin()); log.debug("Changed password for User: {}", user); }); } @@ -223,23 +229,6 @@ public User getUserWithAuthorities() { return userRepository.findOneWithAuthoritiesByLogin(SecurityUtils.getCurrentUserLogin()).orElse(null); } - /** - * Persistent Token are used for providing automatic authentication, they should be automatically deleted after - * 30 days. - *

- * This is scheduled to get fired everyday, at midnight. - */ - @Scheduled(cron = "0 0 0 * * ?") - public void removeOldPersistentTokens() { - LocalDate now = LocalDate.now(); - persistentTokenRepository.findByTokenDateBefore(now.minusMonths(1)).forEach(token -> { - log.debug("Deleting token {}", token.getSeries()); - User user = token.getUser(); - user.getPersistentTokens().remove(token); - persistentTokenRepository.delete(token); - }); - } - /** * Not activated users should be automatically deleted after 3 days. *

@@ -251,6 +240,7 @@ public void removeNotActivatedUsers() { for (User user : users) { log.debug("Deleting not activated user {}", user.getLogin()); userRepository.delete(user); + cacheManager.getCache("users").evict(user.getLogin()); } } diff --git a/src/main/java/io/github/jhipster/sample/service/util/RandomUtil.java b/src/main/java/io/github/jhipster/sample/service/util/RandomUtil.java index 7ecb4e2dc..468232b53 100644 --- a/src/main/java/io/github/jhipster/sample/service/util/RandomUtil.java +++ b/src/main/java/io/github/jhipster/sample/service/util/RandomUtil.java @@ -38,23 +38,4 @@ public static String generateActivationKey() { public static String generateResetKey() { return RandomStringUtils.randomNumeric(DEF_COUNT); } - - /** - * Generate a unique series to validate a persistent token, used in the - * authentication remember-me mechanism. - * - * @return the generated series data - */ - public static String generateSeriesData() { - return RandomStringUtils.randomAlphanumeric(DEF_COUNT); - } - - /** - * Generate a persistent token, used in the authentication remember-me mechanism. - * - * @return the generated token data - */ - public static String generateTokenData() { - return RandomStringUtils.randomAlphanumeric(DEF_COUNT); - } } diff --git a/src/main/java/io/github/jhipster/sample/web/rest/AccountResource.java b/src/main/java/io/github/jhipster/sample/web/rest/AccountResource.java index 9f5005d62..f3041717f 100644 --- a/src/main/java/io/github/jhipster/sample/web/rest/AccountResource.java +++ b/src/main/java/io/github/jhipster/sample/web/rest/AccountResource.java @@ -2,9 +2,7 @@ import com.codahale.metrics.annotation.Timed; -import io.github.jhipster.sample.domain.PersistentToken; import io.github.jhipster.sample.domain.User; -import io.github.jhipster.sample.repository.PersistentTokenRepository; import io.github.jhipster.sample.repository.UserRepository; import io.github.jhipster.sample.security.SecurityUtils; import io.github.jhipster.sample.service.MailService; @@ -25,8 +23,6 @@ import javax.servlet.http.HttpServletRequest; import javax.validation.Valid; -import java.io.UnsupportedEncodingException; -import java.net.URLDecoder; import java.util.*; /** @@ -44,17 +40,14 @@ public class AccountResource { private final MailService mailService; - private final PersistentTokenRepository persistentTokenRepository; - private static final String CHECK_ERROR_MESSAGE = "Incorrect password"; public AccountResource(UserRepository userRepository, UserService userService, - MailService mailService, PersistentTokenRepository persistentTokenRepository) { + MailService mailService) { this.userRepository = userRepository; this.userService = userService; this.mailService = mailService; - this.persistentTokenRepository = persistentTokenRepository; } /** @@ -155,12 +148,12 @@ public ResponseEntity saveAccount(@Valid @RequestBody UserDTO userDTO) { } /** - * POST /account/change_password : changes the current user's password + * POST /account/change-password : changes the current user's password * * @param password the new password * @return the ResponseEntity with status 200 (OK), or status 400 (Bad Request) if the new password is not strong enough */ - @PostMapping(path = "/account/change_password", + @PostMapping(path = "/account/change-password", produces = MediaType.TEXT_PLAIN_VALUE) @Timed public ResponseEntity changePassword(@RequestBody String password) { @@ -172,54 +165,12 @@ public ResponseEntity changePassword(@RequestBody String password) { } /** - * GET /account/sessions : get the current open sessions. - * - * @return the ResponseEntity with status 200 (OK) and the current open sessions in body, - * or status 500 (Internal Server Error) if the current open sessions couldn't be retrieved - */ - @GetMapping("/account/sessions") - @Timed - public ResponseEntity> getCurrentSessions() { - return userRepository.findOneByLogin(SecurityUtils.getCurrentUserLogin()) - .map(user -> new ResponseEntity<>( - persistentTokenRepository.findByUser(user), - HttpStatus.OK)) - .orElse(new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR)); - } - - /** - * DELETE /account/sessions?series={series} : invalidate an existing session. - * - * - You can only delete your own sessions, not any other user's session - * - If you delete one of your existing sessions, and that you are currently logged in on that session, you will - * still be able to use that session, until you quit your browser: it does not work in real time (there is - * no API for that), it only removes the "remember me" cookie - * - This is also true if you invalidate your current session: you will still be able to use it until you close - * your browser or that the session times out. But automatic login (the "remember me" cookie) will not work - * anymore. - * There is an API to invalidate the current session, but there is no API to check which session uses which - * cookie. - * - * @param series the series of an existing session - * @throws UnsupportedEncodingException if the series couldnt be URL decoded - */ - @DeleteMapping("/account/sessions/{series}") - @Timed - public void invalidateSession(@PathVariable String series) throws UnsupportedEncodingException { - String decodedSeries = URLDecoder.decode(series, "UTF-8"); - userRepository.findOneByLogin(SecurityUtils.getCurrentUserLogin()).ifPresent(u -> - persistentTokenRepository.findByUser(u).stream() - .filter(persistentToken -> StringUtils.equals(persistentToken.getSeries(), decodedSeries)) - .findAny().ifPresent(t -> persistentTokenRepository.delete(decodedSeries))); - } - - /** - * POST /account/reset_password/init : Send an email to reset the password of the user + * POST /account/reset-password/init : Send an email to reset the password of the user * * @param mail the mail of the user * @return the ResponseEntity with status 200 (OK) if the email was sent, or status 400 (Bad Request) if the email address is not registered */ - @PostMapping(path = "/account/reset_password/init", + @PostMapping(path = "/account/reset-password/init", produces = MediaType.TEXT_PLAIN_VALUE) @Timed public ResponseEntity requestPasswordReset(@RequestBody String mail) { @@ -231,13 +182,13 @@ public ResponseEntity requestPasswordReset(@RequestBody String mail) { } /** - * POST /account/reset_password/finish : Finish to reset the password of the user + * POST /account/reset-password/finish : Finish to reset the password of the user * * @param keyAndPassword the generated key and the new password * @return the ResponseEntity with status 200 (OK) if the password has been reset, * or status 400 (Bad Request) or 500 (Internal Server Error) if the password could not be reset */ - @PostMapping(path = "/account/reset_password/finish", + @PostMapping(path = "/account/reset-password/finish", produces = MediaType.TEXT_PLAIN_VALUE) @Timed public ResponseEntity finishPasswordReset(@RequestBody KeyAndPasswordVM keyAndPassword) { diff --git a/src/main/java/io/github/jhipster/sample/web/rest/UserJWTController.java b/src/main/java/io/github/jhipster/sample/web/rest/UserJWTController.java new file mode 100644 index 000000000..ce1194b8c --- /dev/null +++ b/src/main/java/io/github/jhipster/sample/web/rest/UserJWTController.java @@ -0,0 +1,84 @@ +package io.github.jhipster.sample.web.rest; + +import io.github.jhipster.sample.security.jwt.JWTConfigurer; +import io.github.jhipster.sample.security.jwt.TokenProvider; +import io.github.jhipster.sample.web.rest.vm.LoginVM; + +import com.codahale.metrics.annotation.Timed; +import com.fasterxml.jackson.annotation.JsonProperty; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.bind.annotation.*; + +import javax.servlet.http.HttpServletResponse; +import javax.validation.Valid; +import java.util.Collections; + +/** + * Controller to authenticate users. + */ +@RestController +@RequestMapping("/api") +public class UserJWTController { + + private final Logger log = LoggerFactory.getLogger(UserJWTController.class); + + private final TokenProvider tokenProvider; + + private final AuthenticationManager authenticationManager; + + public UserJWTController(TokenProvider tokenProvider, AuthenticationManager authenticationManager) { + this.tokenProvider = tokenProvider; + this.authenticationManager = authenticationManager; + } + + @PostMapping("/authenticate") + @Timed + public ResponseEntity authorize(@Valid @RequestBody LoginVM loginVM, HttpServletResponse response) { + + UsernamePasswordAuthenticationToken authenticationToken = + new UsernamePasswordAuthenticationToken(loginVM.getUsername(), loginVM.getPassword()); + + try { + Authentication authentication = this.authenticationManager.authenticate(authenticationToken); + SecurityContextHolder.getContext().setAuthentication(authentication); + boolean rememberMe = (loginVM.isRememberMe() == null) ? false : loginVM.isRememberMe(); + String jwt = tokenProvider.createToken(authentication, rememberMe); + response.addHeader(JWTConfigurer.AUTHORIZATION_HEADER, "Bearer " + jwt); + return ResponseEntity.ok(new JWTToken(jwt)); + } catch (AuthenticationException ae) { + log.trace("Authentication exception trace: {}", ae); + return new ResponseEntity<>(Collections.singletonMap("AuthenticationException", + ae.getLocalizedMessage()), HttpStatus.UNAUTHORIZED); + } + } + + /** + * Object to return as body in JWT Authentication. + */ + static class JWTToken { + + private String idToken; + + JWTToken(String idToken) { + this.idToken = idToken; + } + + @JsonProperty("id_token") + String getIdToken() { + return idToken; + } + + void setIdToken(String idToken) { + this.idToken = idToken; + } + } +} diff --git a/src/main/java/io/github/jhipster/sample/web/rest/errors/ErrorConstants.java b/src/main/java/io/github/jhipster/sample/web/rest/errors/ErrorConstants.java index 781231dd2..4efc45a69 100644 --- a/src/main/java/io/github/jhipster/sample/web/rest/errors/ErrorConstants.java +++ b/src/main/java/io/github/jhipster/sample/web/rest/errors/ErrorConstants.java @@ -3,7 +3,6 @@ public final class ErrorConstants { public static final String ERR_CONCURRENCY_FAILURE = "error.concurrencyFailure"; - public static final String ERR_ACCESS_DENIED = "error.accessDenied"; public static final String ERR_VALIDATION = "error.validation"; public static final String ERR_METHOD_NOT_SUPPORTED = "error.methodNotSupported"; public static final String ERR_INTERNAL_SERVER_ERROR = "error.internalServerError"; diff --git a/src/main/java/io/github/jhipster/sample/web/rest/errors/ExceptionTranslator.java b/src/main/java/io/github/jhipster/sample/web/rest/errors/ExceptionTranslator.java index 68cddc00a..2cf54e5e7 100644 --- a/src/main/java/io/github/jhipster/sample/web/rest/errors/ExceptionTranslator.java +++ b/src/main/java/io/github/jhipster/sample/web/rest/errors/ExceptionTranslator.java @@ -14,7 +14,9 @@ import org.springframework.validation.FieldError; import org.springframework.web.HttpRequestMethodNotSupportedException; import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.MissingServletRequestParameterException; import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.support.MissingServletRequestPartException; /** * Controller advice to translate the server side exceptions to client-friendly json structures. @@ -51,11 +53,25 @@ public ParameterizedErrorVM processParameterizedValidationError(CustomParameteri return ex.getErrorVM(); } + @ExceptionHandler(MissingServletRequestPartException.class) + @ResponseBody + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ErrorVM processMissingServletRequestPartException(MissingServletRequestPartException e) { + return new ErrorVM("error.http." + HttpStatus.BAD_REQUEST, e.getMessage()); + } + + @ExceptionHandler(MissingServletRequestParameterException.class) + @ResponseBody + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ErrorVM processMissingServletRequestParameterException(MissingServletRequestParameterException e) { + return new ErrorVM("error.http." + HttpStatus.BAD_REQUEST, e.getMessage()); + } + @ExceptionHandler(AccessDeniedException.class) @ResponseStatus(HttpStatus.FORBIDDEN) @ResponseBody public ErrorVM processAccessDeniedException(AccessDeniedException e) { - return new ErrorVM(ErrorConstants.ERR_ACCESS_DENIED, e.getMessage()); + return new ErrorVM("error.http." + HttpStatus.FORBIDDEN, e.getMessage()); } @ExceptionHandler(HttpRequestMethodNotSupportedException.class) @@ -77,7 +93,7 @@ public ResponseEntity processException(Exception ex) { ResponseStatus responseStatus = AnnotationUtils.findAnnotation(ex.getClass(), ResponseStatus.class); if (responseStatus != null) { builder = ResponseEntity.status(responseStatus.value()); - errorVM = new ErrorVM("error." + responseStatus.value().value(), responseStatus.reason()); + errorVM = new ErrorVM("error.http." + responseStatus.value().value(), responseStatus.reason()); } else { builder = ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR); errorVM = new ErrorVM(ErrorConstants.ERR_INTERNAL_SERVER_ERROR, "Internal server error"); diff --git a/src/main/java/io/github/jhipster/sample/web/rest/vm/LoginVM.java b/src/main/java/io/github/jhipster/sample/web/rest/vm/LoginVM.java new file mode 100644 index 000000000..c4bc96501 --- /dev/null +++ b/src/main/java/io/github/jhipster/sample/web/rest/vm/LoginVM.java @@ -0,0 +1,55 @@ +package io.github.jhipster.sample.web.rest.vm; + +import io.github.jhipster.sample.config.Constants; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Pattern; +import javax.validation.constraints.Size; + +/** + * View Model object for storing a user's credentials. + */ +public class LoginVM { + + @Pattern(regexp = Constants.LOGIN_REGEX) + @NotNull + @Size(min = 1, max = 50) + private String username; + + @NotNull + @Size(min = ManagedUserVM.PASSWORD_MIN_LENGTH, max = ManagedUserVM.PASSWORD_MAX_LENGTH) + private String password; + + private Boolean rememberMe; + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public Boolean isRememberMe() { + return rememberMe; + } + + public void setRememberMe(Boolean rememberMe) { + this.rememberMe = rememberMe; + } + + @Override + public String toString() { + return "LoginVM{" + + "username='" + username + '\'' + + ", rememberMe=" + rememberMe + + '}'; + } +} diff --git a/src/main/resources/banner.txt b/src/main/resources/banner.txt index 669a09e24..db090f7e7 100644 --- a/src/main/resources/banner.txt +++ b/src/main/resources/banner.txt @@ -7,4 +7,4 @@ ${AnsiColor.GREEN} ╚═════╝ ${AnsiColor.RED} ╚═╝ ╚═╝ ╚═══════╝ ╚═╝ ╚═════╝ ╚═╝ ╚═══════╝ ╚═╝ ╚═╝ ${AnsiColor.BRIGHT_BLUE}:: JHipster 🤓 :: Running Spring Boot ${spring-boot.version} :: -:: https://jhipster.github.io ::${AnsiColor.DEFAULT} +:: http://www.jhipster.tech ::${AnsiColor.DEFAULT} diff --git a/src/main/resources/config/application-dev.yml b/src/main/resources/config/application-dev.yml index a031be0fe..26a3c184b 100644 --- a/src/main/resources/config/application-dev.yml +++ b/src/main/resources/config/application-dev.yml @@ -3,8 +3,8 @@ # # This configuration overrides the application.yml file. # -# More information on profiles: https://jhipster.github.io/profiles/ -# More information on configuration properties: https://jhipster.github.io/common-application-properties/ +# More information on profiles: http://www.jhipster.tech/profiles/ +# More information on configuration properties: http://www.jhipster.tech/common-application-properties/ # =================================================================== # =================================================================== @@ -78,7 +78,7 @@ server: # =================================================================== # JHipster specific properties # -# Full reference is available at: https://jhipster.github.io/common-application-properties/ +# Full reference is available at: http://www.jhipster.tech/common-application-properties/ # =================================================================== jhipster: @@ -97,9 +97,12 @@ jhipster: allow-credentials: true max-age: 1800 security: - remember-me: - # security key (this key should be unique for your application, and kept secret) - key: 5c37379956bd1242f5636c8cb322c2966ad81277 + authentication: + jwt: + secret: my-secret-token-to-change-in-production + # Token is valid 24 hours + token-validity-in-seconds: 86400 + token-validity-in-seconds-for-remember-me: 2592000 mail: # specific JHipster mail property, for standard properties see MailProperties from: jhipsterSampleApplication@localhost base-url: http://127.0.0.1:8080 @@ -129,7 +132,7 @@ jhipster: # to have type-safe configuration, like in the JHipsterProperties above # # More documentation is available at: -# https://jhipster.github.io/common-application-properties/ +# http://www.jhipster.tech/common-application-properties/ # =================================================================== application: diff --git a/src/main/resources/config/application-prod.yml b/src/main/resources/config/application-prod.yml index 9b5472f99..99a22f792 100644 --- a/src/main/resources/config/application-prod.yml +++ b/src/main/resources/config/application-prod.yml @@ -3,8 +3,8 @@ # # This configuration overrides the application.yml file. # -# More information on profiles: https://jhipster.github.io/profiles/ -# More information on configuration properties: https://jhipster.github.io/common-application-properties/ +# More information on profiles: http://www.jhipster.tech/profiles/ +# More information on configuration properties: http://www.jhipster.tech/common-application-properties/ # =================================================================== # =================================================================== @@ -78,7 +78,7 @@ server: # =================================================================== # JHipster specific properties # -# Full reference is available at: https://jhipster.github.io/common-application-properties/ +# Full reference is available at: http://www.jhipster.tech/common-application-properties/ # =================================================================== jhipster: @@ -91,9 +91,12 @@ jhipster: time-to-live-seconds: 3600 # By default objects stay 1 hour in the cache max-entries: 1000 # Number of objects in each cache entry security: - remember-me: - # security key (this key should be unique for your application, and kept secret) - key: 5c37379956bd1242f5636c8cb322c2966ad81277 + authentication: + jwt: + secret: 07a00779cfd8d372c73b40631b62c81503e1b18e + # Token is valid 24 hours + token-validity-in-seconds: 86400 + token-validity-in-seconds-for-remember-me: 2592000 mail: # specific JHipster mail property, for standard properties see MailProperties from: jhipsterSampleApplication@localhost base-url: http://my-server-url-to-change # Modify according to your server's URL @@ -123,7 +126,7 @@ jhipster: # to have type-safe configuration, like in the JHipsterProperties above # # More documentation is available at: -# https://jhipster.github.io/common-application-properties/ +# http://www.jhipster.tech/common-application-properties/ # =================================================================== application: diff --git a/src/main/resources/config/application.yml b/src/main/resources/config/application.yml index a1a46e327..7fc4272cf 100644 --- a/src/main/resources/config/application.yml +++ b/src/main/resources/config/application.yml @@ -4,8 +4,8 @@ # This configuration will be overridden by the Spring profile you use, # for example application-dev.yml if you use the "dev" profile. # -# More information on profiles: https://jhipster.github.io/profiles/ -# More information on configuration properties: https://jhipster.github.io/common-application-properties/ +# More information on profiles: http://www.jhipster.tech/profiles/ +# More information on configuration properties: http://www.jhipster.tech/common-application-properties/ # =================================================================== # =================================================================== @@ -62,7 +62,7 @@ info: # =================================================================== # JHipster specific properties # -# Full reference is available at: https://jhipster.github.io/common-application-properties/ +# Full reference is available at: http://www.jhipster.tech/common-application-properties/ # =================================================================== jhipster: @@ -100,7 +100,7 @@ jhipster: # to have type-safe configuration, like in the JHipsterProperties above # # More documentation is available at: -# https://jhipster.github.io/common-application-properties/ +# http://www.jhipster.tech/common-application-properties/ # =================================================================== application: diff --git a/src/main/resources/config/liquibase/changelog/00000000000000_initial_schema.xml b/src/main/resources/config/liquibase/changelog/00000000000000_initial_schema.xml index e9a4ca424..ade5dfce5 100644 --- a/src/main/resources/config/liquibase/changelog/00000000000000_initial_schema.xml +++ b/src/main/resources/config/liquibase/changelog/00000000000000_initial_schema.xml @@ -75,19 +75,6 @@ - - - - - - - - - - - - - - - -

Active sessions for [{{vm.account.login}}]

- -
- Session invalidated! -
-
- An error has occured! The session could not be invalidated. -
- -
- - - - - - - - - - - - - - - - - -
IP AddressUser agentDate
{{session.ipAddress}}{{session.userAgent}}{{session.tokenDate | date:'longDate'}} - -
-
- diff --git a/src/main/webapp/app/account/sessions/sessions.state.js b/src/main/webapp/app/account/sessions/sessions.state.js deleted file mode 100644 index 19f535ed2..000000000 --- a/src/main/webapp/app/account/sessions/sessions.state.js +++ /dev/null @@ -1,33 +0,0 @@ -(function() { - 'use strict'; - - angular - .module('jhipsterSampleApplicationApp') - .config(stateConfig); - - stateConfig.$inject = ['$stateProvider']; - - function stateConfig($stateProvider) { - $stateProvider.state('sessions', { - parent: 'account', - url: '/sessions', - data: { - authorities: ['ROLE_USER'], - pageTitle: 'global.menu.account.sessions' - }, - views: { - 'content@': { - templateUrl: 'app/account/sessions/sessions.html', - controller: 'SessionsController', - controllerAs: 'vm' - } - }, - resolve: { - translatePartialLoader: ['$translate', '$translatePartialLoader', function ($translate, $translatePartialLoader) { - $translatePartialLoader.addPart('sessions'); - return $translate.refresh(); - }] - } - }); - } -})(); diff --git a/src/main/webapp/app/blocks/config/http.config.js b/src/main/webapp/app/blocks/config/http.config.js index c66ba34ce..17cf240a4 100644 --- a/src/main/webapp/app/blocks/config/http.config.js +++ b/src/main/webapp/app/blocks/config/http.config.js @@ -15,6 +15,7 @@ $httpProvider.interceptors.push('errorHandlerInterceptor'); $httpProvider.interceptors.push('authExpiredInterceptor'); + $httpProvider.interceptors.push('authInterceptor'); $httpProvider.interceptors.push('notificationInterceptor'); // jhipster-needle-angularjs-add-interceptor JHipster will add new application http interceptor here diff --git a/src/main/webapp/app/blocks/interceptor/auth-expired.interceptor.js b/src/main/webapp/app/blocks/interceptor/auth-expired.interceptor.js index 5c8409a16..c1473fdeb 100644 --- a/src/main/webapp/app/blocks/interceptor/auth-expired.interceptor.js +++ b/src/main/webapp/app/blocks/interceptor/auth-expired.interceptor.js @@ -5,9 +5,9 @@ .module('jhipsterSampleApplicationApp') .factory('authExpiredInterceptor', authExpiredInterceptor); - authExpiredInterceptor.$inject = ['$rootScope', '$q', '$injector']; + authExpiredInterceptor.$inject = ['$rootScope', '$q', '$injector', '$localStorage', '$sessionStorage']; - function authExpiredInterceptor($rootScope, $q, $injector) { + function authExpiredInterceptor($rootScope, $q, $injector, $localStorage, $sessionStorage) { var service = { responseError: responseError }; @@ -15,18 +15,14 @@ return service; function responseError(response) { - // If we have an unauthorized request we redirect to the login page - // Don't do this check on the account API to avoid infinite loop - if (response.status === 401 && angular.isDefined(response.data.path) && response.data.path.indexOf('/api/account') === -1) { - var Auth = $injector.get('Auth'); - var to = $rootScope.toState; - var params = $rootScope.toStateParams; - Auth.logout(); - if (to.name !== 'accessdenied') { - Auth.storePreviousState(to.name, params); + if (response.status === 401) { + delete $localStorage.authenticationToken; + delete $sessionStorage.authenticationToken; + var Principal = $injector.get('Principal'); + if (Principal.isAuthenticated()) { + var Auth = $injector.get('Auth'); + Auth.authorize(true); } - var LoginService = $injector.get('LoginService'); - LoginService.open(); } return $q.reject(response); } diff --git a/src/main/webapp/app/blocks/interceptor/auth.interceptor.js b/src/main/webapp/app/blocks/interceptor/auth.interceptor.js new file mode 100644 index 000000000..39a3800d5 --- /dev/null +++ b/src/main/webapp/app/blocks/interceptor/auth.interceptor.js @@ -0,0 +1,27 @@ +(function() { + 'use strict'; + + angular + .module('jhipsterSampleApplicationApp') + .factory('authInterceptor', authInterceptor); + + authInterceptor.$inject = ['$rootScope', '$q', '$location', '$localStorage', '$sessionStorage']; + + function authInterceptor ($rootScope, $q, $location, $localStorage, $sessionStorage) { + var service = { + request: request + }; + + return service; + + function request (config) { + /*jshint camelcase: false */ + config.headers = config.headers || {}; + var token = $localStorage.authenticationToken || $sessionStorage.authenticationToken; + if (token) { + config.headers.Authorization = 'Bearer ' + token; + } + return config; + } + } +})(); diff --git a/src/main/webapp/app/home/home.html b/src/main/webapp/app/home/home.html index ebb3b6508..2147c5af8 100644 --- a/src/main/webapp/app/home/home.html +++ b/src/main/webapp/app/home/home.html @@ -26,7 +26,7 @@

Welcome, Java Hipster!