From d88db9ecc966653bdef41b7be9f34388cad65e3d Mon Sep 17 00:00:00 2001 From: Marcelo Boveto Shima Date: Fri, 25 Jun 2021 17:54:49 -0300 Subject: [PATCH 1/7] Fix angular calling /api/account twice at login. --- .../app/core/auth/account.service.spec.ts.ejs | 9 +++++++++ .../app/core/auth/account.service.ts.ejs | 18 +++++++++--------- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/generators/client/templates/angular/src/main/webapp/app/core/auth/account.service.spec.ts.ejs b/generators/client/templates/angular/src/main/webapp/app/core/auth/account.service.spec.ts.ejs index 1b942ad9cbd..933cfb4f474 100644 --- a/generators/client/templates/angular/src/main/webapp/app/core/auth/account.service.spec.ts.ejs +++ b/generators/client/templates/angular/src/main/webapp/app/core/auth/account.service.spec.ts.ejs @@ -165,6 +165,15 @@ describe('Service Tests', () => { }); describe('identity', () => { + it('should call /account only once if last call have not returned', () => { + // When I call + service.identity().subscribe(); + // Once more + service.identity().subscribe(); + // Then there is only request + httpMock.expectOne({ method: 'GET' }); + }); + it('should call /account only once if not logged out after first authentication and should call /account again if user has logged out', () => { // Given the user is authenticated service.identity().subscribe(); diff --git a/generators/client/templates/angular/src/main/webapp/app/core/auth/account.service.ts.ejs b/generators/client/templates/angular/src/main/webapp/app/core/auth/account.service.ts.ejs index 70f041e7821..8a9780a9deb 100644 --- a/generators/client/templates/angular/src/main/webapp/app/core/auth/account.service.ts.ejs +++ b/generators/client/templates/angular/src/main/webapp/app/core/auth/account.service.ts.ejs @@ -37,7 +37,7 @@ import { TrackerService } from '../tracker/tracker.service'; export class AccountService { private userIdentity: Account | null = null; private authenticationState = new ReplaySubject(1); - private accountCache$?: Observable; + private accountCache$?: Observable | null; constructor( <%_ if (enableTranslation) { _%> @@ -62,6 +62,9 @@ export class AccountService { authenticate(identity: Account | null): void { this.userIdentity = identity; this.authenticationState.next(this.userIdentity); + if (!identity) { + this.accountCache$ = null; + } <%_ if (communicationSpringWebsocket) { _%> if (identity) { this.trackerService.connect(); @@ -82,29 +85,26 @@ export class AccountService { } identity(force?: boolean): Observable { - if (!this.accountCache$ || force || !this.isAuthenticated()) { + if (!this.accountCache$ || force) { this.accountCache$ = this.fetch().pipe( - catchError(() => of(null)), - tap((account: Account | null) => { + tap((account: Account) => { this.authenticate(account); <%_ if (enableTranslation) { _%> // After retrieve the account info, the language will be changed to // the user's preferred language configured in the account setting // unless user have choosed other language in the current session - if (!this.sessionStorageService.retrieve('locale') && account) { + if (!this.sessionStorageService.retrieve('locale')) { this.translateService.use(account.langKey); } <%_ } _%> - if (account) { - this.navigateToStoredUrl(); - } + this.navigateToStoredUrl(); }), shareReplay() ); } - return this.accountCache$; + return this.accountCache$.pipe(catchError(() => of(null))); } isAuthenticated(): boolean { From edaa1c729dc779fd621f11cc0d5a7f9be1353ecb Mon Sep 17 00:00:00 2001 From: Marcelo Boveto Shima Date: Sat, 26 Jun 2021 01:02:05 -0300 Subject: [PATCH 2/7] Add reactive option to cli. --- generators/app/index.js | 5 +++++ generators/generator-base.js | 4 ++++ 2 files changed, 9 insertions(+) diff --git a/generators/app/index.js b/generators/app/index.js index 8bd3ff0b444..439ce5f3dc1 100644 --- a/generators/app/index.js +++ b/generators/app/index.js @@ -260,6 +260,11 @@ module.exports = class JHipsterAppGenerator extends BaseBlueprintGenerator { type: Boolean, }); + this.option('reactive', { + desc: 'Generate a reactive backend', + type: Boolean, + }); + // Just constructing help, stop here if (this.options.help) { return; diff --git a/generators/generator-base.js b/generators/generator-base.js index 5b2a4df484b..71ca922facf 100644 --- a/generators/generator-base.js +++ b/generators/generator-base.js @@ -2422,6 +2422,10 @@ templates: ${JSON.stringify(existingTemplates, null, 2)}`; this.jhipsterConfig.microfrontend = options.microfrontend; } + if (options.reactive !== undefined) { + this.jhipsterConfig.reactive = options.reactive; + } + if (options.clientPackageManager) { this.jhipsterConfig.clientPackageManager = options.clientPackageManager; } From f241ee35e0a7887f6d3e043647cd8da4ff5638d5 Mon Sep 17 00:00:00 2001 From: Marcelo Boveto Shima Date: Mon, 21 Jun 2021 18:37:07 -0300 Subject: [PATCH 3/7] Implement oauth2 refresh tokens filter. --- generators/server/files.js | 29 +++++ generators/server/index.js | 2 + .../config/OAuth2Configuration.java.ejs | 25 +++++ .../config/OAuth2SsoConfiguration.java.ejs | 94 ---------------- .../OAuth2TokenServicesConfiguration.java.ejs | 68 ------------ .../package/config/WebConfigurer.java.ejs | 8 +- .../java/package/service/UserService.java.ejs | 15 ++- ...th2ReactiveRefreshTokensWebFilter.java.ejs | 70 ++++++++++++ .../OAuth2RefreshTokensWebFilter.java.ejs | 72 ++++++++++++ .../config/TestSecurityConfiguration.java.ejs | 54 +++++---- .../package/test/util/OAuth2TestUtil.java.ejs | 104 ++++++++++++++++++ .../rest/AccountResourceIT_oauth2.java.ejs | 101 ++++++++--------- .../web/rest/LogoutResourceIT.java.ejs | 48 ++++---- .../java/package/web/rest/TestUtil.java.ejs | 27 ----- .../package/web/rest/UserResourceIT.java.ejs | 14 ++- 15 files changed, 432 insertions(+), 299 deletions(-) create mode 100644 generators/server/templates/src/main/java/package/config/OAuth2Configuration.java.ejs delete mode 100644 generators/server/templates/src/main/java/package/config/OAuth2SsoConfiguration.java.ejs delete mode 100644 generators/server/templates/src/main/java/package/config/OAuth2TokenServicesConfiguration.java.ejs create mode 100644 generators/server/templates/src/main/java/package/web/filter/OAuth2ReactiveRefreshTokensWebFilter.java.ejs create mode 100644 generators/server/templates/src/main/java/package/web/filter/OAuth2RefreshTokensWebFilter.java.ejs create mode 100644 generators/server/templates/src/test/java/package/test/util/OAuth2TestUtil.java.ejs diff --git a/generators/server/files.js b/generators/server/files.js index 88f2b659d49..b61aab9c2c5 100644 --- a/generators/server/files.js +++ b/generators/server/files.js @@ -1165,6 +1165,35 @@ const serverFiles = { templates: ['META-INF/services/reactor.blockhound.integration.BlockHoundIntegration'], }, ], + springBootOauth2: [ + { + condition: generator => generator.authenticationType === 'oauth2' && generator.applicationType === 'monolith', + path: SERVER_MAIN_SRC_DIR, + templates: [ + { + file: 'package/config/OAuth2Configuration.java', + renameTo: generator => `${generator.javaDir}config/OAuth2Configuration.java`, + }, + ], + }, + { + condition: generator => generator.authenticationType === 'oauth2' && generator.applicationType !== 'microservice', + path: SERVER_MAIN_SRC_DIR, + templates: [ + { + file: generator => `package/web/filter/OAuth2${generator.reactive ? 'Reactive' : ''}RefreshTokensWebFilter.java`, + renameTo: generator => `${generator.javaDir}web/filter/OAuth2${generator.reactive ? 'Reactive' : ''}RefreshTokensWebFilter.java`, + }, + ], + }, + { + condition: generator => generator.authenticationType === 'oauth2' && generator.applicationType !== 'microservice', + path: SERVER_TEST_SRC_DIR, + templates: [ + { file: 'package/test/util/OAuth2TestUtil.java', renameTo: generator => `${generator.testDir}test/util/OAuth2TestUtil.java` }, + ], + }, + ], serverTestFw: [ { condition: generator => generator.databaseType === 'cassandra', diff --git a/generators/server/index.js b/generators/server/index.js index e8528c24017..97a756f018b 100644 --- a/generators/server/index.js +++ b/generators/server/index.js @@ -251,6 +251,7 @@ module.exports = class JHipsterServerGenerator extends BaseBlueprintGenerator { return { loadSharedConfig() { this.loadAppConfig(); + this.loadDerivedAppConfig(); this.loadClientConfig(); this.loadServerConfig(); this.loadTranslationConfig(); @@ -461,6 +462,7 @@ module.exports = class JHipsterServerGenerator extends BaseBlueprintGenerator { 'java:docker': './mvnw -ntp verify -DskipTests jib:dockerBuild', 'backend:unit:test': `./mvnw -ntp -P-webapp verify --batch-mode ${javaCommonLog} ${javaTestLog}`, 'backend:build-cache': './mvnw dependency:go-offline', + 'backend:debug': './mvnw -Dspring-boot.run.jvmArguments="-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:8000"', }); } else if (buildTool === 'gradle') { const excludeWebapp = this.jhipsterConfig.skipClient ? '' : '-x webapp'; diff --git a/generators/server/templates/src/main/java/package/config/OAuth2Configuration.java.ejs b/generators/server/templates/src/main/java/package/config/OAuth2Configuration.java.ejs new file mode 100644 index 00000000000..601d91556e5 --- /dev/null +++ b/generators/server/templates/src/main/java/package/config/OAuth2Configuration.java.ejs @@ -0,0 +1,25 @@ +<%_ const reactivePrefix = reactive ? 'Reactive' : '' %> +package <%= packageName %>.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.oauth2.client.<%= reactivePrefix %>OAuth2AuthorizedClientManager; +import org.springframework.security.oauth2.client.registration.<%= reactivePrefix %>ClientRegistrationRepository; +import org.springframework.security.oauth2.client.web.Default<%= reactivePrefix %>OAuth2AuthorizedClientManager; +import org.springframework.security.oauth2.client.web.<%= reactive ? 'server.Server' : '' %>OAuth2AuthorizedClientRepository; + +@Configuration +public class OAuth2Configuration { + + @Bean + public <%= reactivePrefix %>OAuth2AuthorizedClientManager authorizedClientManager( + <%= reactivePrefix %>ClientRegistrationRepository clientRegistrationRepository, + <%= reactive ? 'Server' : '' %>OAuth2AuthorizedClientRepository authorizedClientRepository + ) { + <%= reactivePrefix %>OAuth2AuthorizedClientManager authorizedClientManager = new Default<%= reactivePrefix %>OAuth2AuthorizedClientManager( + clientRegistrationRepository, + authorizedClientRepository + ); + return authorizedClientManager; + } +} diff --git a/generators/server/templates/src/main/java/package/config/OAuth2SsoConfiguration.java.ejs b/generators/server/templates/src/main/java/package/config/OAuth2SsoConfiguration.java.ejs deleted file mode 100644 index 8a8f2b4b233..00000000000 --- a/generators/server/templates/src/main/java/package/config/OAuth2SsoConfiguration.java.ejs +++ /dev/null @@ -1,94 +0,0 @@ -<%# - Copyright 2013-2021 the original author or authors from the JHipster project. - - This file is part of the JHipster project, see https://www.jhipster.tech/ - for more information. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - https://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. --%> -package <%= packageName %>.config; - -import <%= packageName %>.security.AuthoritiesConstants; - -import org.springframework.boot.autoconfigure.security.oauth2.client.EnableOAuth2Sso; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.http.HttpMethod; -import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; -import org.springframework.security.config.annotation.web.builders.HttpSecurity; -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.oauth2.client.OAuth2ClientContext; -import org.springframework.security.oauth2.client.OAuth2RestTemplate; -import org.springframework.security.oauth2.client.resource.OAuth2ProtectedResourceDetails; -import org.springframework.security.web.csrf.CookieCsrfTokenRepository; -import org.springframework.security.web.csrf.CsrfFilter; -import org.springframework.web.filter.CorsFilter; - -@EnableOAuth2Sso -@EnableWebSecurity -@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true) -@Configuration -public class OAuth2SsoConfiguration extends WebSecurityConfigurerAdapter { - - private final CorsFilter corsFilter; - - public OAuth2SsoConfiguration(CorsFilter corsFilter) { - this.corsFilter = corsFilter; - } - - @Override - public void configure(WebSecurity web) throws Exception { - web.ignoring() - .antMatchers(HttpMethod.OPTIONS, "/**") - .antMatchers("/app/**/*.{js,html}") - .antMatchers("/i18n/**") - .antMatchers("/content/**") - .antMatchers("/swagger-ui/index.html") - .antMatchers("/test/**")<% if (devDatabaseType !== 'h2Disk' && devDatabaseType !== 'h2Memory') { %>;<% } else { %> - .antMatchers("/h2-console/**");<% } %> - } - - @Override - protected void configure(HttpSecurity http) throws Exception { - http - .csrf() - .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) - .and() - .addFilterBefore(corsFilter, CsrfFilter.class) - .headers() - .frameOptions() - .disable() - .and() - .authorizeRequests() - .antMatchers("/api/**").authenticated() - .antMatchers("/management/health").permitAll() - .antMatchers("/management/health/**").permitAll() - .antMatchers("/management/**").hasAuthority(AuthoritiesConstants.ADMIN) - .anyRequest().permitAll(); - } - - /** - * This {@link OAuth2RestTemplate} is used by {@link org.springframework.cloud.security.oauth2.proxy.OAuth2TokenRelayFilter} - * from Spring Cloud Security to refresh the access token when needed. - * @param oAuth2ProtectedResourceDetails the resource details. - * @param oAuth2ClientContext the client context. - * @return the {@link OAuth2RestTemplate}. - */ - @Bean - public OAuth2RestTemplate oAuth2RestTemplate(OAuth2ProtectedResourceDetails oAuth2ProtectedResourceDetails, - OAuth2ClientContext oAuth2ClientContext) { - return new OAuth2RestTemplate(oAuth2ProtectedResourceDetails, oAuth2ClientContext); - } -} diff --git a/generators/server/templates/src/main/java/package/config/OAuth2TokenServicesConfiguration.java.ejs b/generators/server/templates/src/main/java/package/config/OAuth2TokenServicesConfiguration.java.ejs deleted file mode 100644 index aba815a4f58..00000000000 --- a/generators/server/templates/src/main/java/package/config/OAuth2TokenServicesConfiguration.java.ejs +++ /dev/null @@ -1,68 +0,0 @@ -<%# - Copyright 2013-2021 the original author or authors from the JHipster project. - - This file is part of the JHipster project, see https://www.jhipster.tech/ - for more information. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - https://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. --%> -package <%= packageName %>.config; - -import org.springframework.boot.autoconfigure.security.oauth2.resource.AuthoritiesExtractor; -import org.springframework.boot.autoconfigure.security.oauth2.resource.PrincipalExtractor; -<%_ if (applicationType === 'microservice' && cacheProvider !== 'no') { _%> -import org.springframework.boot.autoconfigure.security.oauth2.resource.ResourceServerProperties; -import org.springframework.boot.autoconfigure.security.oauth2.resource.UserInfoTokenServices; -<%_ } _%> -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -<%_ if (applicationType === 'microservice' && cacheProvider !== 'no') { _%> -import <%= packageName %>.security.oauth2.CachedUserInfoTokenServices; -<%_ } _%> -import <%= packageName %>.security.oauth2.SimpleAuthoritiesExtractor; -import <%= packageName %>.security.oauth2.SimplePrincipalExtractor; - -@Configuration -public class OAuth2TokenServicesConfiguration { - - private static final String OAUTH2_PRINCIPAL_ATTRIBUTE = "preferred_username"; - private static final String OAUTH2_AUTHORITIES_ATTRIBUTE = "roles"; - -<%_ if (applicationType === 'microservice' && cacheProvider !== 'no') { _%> - private final ResourceServerProperties resourceServerProperties; - - public OAuth2TokenServicesConfiguration(ResourceServerProperties resourceServerProperties) { - this.resourceServerProperties = resourceServerProperties; - } - - @Bean - public UserInfoTokenServices userInfoTokenServices(PrincipalExtractor principalExtractor, AuthoritiesExtractor authoritiesExtractor) { - UserInfoTokenServices userInfoTokenServices = - new CachedUserInfoTokenServices(resourceServerProperties.getUserInfoUri(), resourceServerProperties.getClientId()); - - userInfoTokenServices.setPrincipalExtractor(principalExtractor); - userInfoTokenServices.setAuthoritiesExtractor(authoritiesExtractor); - return userInfoTokenServices; - } -<%_ } _%> - @Bean - public PrincipalExtractor principalExtractor() { - return new SimplePrincipalExtractor(OAUTH2_PRINCIPAL_ATTRIBUTE); - } - - @Bean - public AuthoritiesExtractor authoritiesExtractor() { - return new SimpleAuthoritiesExtractor(OAUTH2_AUTHORITIES_ATTRIBUTE); - } -} diff --git a/generators/server/templates/src/main/java/package/config/WebConfigurer.java.ejs b/generators/server/templates/src/main/java/package/config/WebConfigurer.java.ejs index 269da9597fe..07824a7d9c6 100644 --- a/generators/server/templates/src/main/java/package/config/WebConfigurer.java.ejs +++ b/generators/server/templates/src/main/java/package/config/WebConfigurer.java.ejs @@ -111,7 +111,13 @@ public class WebConfigurer implements <% if (!reactive) { %>ServletContextInitia this.jHipsterProperties = jHipsterProperties; <%_ if (devDatabaseTypeH2Any && reactive) { _%> if (env.acceptsProfiles(Profiles.of(JHipsterConstants.SPRING_PROFILE_DEVELOPMENT))) { - H2ConfigurationHelper.initH2Console(); + <%_ if (!applicationTypeMonolith) { _%> + try { + <%_ } _%> + H2ConfigurationHelper.initH2Console(); + <%_ if (!applicationTypeMonolith) { _%> + } catch (Exception e) {}; + <%_ } _%> } <%_ } _%> } diff --git a/generators/server/templates/src/main/java/package/service/UserService.java.ejs b/generators/server/templates/src/main/java/package/service/UserService.java.ejs index 24bb5234b63..ebdb23f266c 100644 --- a/generators/server/templates/src/main/java/package/service/UserService.java.ejs +++ b/generators/server/templates/src/main/java/package/service/UserService.java.ejs @@ -74,6 +74,7 @@ import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; <%_ if (databaseType === 'sql') { _%> import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.annotation.Propagation; <%_ } _%> <%_ if (reactive) { _%> import reactor.core.publisher.Flux; @@ -878,6 +879,9 @@ public class UserService { <%_ } _%> <%_ if (authenticationType === 'oauth2') { _%> + <%_ if (databaseType === 'sql') { _%> + @Transactional(propagation = Propagation.REQUIRES_NEW) + <%_ } _%> private <% if (reactive) { %>Mono<<%= asEntity('User') %>><% } else { %><%= asEntity('User') %><% } %> syncUserWithIdP(Map details, <%= asEntity('User') %> user) { // save authorities in to sync user roles/groups between IdP and JHipster's local database <%_ if (!reactive) { _%> @@ -999,7 +1003,16 @@ public class UserService { }) <%_ } _%> .collect(Collectors.toSet())); - return <% if (databaseType !== 'no' && !reactive) { %>new <%= asDto('AdminUser') %>(syncUserWithIdP(attributes, user))<% } else if (!reactive) { %>user<% } %><% if (databaseType === 'no' && reactive) { %>Mono.just(user)<% } else if (reactive) { %>syncUserWithIdP(attributes, user).flatMap(u -> Mono.just(new <%= asDto('AdminUser') %>(u)))<% } %>; + + <%_ if (databaseType === 'no') { _%> + return <% if (reactive) { %>Mono.just(user)<% } else { %>user<% } %>; + <%_ } else { _%> + <%_ if (!reactive) { _%> + return new <%= asDto('AdminUser') %>(syncUserWithIdP(attributes, user)); + <%_ } else { _%> + return syncUserWithIdP(attributes, user).flatMap(u -> Mono.just(new <%= asDto('AdminUser') %>(u))); + <%_ } _%> + <%_ } _%> } private static <%= databaseType === 'no' ? asDto('AdminUser') : asEntity('User') %> getUser(Map details) { diff --git a/generators/server/templates/src/main/java/package/web/filter/OAuth2ReactiveRefreshTokensWebFilter.java.ejs b/generators/server/templates/src/main/java/package/web/filter/OAuth2ReactiveRefreshTokensWebFilter.java.ejs new file mode 100644 index 00000000000..4ba2c59c522 --- /dev/null +++ b/generators/server/templates/src/main/java/package/web/filter/OAuth2ReactiveRefreshTokensWebFilter.java.ejs @@ -0,0 +1,70 @@ +<%# + Copyright 2013-2021 the original author or authors from the JHipster project. + + This file is part of the JHipster project, see https://www.jhipster.tech/ + for more information. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +-%> +package <%= packageName %>.web.filter; + +import org.springframework.security.oauth2.client.OAuth2AuthorizeRequest; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; +import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientManager; +import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; +import org.springframework.stereotype.Component; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.WebFilter; +import org.springframework.web.server.WebFilterChain; +import reactor.core.publisher.Mono; + +/** + * Refresh oauth2 tokens based on TokenRelayGatewayFilterFactory. + */ +@Component +public class OAuth2ReactiveRefreshTokensWebFilter implements WebFilter { + + private final ReactiveOAuth2AuthorizedClientManager clientManager; + + public OAuth2ReactiveRefreshTokensWebFilter(ReactiveOAuth2AuthorizedClientManager clientManager) { + this.clientManager = clientManager; + } + + public Mono filter(ServerWebExchange exchange, WebFilterChain chain) { + return exchange + .getPrincipal() + .filter(principal -> principal instanceof OAuth2AuthenticationToken) + .cast(OAuth2AuthenticationToken.class) + .flatMap(authentication -> authorizedClient(exchange, authentication)) + .thenReturn(exchange) + .flatMap(chain::filter); + } + + private Mono authorizedClient(ServerWebExchange exchange, OAuth2AuthenticationToken oauth2Authentication) { + String clientRegistrationId = oauth2Authentication.getAuthorizedClientRegistrationId(); + OAuth2AuthorizeRequest request = OAuth2AuthorizeRequest + .withClientRegistrationId(clientRegistrationId) + .principal(oauth2Authentication) + .attribute(ServerWebExchange.class.getName(), exchange) + .build(); + if (clientManager == null) { + return Mono.error( + new IllegalStateException( + "No ReactiveOAuth2AuthorizedClientManager bean was found. Did you include the " + + "org.springframework.boot:spring-boot-starter-oauth2-client dependency?" + ) + ); + } + return clientManager.authorize(request); + } +} diff --git a/generators/server/templates/src/main/java/package/web/filter/OAuth2RefreshTokensWebFilter.java.ejs b/generators/server/templates/src/main/java/package/web/filter/OAuth2RefreshTokensWebFilter.java.ejs new file mode 100644 index 00000000000..71dacfece8f --- /dev/null +++ b/generators/server/templates/src/main/java/package/web/filter/OAuth2RefreshTokensWebFilter.java.ejs @@ -0,0 +1,72 @@ +<%# + Copyright 2013-2021 the original author or authors from the JHipster project. + + This file is part of the JHipster project, see https://www.jhipster.tech/ + for more information. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +-%> +package <%= packageName %>.web.filter; + +import java.io.IOException; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.client.OAuth2AuthorizeRequest; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager; +import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +/** + * Refresh oauth2 tokens. + */ +@Component +public class OAuth2RefreshTokensWebFilter extends OncePerRequestFilter { + + private final OAuth2AuthorizedClientManager clientManager; + + public OAuth2RefreshTokensWebFilter(OAuth2AuthorizedClientManager clientManager) { + this.clientManager = clientManager; + } + + @Override + public void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws IOException, ServletException { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if ((authentication instanceof OAuth2AuthenticationToken)) { + authorizedClient((OAuth2AuthenticationToken) authentication); + } + + filterChain.doFilter(request, response); + } + + private OAuth2AuthorizedClient authorizedClient(OAuth2AuthenticationToken oauth2Authentication) { + String clientRegistrationId = oauth2Authentication.getAuthorizedClientRegistrationId(); + OAuth2AuthorizeRequest request = OAuth2AuthorizeRequest + .withClientRegistrationId(clientRegistrationId) + .principal(oauth2Authentication) + .build(); + if (clientManager == null) { + throw new IllegalStateException( + "No OAuth2AuthorizedClientManager bean was found. Did you include the " + + "org.springframework.boot:spring-boot-starter-oauth2-client dependency?" + ); + } + return clientManager.authorize(request); + } +} diff --git a/generators/server/templates/src/test/java/package/config/TestSecurityConfiguration.java.ejs b/generators/server/templates/src/test/java/package/config/TestSecurityConfiguration.java.ejs index 4a20ac4ff31..3377c4fa662 100644 --- a/generators/server/templates/src/test/java/package/config/TestSecurityConfiguration.java.ejs +++ b/generators/server/templates/src/test/java/package/config/TestSecurityConfiguration.java.ejs @@ -16,44 +16,47 @@ See the License for the specific language governing permissions and limitations under the License. -%> +<%_ const reactivePrefix = reactive ? 'Reactive' : ''; _%> package <%= packageName %>.config; +import static org.mockito.Mockito.mock; + +import java.util.HashMap; +import java.util.Map; import org.springframework.boot.test.context.TestConfiguration; import org.springframework.context.annotation.Bean; -import org.springframework.security.oauth2.client.InMemory<% if (reactive) { %>Reactive<% } %>OAuth2AuthorizedClientService; -import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; -import org.springframework.security.oauth2.client.<% if (reactive) { %>Reactive<% } %>OAuth2AuthorizedClientService; +import org.springframework.security.oauth2.client.InMemory<%= reactivePrefix %>OAuth2AuthorizedClientService; +import org.springframework.security.oauth2.client.<%= reactivePrefix %>OAuth2AuthorizedClientService; import org.springframework.security.oauth2.client.registration.ClientRegistration; -import org.springframework.security.oauth2.client.registration.<% if (reactive) { %>Reactive<% } %>ClientRegistrationRepository; -import org.springframework.security.oauth2.client.registration.InMemory<% if (reactive) { %>Reactive<% } %>ClientRegistrationRepository; -import org.springframework.security.oauth2.client.web.AuthenticatedPrincipalOAuth2AuthorizedClientRepository; -import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository; +import org.springframework.security.oauth2.client.registration.<%= reactivePrefix %>ClientRegistrationRepository; +import org.springframework.security.oauth2.client.registration.InMemory<%= reactivePrefix %>ClientRegistrationRepository; import org.springframework.security.oauth2.core.AuthorizationGrantType; import org.springframework.security.oauth2.core.ClientAuthenticationMethod; -import org.springframework.security.oauth2.jwt.<% if (reactive) { %>Reactive<% } %>JwtDecoder; - -import java.util.HashMap; -import java.util.Map; - -import static org.mockito.Mockito.mock; +import org.springframework.security.oauth2.jwt.<%= reactivePrefix %>JwtDecoder; +<%_ if (applicationTypeMonolith) { _%> +import org.springframework.context.annotation.Import; +<%_ } _%> /** * This class allows you to run unit and integration tests without an IdP. */ @TestConfiguration +<%_ if (applicationTypeMonolith) { _%> +@Import(OAuth2Configuration.class) +<%_ } _%> public class TestSecurityConfiguration { - private final ClientRegistration clientRegistration; - public TestSecurityConfiguration() { - this.clientRegistration = clientRegistration().build(); + @Bean + ClientRegistration clientRegistration() { + return clientRegistrationBuilder().build(); } @Bean - <% if (reactive) { %>Reactive<% } %>ClientRegistrationRepository clientRegistrationRepository() { - return new InMemory<% if (reactive) { %>Reactive<% } %>ClientRegistrationRepository(clientRegistration); + <%= reactivePrefix %>ClientRegistrationRepository clientRegistrationRepository(ClientRegistration clientRegistration) { + return new InMemory<%= reactivePrefix %>ClientRegistrationRepository(clientRegistration); } - private ClientRegistration.Builder clientRegistration() { + private ClientRegistration.Builder clientRegistrationBuilder() { Map metadata = new HashMap<>(); metadata.put("end_session_endpoint", "https://jhipster.org/logout"); @@ -74,17 +77,12 @@ public class TestSecurityConfiguration { } @Bean - <% if (reactive) { %>Reactive<% } %>JwtDecoder jwtDecoder() { - return mock(<% if (reactive) { %>Reactive<% } %>JwtDecoder.class); - } - - @Bean - public <% if (reactive) { %>Reactive<% } %>OAuth2AuthorizedClientService authorizedClientService(<% if (reactive) { %>Reactive<% } %>ClientRegistrationRepository clientRegistrationRepository) { - return new InMemory<% if (reactive) { %>Reactive<% } %>OAuth2AuthorizedClientService(clientRegistrationRepository); + <%= reactivePrefix %>JwtDecoder jwtDecoder() { + return mock(<%= reactivePrefix %>JwtDecoder.class); } @Bean - public OAuth2AuthorizedClientRepository authorizedClientRepository(OAuth2AuthorizedClientService authorizedClientService) { - return new AuthenticatedPrincipalOAuth2AuthorizedClientRepository(authorizedClientService); + <%= reactivePrefix %>OAuth2AuthorizedClientService authorizedClientService(<%= reactivePrefix %>ClientRegistrationRepository clientRegistrationRepository) { + return new InMemory<%= reactivePrefix %>OAuth2AuthorizedClientService(clientRegistrationRepository); } } diff --git a/generators/server/templates/src/test/java/package/test/util/OAuth2TestUtil.java.ejs b/generators/server/templates/src/test/java/package/test/util/OAuth2TestUtil.java.ejs new file mode 100644 index 00000000000..f5799655bdb --- /dev/null +++ b/generators/server/templates/src/test/java/package/test/util/OAuth2TestUtil.java.ejs @@ -0,0 +1,104 @@ +<%# + Copyright 2013-2021 the original author or authors from the JHipster project. + + This file is part of the JHipster project, see https://www.jhipster.tech/ + for more information. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +-%> +<%_ const reactivePrefix = reactive ? 'Reactive' : ''; _%> +package <%= packageName %>.test.util; + +import <%= packageName %>.security.AuthoritiesConstants; +import <%= packageName %>.security.SecurityUtils; + +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; +import org.springframework.security.oauth2.client.<%= reactivePrefix %>OAuth2AuthorizedClientService; +import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.core.OAuth2AccessToken; +import org.springframework.security.oauth2.core.OAuth2AccessToken.TokenType; +import org.springframework.security.oauth2.core.oidc.OidcIdToken; +import org.springframework.security.oauth2.core.oidc.OidcUserInfo; +import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser; + +public class OAuth2TestUtil { + public static final String TEST_USER_LOGIN = "test"; + + public static final String ID_TOKEN = + "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9" + + ".eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsIm" + + "p0aSI6ImQzNWRmMTRkLTA5ZjYtNDhmZi04YTkzLTdjNmYwMzM5MzE1OSIsImlhdCI6MTU0M" + + "Tk3MTU4MywiZXhwIjoxNTQxOTc1MTgzfQ.QaQOarmV8xEUYV7yvWzX3cUE_4W1luMcWCwpr" + + "oqqUrg"; + + public static OAuth2AuthenticationToken testAuthenticationToken() { + Map claims = new HashMap<>(); + claims.put("sub", TEST_USER_LOGIN); + claims.put("preferred_username", TEST_USER_LOGIN); + claims.put("email", "john.doe@jhipster.com"); + claims.put("roles", Collections.singletonList(AuthoritiesConstants.ADMIN)); + + return authenticationToken(claims); + } + + public static OAuth2AuthenticationToken authenticationToken(Map claims) { + Instant issuedAt = Instant.now(); + Instant expiresAt = Instant.now().plus(1, ChronoUnit.DAYS); + if (!claims.containsKey("sub")) { + claims.put("sub", "jane"); + } + if (!claims.containsKey("preferred_username")) { + claims.put("preferred_username", "jane"); + } + if (!claims.containsKey("email")) { + claims.put("email", "jane.doe@jhipster.com"); + } + if (claims.containsKey("auth_time")) { + issuedAt = (Instant) claims.get("auth_time"); + } else { + claims.put("auth_time", issuedAt); + } + if (claims.containsKey("exp")) { + expiresAt = (Instant) claims.get("exp"); + } else { + claims.put("exp", expiresAt); + } + Collection authorities = SecurityUtils.extractAuthorityFromClaims(claims); + OidcIdToken token = new OidcIdToken(ID_TOKEN, issuedAt, expiresAt, claims); + OidcUserInfo userInfo = new OidcUserInfo(claims); + DefaultOidcUser user = new DefaultOidcUser(authorities, token, userInfo, "preferred_username"); + return new OAuth2AuthenticationToken(user, user.getAuthorities(), "oidc"); + } + + public static OAuth2AuthenticationToken registerAuthenticationToken(<%= reactivePrefix %>OAuth2AuthorizedClientService authorizedClientService, ClientRegistration clientRegistration, OAuth2AuthenticationToken authentication) { + Map userDetails = authentication.getPrincipal().getAttributes(); + + OAuth2AccessToken token = new OAuth2AccessToken(TokenType.BEARER, "Token", (Instant)userDetails.get("auth_time"), (Instant)userDetails.get("exp")); + + authorizedClientService.saveAuthorizedClient( + new OAuth2AuthorizedClient(clientRegistration, authentication.getName(), token), + authentication + )<% if (reactive) { %>.block()<% } %>; + + return authentication; + } +} diff --git a/generators/server/templates/src/test/java/package/web/rest/AccountResourceIT_oauth2.java.ejs b/generators/server/templates/src/test/java/package/web/rest/AccountResourceIT_oauth2.java.ejs index b7097662000..2483fd0546a 100644 --- a/generators/server/templates/src/test/java/package/web/rest/AccountResourceIT_oauth2.java.ejs +++ b/generators/server/templates/src/test/java/package/web/rest/AccountResourceIT_oauth2.java.ejs @@ -18,62 +18,53 @@ -%> package <%= packageName %>.web.rest; +import static <%= packageName %>.test.util.OAuth2TestUtil.TEST_USER_LOGIN; +import static <%= packageName %>.test.util.OAuth2TestUtil.registerAuthenticationToken; +<%_ if (!reactive) { _%> +import static <%= packageName %>.test.util.OAuth2TestUtil.testAuthenticationToken; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; +<%_ } else { _%> +import static <%= packageName %>.test.util.OAuth2TestUtil.authenticationToken; +import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.*; + <%_ if (searchEngine === 'elasticsearch') { _%> +import static org.mockito.Mockito.*; + <%_ } _%> +<%_ } _%> + <%_ if (databaseType === 'cassandra') { _%> import <%= packageName %>.AbstractCassandraTest; <%_ } _%> import <%= packageName %>.IntegrationTest; -import <%= packageName %>.config.TestSecurityConfiguration; import <%= packageName %>.security.AuthoritiesConstants; -import <%= packageName %>.service.UserService; -<%_ if (reactive) { _%> -import org.junit.jupiter.api.BeforeEach; -<%_ } _%> import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -<%_ if (!reactive) { _%> -import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; -<%_ } else { _%> -import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient; -<%_ } _%> import org.springframework.http.MediaType; +import org.springframework.security.test.context.support.WithMockUser; <%_ if (!reactive) { _%> -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.authority.SimpleGrantedAuthority; -import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; -import org.springframework.security.oauth2.core.user.DefaultOAuth2User; -import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; +import org.springframework.security.oauth2.client.registration.ClientRegistration; import org.springframework.security.test.context.TestSecurityContextHolder; -import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.web.servlet.MockMvc; <%_ if (databaseType === 'sql' && !reactive) { _%> import org.springframework.transaction.annotation.Transactional; <%_ } _%> -import java.util.Collection; <%_ } else { _%> -import org.springframework.security.oauth2.core.oidc.OidcIdToken; +import org.junit.jupiter.api.BeforeEach; +import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient; +import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientService; +import org.springframework.security.oauth2.client.registration.ClientRegistration; import org.springframework.test.web.reactive.server.WebTestClient; - <%_ if (searchEngine === 'elasticsearch' && reactive) { _%> + <%_ if (searchEngine === 'elasticsearch') { _%> import reactor.core.publisher.Mono; <%_ } _%> -import java.time.Instant; -<%_ } _%> import java.util.Collections; import java.util.HashMap; import java.util.Map; - -<%_ if (reactive && searchEngine === 'elasticsearch') { _%> -import static org.mockito.Mockito.*; -<%_ } _%> -import static <%= packageName %>.web.rest.AccountResourceIT.TEST_USER_LOGIN; -import org.springframework.security.test.context.support.WithMockUser; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; -<%_ if (reactive) { _%> -import static <%= packageName %>.web.rest.TestUtil.ID_TOKEN; -import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.*; <%_ } _%> /** @@ -87,42 +78,46 @@ import static org.springframework.security.test.web.reactive.server.SecurityMock @WithMockUser(value = TEST_USER_LOGIN) @IntegrationTest class AccountResourceIT <% if (databaseType === 'cassandra') { %>extends AbstractCassandraTest <% } %>{ +<%_ if (!reactive) { _%> + @Autowired + private MockMvc restAccountMockMvc; - static final String TEST_USER_LOGIN = "test"; + @Autowired + OAuth2AuthorizedClientService authorizedClientService; - <%_ if (!reactive) { _%> @Autowired - private MockMvc restAccountMockMvc; + ClientRegistration clientRegistration; <%_ } else { _%> - private OidcIdToken idToken; + + private Map claims; @Autowired private WebTestClient webTestClient; - <%_ } _%> - <%_ if (reactive) { _%> + @Autowired + private ReactiveOAuth2AuthorizedClientService authorizedClientService; + + @Autowired + private ClientRegistration clientRegistration; +<%_ } _%> + +<%_ if (reactive) { _%> @BeforeEach public void setup() { - Map claims = new HashMap<>(); + claims = new HashMap<>(); claims.put("groups", Collections.singletonList(AuthoritiesConstants.ADMIN)); claims.put("sub", "jane"); claims.put("email", "jane.doe@jhipster.com"); - this.idToken = new OidcIdToken(ID_TOKEN, Instant.now(), - Instant.now().plusSeconds(60), claims); } - <%_ } _%> +<%_ } _%> @Test<% if (databaseType === 'sql' && !reactive) { %> @Transactional<% } %> void testGetExistingAccount() <% if (!reactive) { %>throws Exception <% } %>{ - <%_ if (!reactive) { _%> - Map userDetails = new HashMap<>(); - userDetails.put("sub", TEST_USER_LOGIN); - userDetails.put("email", "john.doe@jhipster.com"); - Collection authorities = Collections.singletonList(new SimpleGrantedAuthority(AuthoritiesConstants.ADMIN)); - OAuth2User user = new DefaultOAuth2User(authorities, userDetails, "sub"); - OAuth2AuthenticationToken authentication = new OAuth2AuthenticationToken(user, authorities, "oidc"); - TestSecurityContextHolder.getContext().setAuthentication(authentication); +<%_ if (!reactive) { _%> + TestSecurityContextHolder + .getContext() + .setAuthentication(registerAuthenticationToken(authorizedClientService, clientRegistration, testAuthenticationToken())); restAccountMockMvc.perform(get("/api/account") .accept(MediaType.APPLICATION_JSON)) @@ -131,9 +126,9 @@ class AccountResourceIT <% if (databaseType === 'cassandra') { %>extends Abstrac .andExpect(jsonPath("$.login").value(TEST_USER_LOGIN)) .andExpect(jsonPath("$.email").value("john.doe@jhipster.com")) .andExpect(jsonPath("$.authorities").value(AuthoritiesConstants.ADMIN)); - <%_ } else { _%> +<%_ } else { _%> webTestClient - .mutateWith(mockAuthentication(TestUtil.authenticationToken(idToken))) + .mutateWith(mockAuthentication(registerAuthenticationToken(authorizedClientService, clientRegistration, authenticationToken(claims)))) .mutateWith(csrf()) .get().uri("/api/account") .accept(MediaType.APPLICATION_JSON) @@ -144,7 +139,7 @@ class AccountResourceIT <% if (databaseType === 'cassandra') { %>extends Abstrac .jsonPath("$.login").isEqualTo("jane") .jsonPath("$.email").isEqualTo("jane.doe@jhipster.com") .jsonPath("$.authorities").isEqualTo(AuthoritiesConstants.ADMIN); - <%_ } _%> +<%_ } _%> } @Test diff --git a/generators/server/templates/src/test/java/package/web/rest/LogoutResourceIT.java.ejs b/generators/server/templates/src/test/java/package/web/rest/LogoutResourceIT.java.ejs index ebba8f682fa..14f8bdcc0f0 100644 --- a/generators/server/templates/src/test/java/package/web/rest/LogoutResourceIT.java.ejs +++ b/generators/server/templates/src/test/java/package/web/rest/LogoutResourceIT.java.ejs @@ -16,44 +16,43 @@ See the License for the specific language governing permissions and limitations under the License. -%> +<%_ const reactivePrefix = reactive ? 'Reactive' : ''; _%> package <%= packageName %>.web.rest; +import static <%= packageName %>.test.util.OAuth2TestUtil.ID_TOKEN; +import static <%= packageName %>.test.util.OAuth2TestUtil.authenticationToken; +import static <%= packageName %>.test.util.OAuth2TestUtil.registerAuthenticationToken; +<%_ if (!reactive) { _%> +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; +<%_ } else { _%> +import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.*; +<%_ } _%> + import <%= packageName %>.IntegrationTest; import <%= packageName %>.security.AuthoritiesConstants; -import <%= packageName %>.config.TestSecurityConfiguration; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -<%_ if (reactive) { _%> -import org.springframework.context.ApplicationContext; -<%_ } _%> import org.springframework.http.MediaType; -import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.client.<% if (reactive) { %>Reactive<% } %>OAuth2AuthorizedClientService; +import org.springframework.security.oauth2.client.registration.ClientRegistration; import org.springframework.security.oauth2.client.registration.<% if (reactive) { %>Reactive<% } %>ClientRegistrationRepository; -import org.springframework.security.oauth2.core.oidc.OidcIdToken; <%_ if (!reactive) { _%> +import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; <%_ } else { _%> +import org.springframework.context.ApplicationContext; import org.springframework.test.web.reactive.server.WebTestClient; <%_ } _%> -import java.time.Instant; import java.util.Collections; import java.util.HashMap; import java.util.Map; -import static <%= packageName %>.web.rest.TestUtil.ID_TOKEN; -import static <%= packageName %>.web.rest.TestUtil.authenticationToken; -<%_ if (!reactive) { _%> -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; -<%_ } else { _%> -import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.*; -<%_ } _%> - /** * Integration tests for the {@link LogoutResource} REST controller. */ @@ -66,23 +65,30 @@ class LogoutResourceIT { @Autowired private <% if (!reactive) { %>Web<% } %>ApplicationContext context; + @Autowired + private <%= reactivePrefix %>OAuth2AuthorizedClientService authorizedClientService; + + @Autowired + private ClientRegistration clientRegistration; + <%_ if (!reactive) { _%> private MockMvc restLogoutMockMvc; <%_ } else { _%> private WebTestClient webTestClient; <%_ } _%> - private OidcIdToken idToken; + private Map claims; @BeforeEach public void before() <% if (!reactive) { %>throws Exception <% } %>{ - Map claims = new HashMap<>(); + claims = new HashMap<>(); claims.put("groups", Collections.singletonList(AuthoritiesConstants.USER)); claims.put("sub", 123); - this.idToken = new OidcIdToken(ID_TOKEN, Instant.now(), Instant.now().plusSeconds(60), claims); <%_ if (!reactive) { _%> - SecurityContextHolder.getContext().setAuthentication(authenticationToken(idToken)); + SecurityContextHolder + .getContext() + .setAuthentication(registerAuthenticationToken(authorizedClientService, clientRegistration, authenticationToken(claims))); SecurityContextHolderAwareRequestFilter authInjector = new SecurityContextHolderAwareRequestFilter(); authInjector.afterPropertiesSet(); @@ -111,7 +117,7 @@ class LogoutResourceIT { .get("end_session_endpoint").toString()).block(); this.webTestClient.mutateWith(csrf()) - .mutateWith(mockAuthentication(TestUtil.authenticationToken(idToken))) + .mutateWith(mockAuthentication(registerAuthenticationToken(authorizedClientService, clientRegistration, authenticationToken(claims)))) .post().uri("/api/logout").exchange() .expectStatus().isOk() .expectHeader().contentType(MediaType.APPLICATION_JSON_VALUE) diff --git a/generators/server/templates/src/test/java/package/web/rest/TestUtil.java.ejs b/generators/server/templates/src/test/java/package/web/rest/TestUtil.java.ejs index 1a3ca78d3b4..6e8ac82e558 100644 --- a/generators/server/templates/src/test/java/package/web/rest/TestUtil.java.ejs +++ b/generators/server/templates/src/test/java/package/web/rest/TestUtil.java.ejs @@ -26,9 +26,6 @@ import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; -<%_ if (authenticationType === 'oauth2') { _%> -import <%= packageName %>.security.SecurityUtils; -<%_ } _%> import org.hamcrest.Description; import org.hamcrest.TypeSafeDiagnosingMatcher; import org.hamcrest.TypeSafeMatcher; @@ -45,21 +42,11 @@ import org.springframework.security.test.context.TestSecurityContextHolder; import rx.Observable; import rx.internal.util.UtilityFunctions; <%_ } _%> -<%_ if (authenticationType === 'oauth2') { _%> -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; -import org.springframework.security.oauth2.core.oidc.OidcIdToken; -import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser; -import org.springframework.security.oauth2.core.oidc.user.OidcUser; -<%_ } _%> import java.io.IOException; import java.math.BigDecimal; import java.time.ZonedDateTime; import java.time.format.DateTimeParseException; -<%_ if (authenticationType === 'oauth2') { _%> -import java.util.Collection; -<%_ } _%> <%_ if (databaseType === 'sql') { _%> import java.util.List; @@ -276,20 +263,6 @@ public final class TestUtil { return allQuery.getResultList(); } <%_ } _%> -<%_ if (authenticationType === 'oauth2') { _%> - - final static String ID_TOKEN = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9" + - ".eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsIm" + - "p0aSI6ImQzNWRmMTRkLTA5ZjYtNDhmZi04YTkzLTdjNmYwMzM5MzE1OSIsImlhdCI6MTU0M" + - "Tk3MTU4MywiZXhwIjoxNTQxOTc1MTgzfQ.QaQOarmV8xEUYV7yvWzX3cUE_4W1luMcWCwpr" + - "oqqUrg"; - - public static OAuth2AuthenticationToken authenticationToken(OidcIdToken idToken) { - Collection authorities = SecurityUtils.extractAuthorityFromClaims(idToken.getClaims()); - OidcUser user = new DefaultOidcUser(authorities, idToken); - return new OAuth2AuthenticationToken(user, authorities, "oidc"); - } -<%_ } _%> <%_ if (searchEngine === 'couchbase') { _%> public static void retryUntilNotEmpty(Callable> iterableCallable) { diff --git a/generators/server/templates/src/test/java/package/web/rest/UserResourceIT.java.ejs b/generators/server/templates/src/test/java/package/web/rest/UserResourceIT.java.ejs index d24d7b76d6d..7a151690e46 100644 --- a/generators/server/templates/src/test/java/package/web/rest/UserResourceIT.java.ejs +++ b/generators/server/templates/src/test/java/package/web/rest/UserResourceIT.java.ejs @@ -489,7 +489,7 @@ class UserResourceIT <% if (databaseType === 'cassandra') { %>extends AbstractCa void getAllUsers()<% if (!reactive) { %> throws Exception<% } %> { // Initialize the database userRepository.<% if (databaseType === 'sql' && reactive && authenticationType === 'oauth2') { %>create<% } else { %>save<% } %><% if (databaseType === 'sql' && !reactive) { %>AndFlush<% } %>(user)<% if (reactive) { %>.block()<% } %>; - <%_ if (databaseType === 'sql' && reactive) { _%> + <%_ if (databaseType === 'sql' && reactive && applicationType !== 'microservice') { _%> authorityRepository .findById(AuthoritiesConstants.USER) .flatMap(authority -> userRepository.saveUserAuthority(user.getId(), authority.getName())) @@ -526,7 +526,7 @@ class UserResourceIT <% if (databaseType === 'cassandra') { %>extends AbstractCa assertThat(foundUser.getImageUrl()).isEqualTo(DEFAULT_IMAGEURL); <%_ } _%> assertThat(foundUser.getLangKey()).isEqualTo(DEFAULT_LANGKEY); - <%_ if (databaseType === 'sql') { _%> + <%_ if (databaseType === 'sql' && applicationType !== 'microservice') { _%> assertThat(foundUser.getAuthorities()).containsExactly(AuthoritiesConstants.USER); <%_ } _%> <%_ } _%> @@ -539,7 +539,7 @@ class UserResourceIT <% if (databaseType === 'cassandra') { %>extends AbstractCa void getUser()<% if (!reactive) { %> throws Exception<% } %> { // Initialize the database userRepository.<% if (databaseType === 'sql' && reactive && authenticationType === 'oauth2') { %>create<% } else { %>save<% } %><% if (databaseType === 'sql' && !reactive) { %>AndFlush<% } %>(user)<% if (reactive) { %>.block()<% } %>; - <%_ if (databaseType === 'sql' && reactive) { _%> + <%_ if (databaseType === 'sql' && reactive && applicationType !== 'microservice') { _%> authorityRepository .findById(AuthoritiesConstants.USER) .flatMap(authority -> userRepository.saveUserAuthority(user.getId(), authority.getName())) @@ -577,10 +577,12 @@ class UserResourceIT <% if (databaseType === 'cassandra') { %>extends AbstractCa .jsonPath("$.firstName").isEqualTo(DEFAULT_FIRSTNAME) .jsonPath("$.lastName").isEqualTo(DEFAULT_LASTNAME) .jsonPath("$.email").isEqualTo(DEFAULT_EMAIL) - <%_ if (databaseType !== 'cassandra') { _%> + <%_ if (databaseType !== 'cassandra') { _%> .jsonPath("$.imageUrl").isEqualTo(DEFAULT_IMAGEURL) - <%_ } _%> - .jsonPath("$.langKey").isEqualTo(DEFAULT_LANGKEY)<% if (databaseType !== 'sql') { %>;<% } else { %> + <%_ } _%> + .jsonPath("$.langKey").isEqualTo(DEFAULT_LANGKEY)<% + if (databaseType !== 'sql' || applicationType === 'microservice') { %>; + <%_ } else { _%> .jsonPath("$.authorities") .isEqualTo(AuthoritiesConstants.USER); <% } %> From 92d2ef7c7e50f3f0fc158ed1ef5a0d7c71966951 Mon Sep 17 00:00:00 2001 From: Marcelo Shima Date: Sat, 26 Jun 2021 10:18:36 -0300 Subject: [PATCH 4/7] Cleanup --- .../src/main/java/package/service/UserService.java.ejs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/generators/server/templates/src/main/java/package/service/UserService.java.ejs b/generators/server/templates/src/main/java/package/service/UserService.java.ejs index ebdb23f266c..d377603972c 100644 --- a/generators/server/templates/src/main/java/package/service/UserService.java.ejs +++ b/generators/server/templates/src/main/java/package/service/UserService.java.ejs @@ -74,7 +74,6 @@ import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; <%_ if (databaseType === 'sql') { _%> import org.springframework.transaction.annotation.Transactional; -import org.springframework.transaction.annotation.Propagation; <%_ } _%> <%_ if (reactive) { _%> import reactor.core.publisher.Flux; @@ -879,9 +878,6 @@ public class UserService { <%_ } _%> <%_ if (authenticationType === 'oauth2') { _%> - <%_ if (databaseType === 'sql') { _%> - @Transactional(propagation = Propagation.REQUIRES_NEW) - <%_ } _%> private <% if (reactive) { %>Mono<<%= asEntity('User') %>><% } else { %><%= asEntity('User') %><% } %> syncUserWithIdP(Map details, <%= asEntity('User') %> user) { // save authorities in to sync user roles/groups between IdP and JHipster's local database <%_ if (!reactive) { _%> From 3301376e5f27199f8f0e87eb9bb079fb72c1fe3f Mon Sep 17 00:00:00 2001 From: Marcelo Shima Date: Sat, 26 Jun 2021 10:38:20 -0300 Subject: [PATCH 5/7] Print stack trace on h2 console startup failure. --- .../src/main/java/package/config/WebConfigurer.java.ejs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/generators/server/templates/src/main/java/package/config/WebConfigurer.java.ejs b/generators/server/templates/src/main/java/package/config/WebConfigurer.java.ejs index 07824a7d9c6..a3f7a4d1604 100644 --- a/generators/server/templates/src/main/java/package/config/WebConfigurer.java.ejs +++ b/generators/server/templates/src/main/java/package/config/WebConfigurer.java.ejs @@ -116,7 +116,10 @@ public class WebConfigurer implements <% if (!reactive) { %>ServletContextInitia <%_ } _%> H2ConfigurationHelper.initH2Console(); <%_ if (!applicationTypeMonolith) { _%> - } catch (Exception e) {}; + } catch (Exception e) { + // Console may already be running on another app. + e.printStackTrace(); + }; <%_ } _%> } <%_ } _%> From ff17f09d917e8945d9f3fde10a2a162001024a6f Mon Sep 17 00:00:00 2001 From: Marcelo Shima Date: Sat, 26 Jun 2021 18:35:18 -0300 Subject: [PATCH 6/7] Switch to derived variables. --- generators/server/files.js | 6 +++--- generators/server/index.js | 2 ++ .../src/main/java/package/service/UserService.java.ejs | 2 +- .../test/java/package/web/rest/UserResourceIT.java.ejs | 10 +++++----- 4 files changed, 11 insertions(+), 9 deletions(-) diff --git a/generators/server/files.js b/generators/server/files.js index b61aab9c2c5..79d010b4fcf 100644 --- a/generators/server/files.js +++ b/generators/server/files.js @@ -1167,7 +1167,7 @@ const serverFiles = { ], springBootOauth2: [ { - condition: generator => generator.authenticationType === 'oauth2' && generator.applicationType === 'monolith', + condition: generator => generator.authenticationTypeOauth2 && generator.applicationTypeMonolith, path: SERVER_MAIN_SRC_DIR, templates: [ { @@ -1177,7 +1177,7 @@ const serverFiles = { ], }, { - condition: generator => generator.authenticationType === 'oauth2' && generator.applicationType !== 'microservice', + condition: generator => generator.authenticationTypeOauth2 && !generator.applicationTypeMicroservice, path: SERVER_MAIN_SRC_DIR, templates: [ { @@ -1187,7 +1187,7 @@ const serverFiles = { ], }, { - condition: generator => generator.authenticationType === 'oauth2' && generator.applicationType !== 'microservice', + condition: generator => generator.authenticationTypeOauth2 && !generator.applicationTypeMicroservice, path: SERVER_TEST_SRC_DIR, templates: [ { file: 'package/test/util/OAuth2TestUtil.java', renameTo: generator => `${generator.testDir}test/util/OAuth2TestUtil.java` }, diff --git a/generators/server/index.js b/generators/server/index.js index 97a756f018b..84882db723a 100644 --- a/generators/server/index.js +++ b/generators/server/index.js @@ -253,7 +253,9 @@ module.exports = class JHipsterServerGenerator extends BaseBlueprintGenerator { this.loadAppConfig(); this.loadDerivedAppConfig(); this.loadClientConfig(); + this.loadDerivedClientConfig(); this.loadServerConfig(); + this.loadDerivedServerConfig(); this.loadTranslationConfig(); }, }; diff --git a/generators/server/templates/src/main/java/package/service/UserService.java.ejs b/generators/server/templates/src/main/java/package/service/UserService.java.ejs index d377603972c..febc60194c1 100644 --- a/generators/server/templates/src/main/java/package/service/UserService.java.ejs +++ b/generators/server/templates/src/main/java/package/service/UserService.java.ejs @@ -1000,7 +1000,7 @@ public class UserService { <%_ } _%> .collect(Collectors.toSet())); - <%_ if (databaseType === 'no') { _%> + <%_ if (databaseTypeNo) { _%> return <% if (reactive) { %>Mono.just(user)<% } else { %>user<% } %>; <%_ } else { _%> <%_ if (!reactive) { _%> diff --git a/generators/server/templates/src/test/java/package/web/rest/UserResourceIT.java.ejs b/generators/server/templates/src/test/java/package/web/rest/UserResourceIT.java.ejs index 7a151690e46..38edda9412b 100644 --- a/generators/server/templates/src/test/java/package/web/rest/UserResourceIT.java.ejs +++ b/generators/server/templates/src/test/java/package/web/rest/UserResourceIT.java.ejs @@ -489,7 +489,7 @@ class UserResourceIT <% if (databaseType === 'cassandra') { %>extends AbstractCa void getAllUsers()<% if (!reactive) { %> throws Exception<% } %> { // Initialize the database userRepository.<% if (databaseType === 'sql' && reactive && authenticationType === 'oauth2') { %>create<% } else { %>save<% } %><% if (databaseType === 'sql' && !reactive) { %>AndFlush<% } %>(user)<% if (reactive) { %>.block()<% } %>; - <%_ if (databaseType === 'sql' && reactive && applicationType !== 'microservice') { _%> + <%_ if (databaseTypeSql && reactive && !applicationTypeMicroservice) { _%> authorityRepository .findById(AuthoritiesConstants.USER) .flatMap(authority -> userRepository.saveUserAuthority(user.getId(), authority.getName())) @@ -526,7 +526,7 @@ class UserResourceIT <% if (databaseType === 'cassandra') { %>extends AbstractCa assertThat(foundUser.getImageUrl()).isEqualTo(DEFAULT_IMAGEURL); <%_ } _%> assertThat(foundUser.getLangKey()).isEqualTo(DEFAULT_LANGKEY); - <%_ if (databaseType === 'sql' && applicationType !== 'microservice') { _%> + <%_ if (databaseTypeSql && !applicationTypeMicroservice) { _%> assertThat(foundUser.getAuthorities()).containsExactly(AuthoritiesConstants.USER); <%_ } _%> <%_ } _%> @@ -539,7 +539,7 @@ class UserResourceIT <% if (databaseType === 'cassandra') { %>extends AbstractCa void getUser()<% if (!reactive) { %> throws Exception<% } %> { // Initialize the database userRepository.<% if (databaseType === 'sql' && reactive && authenticationType === 'oauth2') { %>create<% } else { %>save<% } %><% if (databaseType === 'sql' && !reactive) { %>AndFlush<% } %>(user)<% if (reactive) { %>.block()<% } %>; - <%_ if (databaseType === 'sql' && reactive && applicationType !== 'microservice') { _%> + <%_ if (databaseTypeSql && reactive && !applicationTypeMicroservice) { _%> authorityRepository .findById(AuthoritiesConstants.USER) .flatMap(authority -> userRepository.saveUserAuthority(user.getId(), authority.getName())) @@ -577,11 +577,11 @@ class UserResourceIT <% if (databaseType === 'cassandra') { %>extends AbstractCa .jsonPath("$.firstName").isEqualTo(DEFAULT_FIRSTNAME) .jsonPath("$.lastName").isEqualTo(DEFAULT_LASTNAME) .jsonPath("$.email").isEqualTo(DEFAULT_EMAIL) - <%_ if (databaseType !== 'cassandra') { _%> + <%_ if (!databaseTypeCassandra) { _%> .jsonPath("$.imageUrl").isEqualTo(DEFAULT_IMAGEURL) <%_ } _%> .jsonPath("$.langKey").isEqualTo(DEFAULT_LANGKEY)<% - if (databaseType !== 'sql' || applicationType === 'microservice') { %>; + if (!databaseTypeSql || applicationTypeMicroservice) { %>; <%_ } else { _%> .jsonPath("$.authorities") .isEqualTo(AuthoritiesConstants.USER); From e53c5e17f84cdadce2f0c20120509b953f51a5fc Mon Sep 17 00:00:00 2001 From: Marcelo Shima Date: Sat, 26 Jun 2021 19:09:19 -0300 Subject: [PATCH 7/7] Fix ident. --- .../java/package/web/rest/AccountResourceIT_oauth2.java.ejs | 2 +- .../src/test/java/package/web/rest/LogoutResourceIT.java.ejs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/generators/server/templates/src/test/java/package/web/rest/AccountResourceIT_oauth2.java.ejs b/generators/server/templates/src/test/java/package/web/rest/AccountResourceIT_oauth2.java.ejs index 2483fd0546a..a3b470cc9f0 100644 --- a/generators/server/templates/src/test/java/package/web/rest/AccountResourceIT_oauth2.java.ejs +++ b/generators/server/templates/src/test/java/package/web/rest/AccountResourceIT_oauth2.java.ejs @@ -98,7 +98,7 @@ class AccountResourceIT <% if (databaseType === 'cassandra') { %>extends Abstrac private ReactiveOAuth2AuthorizedClientService authorizedClientService; @Autowired - private ClientRegistration clientRegistration; + private ClientRegistration clientRegistration; <%_ } _%> <%_ if (reactive) { _%> diff --git a/generators/server/templates/src/test/java/package/web/rest/LogoutResourceIT.java.ejs b/generators/server/templates/src/test/java/package/web/rest/LogoutResourceIT.java.ejs index 14f8bdcc0f0..ce4d270aff3 100644 --- a/generators/server/templates/src/test/java/package/web/rest/LogoutResourceIT.java.ejs +++ b/generators/server/templates/src/test/java/package/web/rest/LogoutResourceIT.java.ejs @@ -69,7 +69,7 @@ class LogoutResourceIT { private <%= reactivePrefix %>OAuth2AuthorizedClientService authorizedClientService; @Autowired - private ClientRegistration clientRegistration; + private ClientRegistration clientRegistration; <%_ if (!reactive) { _%> private MockMvc restLogoutMockMvc;