├── .gitignore ├── .travis.yml ├── LICENSE ├── README-zh.md ├── README.md ├── authserver ├── build.gradle └── src │ └── main │ ├── java │ └── demo │ │ ├── Application.java │ │ ├── authentication │ │ ├── DeviceClientAuthenticationProvider.java │ │ └── DeviceClientAuthenticationToken.java │ │ ├── config │ │ ├── AuthorizationServerConfig.java │ │ └── DefaultSecurityConfig.java │ │ ├── federation │ │ ├── FederatedIdentityAuthenticationSuccessHandler.java │ │ ├── FederatedIdentityIdTokenCustomizer.java │ │ └── UserRepositoryOAuth2UserHandler.java │ │ ├── jose │ │ ├── Jwks.java │ │ └── KeyGeneratorUtils.java │ │ └── web │ │ ├── AuthorizationConsentController.java │ │ ├── DefaultErrorController.java │ │ ├── DeviceController.java │ │ ├── LoginController.java │ │ └── authentication │ │ └── DeviceClientAuthenticationConverter.java │ └── resources │ ├── application.yml │ ├── static │ ├── assets │ │ ├── css │ │ │ └── signin.css │ │ └── img │ │ │ ├── devices.png │ │ │ ├── github.png │ │ │ └── google.png │ └── index.html │ └── templates │ ├── authorize.html │ ├── consent.html │ ├── device-activate.html │ ├── device-activated.html │ ├── error.html │ └── login.html ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── resourceserver ├── build.gradle └── src │ └── main │ ├── java │ └── demo │ │ ├── UiApplication.java │ │ └── config │ │ └── SecurityConfig.java │ └── resources │ └── application.yml ├── settings.gradle ├── ui-spa ├── .editorconfig ├── .vscode │ ├── extensions.json │ ├── launch.json │ └── tasks.json ├── README.md ├── angular.json ├── package.json ├── public │ └── spring-security.svg ├── src │ ├── app │ │ ├── app.component.css │ │ ├── app.component.html │ │ ├── app.component.ts │ │ ├── app.config.ts │ │ ├── app.routes.ts │ │ ├── environment.ts │ │ └── http.interceptors.ts │ ├── index.html │ ├── main.ts │ └── styles.css ├── tsconfig.app.json ├── tsconfig.json ├── tsconfig.spec.json └── ui-spa.iml └── ui ├── build.gradle └── src └── main ├── java └── demo │ └── UiApplication.java └── resources ├── application.yml └── static └── index.html /.gitignore: -------------------------------------------------------------------------------- 1 | /logs/ 2 | /authserver/logs/ 3 | /authserver/out/ 4 | /authserver/build/ 5 | /resourceserver/logs/ 6 | /resourceserver/out/ 7 | /resourceserver/build/ 8 | /ui-spa/logs/ 9 | /ui-spa/dist/ 10 | /ui-spa/node_modules/ 11 | /ui-spa/src/main/resources/static/ 12 | /ui-spa/out/ 13 | /ui-spa/build/ 14 | /ui-spa/.angular/ 15 | /ui/out/ 16 | /build/ 17 | /ui/build/ 18 | 19 | ### STS ### 20 | .classpath 21 | .factorypath 22 | .project 23 | .settings 24 | .springBeans 25 | 26 | ### IntelliJ IDEA ### 27 | .idea 28 | .gradle 29 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 HaiYan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README-zh.md: -------------------------------------------------------------------------------- 1 | # ng-boot-oauth 2 | oauth2 demo 程序,使用了angular 19 和 springboot框架。 3 | 4 | 默认的用户名是 `admin`, 密码是 `111111` 5 | ## 1. 特点 6 | ### 模块 7 | 8 | 项目包括3个模块: 9 | * authserver 10 | * ui (oauth2 code flow的客户端应用) 11 | * ui-spa (oauth2 code flow with pkce 的客户端应用,有一个独立的前端模块) 12 | 13 | ### 前端 14 | * Angular 19 15 | 16 | ### 后端 17 | * Gradle 构建工具 18 | * Spring Boot 19 | * Spring Authorization Server 20 | * Thymeleaf 模版引擎 21 | 22 | ## 2. 在开发模式下运行 23 | 注意 **ui** 模块和 **ui-spa** 模块由于都用了8080端口,所以不能同时运行,每次只能运行一个模块。 24 | ### 获取代码 25 | ```bash 26 | git clone https://github.com/qihaiyan/ng-boot-oauth.git 27 | cd ng-boot-oauth 28 | ``` 29 | 30 | ### 运行 OAuth2 Server 31 | ```bash 32 | cd authserver 33 | ./gradlew bootRun 34 | ``` 35 | 36 | ### 运行 ui 模块 37 | ```bash 38 | cd ui 39 | ./gradlew bootRun 40 | ``` 41 | 通过 `http://localhost:8080` 这个地址访问应用。 42 | 43 | ### 运行 ui-spa 模块 44 | 45 | * 运行后端服务 46 | ```bash 47 | cd ui-spa 48 | ./gradlew bootRun 49 | ``` 50 | 51 | * 运行前端模块 52 | 53 | 如果是第一次运行程序,需要先安装依赖。 54 | ```bash 55 | cd ui-spa 56 | npm install 57 | ``` 58 | 59 | 依赖安装完成后,运行前端程序 60 | ```bash 61 | cd ui-spa 62 | npm run dev 63 | ``` 64 | 65 | 通过 `http://localhost:4200` 这个地址访问应用。 66 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ng-boot-oauth 2 | [中文介绍](README-zh.md) 3 | 4 | An oauth2 demo with angularjs and springboot. 5 | 6 | Default username is `user`, and password is `111111`. 7 | ## 1. Features 8 | ### Modules 9 | 10 | The project contains 3 modules 11 | * authserver 12 | * ui (A client using oauth2 code flow) 13 | * ui-spa (A client using oauth2 pkce flow, with a standalone frontend module) 14 | 15 | ### Frontend 16 | * Angular 19 17 | 18 | ### Backend 19 | * Gradle Build Tool 20 | * Spring Boot 21 | * Spring security Oauth2 integration 22 | * Thymeleaf server-side Java template engine 23 | 24 | ## 2. RUNNING IN DEVELOPMENT MODE 25 | Note that **ui** module and **ui-spa** module can't be running at the same time, because they use the same port 8080. 26 | ### GET THE CODE 27 | ```bash 28 | git clone https://github.com/qihaiyan/ng-boot-oauth.git 29 | cd ng-boot-oauth 30 | ``` 31 | 32 | ### RUNNING OAuth2 Server 33 | ```bash 34 | cd authserver 35 | ./gradlew bootRun 36 | ``` 37 | 38 | ### RUNNING ui MODULE 39 | ```bash 40 | cd ui 41 | ./gradlew bootRun 42 | ``` 43 | Now we can visit the app at `http://localhost:8080` 44 | 45 | ### RUNNING ui-spa MODULE 46 | 47 | * RUNNING BACKEND SERVER 48 | ```bash 49 | cd ui-spa 50 | ./gradlew bootRun 51 | ``` 52 | 53 | If it's the first time to run ui-spa module, install dependencies at first. 54 | ```bash 55 | cd ui-spa 56 | npm install 57 | ``` 58 | 59 | * RUNNING DEV SERVER 60 | ```bash 61 | cd ui-spa 62 | npm run dev 63 | ``` 64 | 65 | Now we can visit the app at `http://localhost:4200` 66 | -------------------------------------------------------------------------------- /authserver/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java' 3 | id 'org.springframework.boot' version '3.3.6' 4 | id 'io.spring.dependency-management' version '1.1.6' 5 | } 6 | 7 | tasks.withType(JavaCompile).configureEach { 8 | options.compilerArgs.add("-parameters") 9 | } 10 | 11 | configurations { 12 | compileOnly { 13 | extendsFrom annotationProcessor 14 | } 15 | testCompileOnly { 16 | extendsFrom testAnnotationProcessor 17 | } 18 | } 19 | 20 | repositories { 21 | mavenCentral() 22 | } 23 | 24 | dependencies { 25 | implementation "org.springframework.boot:spring-boot-starter-web" 26 | implementation "org.springframework.boot:spring-boot-starter-jdbc" 27 | implementation "org.springframework.boot:spring-boot-starter-thymeleaf" 28 | implementation "org.springframework.boot:spring-boot-starter-security" 29 | implementation "org.springframework.boot:spring-boot-starter-oauth2-client" 30 | implementation "org.springframework.security:spring-security-oauth2-authorization-server:1.4.1" 31 | implementation "org.webjars:webjars-locator-core" 32 | implementation "org.webjars:bootstrap:5.2.3" 33 | implementation "org.webjars:popper.js:2.9.3" 34 | implementation "org.webjars:jquery:3.6.4" 35 | runtimeOnly "com.h2database:h2" 36 | annotationProcessor 'org.projectlombok:lombok' 37 | } 38 | -------------------------------------------------------------------------------- /authserver/src/main/java/demo/Application.java: -------------------------------------------------------------------------------- 1 | package demo; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class Application { 8 | public static void main(String[] args) { 9 | SpringApplication.run(Application.class, args); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /authserver/src/main/java/demo/authentication/DeviceClientAuthenticationProvider.java: -------------------------------------------------------------------------------- 1 | package demo.authentication; 2 | 3 | import org.apache.commons.logging.Log; 4 | import org.apache.commons.logging.LogFactory; 5 | 6 | import org.springframework.security.authentication.AuthenticationProvider; 7 | import org.springframework.security.core.Authentication; 8 | import org.springframework.security.core.AuthenticationException; 9 | import org.springframework.security.oauth2.core.ClientAuthenticationMethod; 10 | import org.springframework.security.oauth2.core.OAuth2AuthenticationException; 11 | import org.springframework.security.oauth2.core.OAuth2Error; 12 | import org.springframework.security.oauth2.core.OAuth2ErrorCodes; 13 | import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; 14 | import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; 15 | import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository; 16 | import org.springframework.util.Assert; 17 | 18 | public final class DeviceClientAuthenticationProvider implements AuthenticationProvider { 19 | private static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-3.2.1"; 20 | private final Log logger = LogFactory.getLog(getClass()); 21 | private final RegisteredClientRepository registeredClientRepository; 22 | 23 | public DeviceClientAuthenticationProvider(RegisteredClientRepository registeredClientRepository) { 24 | Assert.notNull(registeredClientRepository, "registeredClientRepository cannot be null"); 25 | this.registeredClientRepository = registeredClientRepository; 26 | } 27 | 28 | @Override 29 | public Authentication authenticate(Authentication authentication) throws AuthenticationException { 30 | DeviceClientAuthenticationToken deviceClientAuthentication = 31 | (DeviceClientAuthenticationToken) authentication; 32 | 33 | if (!ClientAuthenticationMethod.NONE.equals(deviceClientAuthentication.getClientAuthenticationMethod())) { 34 | return null; 35 | } 36 | 37 | String clientId = deviceClientAuthentication.getPrincipal().toString(); 38 | RegisteredClient registeredClient = this.registeredClientRepository.findByClientId(clientId); 39 | if (registeredClient == null) { 40 | throwInvalidClient(OAuth2ParameterNames.CLIENT_ID); 41 | } 42 | 43 | if (this.logger.isTraceEnabled()) { 44 | this.logger.trace("Retrieved registered client"); 45 | } 46 | 47 | if (!registeredClient.getClientAuthenticationMethods().contains( 48 | deviceClientAuthentication.getClientAuthenticationMethod())) { 49 | throwInvalidClient("authentication_method"); 50 | } 51 | 52 | if (this.logger.isTraceEnabled()) { 53 | this.logger.trace("Validated device client authentication parameters"); 54 | } 55 | 56 | if (this.logger.isTraceEnabled()) { 57 | this.logger.trace("Authenticated device client"); 58 | } 59 | 60 | return new DeviceClientAuthenticationToken(registeredClient, 61 | deviceClientAuthentication.getClientAuthenticationMethod(), null); 62 | } 63 | 64 | @Override 65 | public boolean supports(Class authentication) { 66 | return DeviceClientAuthenticationToken.class.isAssignableFrom(authentication); 67 | } 68 | 69 | private static void throwInvalidClient(String parameterName) { 70 | OAuth2Error error = new OAuth2Error( 71 | OAuth2ErrorCodes.INVALID_CLIENT, 72 | "Device client authentication failed: " + parameterName, 73 | ERROR_URI 74 | ); 75 | throw new OAuth2AuthenticationException(error); 76 | } 77 | 78 | } 79 | -------------------------------------------------------------------------------- /authserver/src/main/java/demo/authentication/DeviceClientAuthenticationToken.java: -------------------------------------------------------------------------------- 1 | package demo.authentication; 2 | 3 | import org.springframework.lang.Nullable; 4 | import org.springframework.security.core.Transient; 5 | import org.springframework.security.oauth2.core.ClientAuthenticationMethod; 6 | import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken; 7 | import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; 8 | 9 | import java.util.Map; 10 | 11 | @Transient 12 | public class DeviceClientAuthenticationToken extends OAuth2ClientAuthenticationToken { 13 | 14 | public DeviceClientAuthenticationToken( 15 | String clientId, ClientAuthenticationMethod clientAuthenticationMethod, 16 | @Nullable Object credentials, @Nullable Map additionalParameters) { 17 | super(clientId, clientAuthenticationMethod, credentials, additionalParameters); 18 | } 19 | 20 | public DeviceClientAuthenticationToken( 21 | RegisteredClient registeredClient, ClientAuthenticationMethod clientAuthenticationMethod, 22 | @Nullable Object credentials) { 23 | super(registeredClient, clientAuthenticationMethod, credentials); 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /authserver/src/main/java/demo/config/AuthorizationServerConfig.java: -------------------------------------------------------------------------------- 1 | package demo.config; 2 | 3 | import com.nimbusds.jose.jwk.JWKSet; 4 | import com.nimbusds.jose.jwk.RSAKey; 5 | import com.nimbusds.jose.jwk.source.JWKSource; 6 | import com.nimbusds.jose.proc.SecurityContext; 7 | import demo.authentication.DeviceClientAuthenticationProvider; 8 | import demo.federation.FederatedIdentityIdTokenCustomizer; 9 | import demo.jose.Jwks; 10 | import demo.web.authentication.DeviceClientAuthenticationConverter; 11 | import org.springframework.context.annotation.Bean; 12 | import org.springframework.context.annotation.Configuration; 13 | import org.springframework.core.Ordered; 14 | import org.springframework.core.annotation.Order; 15 | import org.springframework.http.MediaType; 16 | import org.springframework.jdbc.core.JdbcTemplate; 17 | import org.springframework.jdbc.datasource.embedded.EmbeddedDatabase; 18 | import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder; 19 | import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType; 20 | import org.springframework.security.config.Customizer; 21 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; 22 | import org.springframework.security.oauth2.core.AuthorizationGrantType; 23 | import org.springframework.security.oauth2.core.ClientAuthenticationMethod; 24 | import org.springframework.security.oauth2.core.oidc.OidcScopes; 25 | import org.springframework.security.oauth2.jwt.JwtDecoder; 26 | import org.springframework.security.oauth2.server.authorization.JdbcOAuth2AuthorizationConsentService; 27 | import org.springframework.security.oauth2.server.authorization.JdbcOAuth2AuthorizationService; 28 | import org.springframework.security.oauth2.server.authorization.client.JdbcRegisteredClientRepository; 29 | import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; 30 | import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository; 31 | import org.springframework.security.oauth2.server.authorization.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration; 32 | import org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer; 33 | import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings; 34 | import org.springframework.security.oauth2.server.authorization.settings.ClientSettings; 35 | import org.springframework.security.oauth2.server.authorization.settings.TokenSettings; 36 | import org.springframework.security.oauth2.server.authorization.token.JwtEncodingContext; 37 | import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenCustomizer; 38 | import org.springframework.security.web.SecurityFilterChain; 39 | import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint; 40 | import org.springframework.security.web.util.matcher.MediaTypeRequestMatcher; 41 | 42 | import java.util.UUID; 43 | 44 | import static org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer.authorizationServer; 45 | 46 | @Configuration(proxyBeanMethods = false) 47 | public class AuthorizationServerConfig { 48 | private static final String CUSTOM_CONSENT_PAGE_URI = "/oauth2/consent"; 49 | 50 | @Bean 51 | @Order(Ordered.HIGHEST_PRECEDENCE) 52 | public SecurityFilterChain authorizationServerSecurityFilterChain( 53 | HttpSecurity http, RegisteredClientRepository registeredClientRepository, 54 | AuthorizationServerSettings authorizationServerSettings) throws Exception { 55 | 56 | DeviceClientAuthenticationConverter deviceClientAuthenticationConverter = 57 | new DeviceClientAuthenticationConverter( 58 | authorizationServerSettings.getDeviceAuthorizationEndpoint()); 59 | DeviceClientAuthenticationProvider deviceClientAuthenticationProvider = 60 | new DeviceClientAuthenticationProvider(registeredClientRepository); 61 | 62 | OAuth2AuthorizationServerConfigurer authorizationServerConfigurer = authorizationServer(); 63 | 64 | http 65 | .securityMatcher(authorizationServerConfigurer.getEndpointsMatcher()) 66 | .with(authorizationServerConfigurer, (authorizationServer) -> 67 | authorizationServer 68 | .deviceAuthorizationEndpoint(deviceAuthorizationEndpoint -> 69 | deviceAuthorizationEndpoint.verificationUri("/activate") 70 | ) 71 | .deviceVerificationEndpoint(deviceVerificationEndpoint -> 72 | deviceVerificationEndpoint.consentPage(CUSTOM_CONSENT_PAGE_URI) 73 | ) 74 | .clientAuthentication(clientAuthentication -> 75 | clientAuthentication 76 | .authenticationConverter(deviceClientAuthenticationConverter) 77 | .authenticationProvider(deviceClientAuthenticationProvider) 78 | ) 79 | .authorizationEndpoint(authorizationEndpoint -> 80 | authorizationEndpoint.consentPage(CUSTOM_CONSENT_PAGE_URI)) 81 | .oidc(Customizer.withDefaults()) // Enable OpenID Connect 1.0 82 | ) 83 | .cors(Customizer.withDefaults()) 84 | .authorizeHttpRequests((authorize) -> 85 | authorize.anyRequest().authenticated() 86 | ) 87 | .exceptionHandling((exceptions) -> exceptions 88 | .defaultAuthenticationEntryPointFor( 89 | new LoginUrlAuthenticationEntryPoint("/login"), 90 | new MediaTypeRequestMatcher(MediaType.TEXT_HTML) 91 | ) 92 | ); 93 | return http.build(); 94 | } 95 | 96 | @Bean 97 | public JdbcRegisteredClientRepository registeredClientRepository(JdbcTemplate jdbcTemplate) { 98 | RegisteredClient messagingClient = RegisteredClient.withId(UUID.randomUUID().toString()) 99 | .clientId("messaging-client") 100 | .clientSecret("{noop}secret") 101 | .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC) 102 | .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) 103 | .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN) 104 | .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS) 105 | .redirectUri("http://127.0.0.1:8080/login/oauth2/code/messaging-client-oidc") 106 | .redirectUri("http://127.0.0.1:8080/authorized") 107 | .postLogoutRedirectUri("http://127.0.0.1:8080/logged-out") 108 | .scope(OidcScopes.OPENID) 109 | .scope(OidcScopes.PROFILE) 110 | .scope("message.read") 111 | .scope("message.write") 112 | .scope("user.read") 113 | // .clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build()) 114 | .build(); 115 | 116 | RegisteredClient deviceClient = RegisteredClient.withId(UUID.randomUUID().toString()) 117 | .clientId("device-messaging-client") 118 | .clientAuthenticationMethod(ClientAuthenticationMethod.NONE) 119 | .authorizationGrantType(AuthorizationGrantType.DEVICE_CODE) 120 | .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN) 121 | .scope("message.read") 122 | .scope("message.write") 123 | .build(); 124 | 125 | RegisteredClient tokenExchangeClient = RegisteredClient.withId(UUID.randomUUID().toString()) 126 | .clientId("token-client") 127 | .clientSecret("{noop}token") 128 | .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC) 129 | .authorizationGrantType(new AuthorizationGrantType("urn:ietf:params:oauth:grant-type:token-exchange")) 130 | .scope("message.read") 131 | .scope("message.write") 132 | .build(); 133 | 134 | RegisteredClient mtlsDemoClient = RegisteredClient.withId(UUID.randomUUID().toString()) 135 | .clientId("mtls-demo-client") 136 | .clientAuthenticationMethod(ClientAuthenticationMethod.TLS_CLIENT_AUTH) 137 | .clientAuthenticationMethod(ClientAuthenticationMethod.SELF_SIGNED_TLS_CLIENT_AUTH) 138 | .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS) 139 | .scope("message.read") 140 | .scope("message.write") 141 | .clientSettings( 142 | ClientSettings.builder() 143 | .x509CertificateSubjectDN("CN=demo-client-sample,OU=Spring Samples,O=Spring,C=US") 144 | .jwkSetUrl("http://127.0.0.1:8080/jwks") 145 | .build() 146 | ) 147 | .tokenSettings( 148 | TokenSettings.builder() 149 | .x509CertificateBoundAccessTokens(true) 150 | .build() 151 | ) 152 | .build(); 153 | 154 | RegisteredClient publicClient = RegisteredClient.withId(UUID.randomUUID().toString()) 155 | .clientId("spa-client") 156 | .clientAuthenticationMethod(ClientAuthenticationMethod.NONE) 157 | .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) 158 | .redirectUri("http://localhost:4200") 159 | .postLogoutRedirectUri("http://localhost:4200") 160 | .scope(OidcScopes.OPENID) 161 | .scope(OidcScopes.PROFILE) 162 | .scope(OidcScopes.EMAIL) 163 | .build(); 164 | 165 | JdbcRegisteredClientRepository registeredClientRepository = new JdbcRegisteredClientRepository(jdbcTemplate); 166 | registeredClientRepository.save(messagingClient); 167 | registeredClientRepository.save(deviceClient); 168 | registeredClientRepository.save(tokenExchangeClient); 169 | registeredClientRepository.save(mtlsDemoClient); 170 | registeredClientRepository.save(publicClient); 171 | 172 | return registeredClientRepository; 173 | } 174 | 175 | @Bean 176 | public JdbcOAuth2AuthorizationService authorizationService(JdbcTemplate jdbcTemplate, 177 | RegisteredClientRepository registeredClientRepository) { 178 | return new JdbcOAuth2AuthorizationService(jdbcTemplate, registeredClientRepository); 179 | } 180 | 181 | @Bean 182 | public JdbcOAuth2AuthorizationConsentService authorizationConsentService(JdbcTemplate jdbcTemplate, 183 | RegisteredClientRepository registeredClientRepository) { 184 | return new JdbcOAuth2AuthorizationConsentService(jdbcTemplate, registeredClientRepository); 185 | } 186 | 187 | @Bean 188 | public OAuth2TokenCustomizer idTokenCustomizer() { 189 | return new FederatedIdentityIdTokenCustomizer(); 190 | } 191 | 192 | @Bean 193 | public JWKSource jwkSource() { 194 | RSAKey rsaKey = Jwks.generateRsa(); 195 | JWKSet jwkSet = new JWKSet(rsaKey); 196 | return (jwkSelector, securityContext) -> jwkSelector.select(jwkSet); 197 | } 198 | 199 | @Bean 200 | public JwtDecoder jwtDecoder(JWKSource jwkSource) { 201 | return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource); 202 | } 203 | 204 | @Bean 205 | public AuthorizationServerSettings authorizationServerSettings() { 206 | return AuthorizationServerSettings.builder().build(); 207 | } 208 | 209 | @Bean 210 | public EmbeddedDatabase embeddedDatabase() { 211 | return new EmbeddedDatabaseBuilder() 212 | .generateUniqueName(true) 213 | .setType(EmbeddedDatabaseType.H2) 214 | .setScriptEncoding("UTF-8") 215 | .addScript("org/springframework/security/oauth2/server/authorization/oauth2-authorization-schema.sql") 216 | .addScript("org/springframework/security/oauth2/server/authorization/oauth2-authorization-consent-schema.sql") 217 | .addScript("org/springframework/security/oauth2/server/authorization/client/oauth2-registered-client-schema.sql") 218 | .build(); 219 | } 220 | 221 | } 222 | -------------------------------------------------------------------------------- /authserver/src/main/java/demo/config/DefaultSecurityConfig.java: -------------------------------------------------------------------------------- 1 | package demo.config; 2 | 3 | import demo.federation.FederatedIdentityAuthenticationSuccessHandler; 4 | import jakarta.servlet.http.HttpServletRequest; 5 | import jakarta.servlet.http.HttpServletResponse; 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.context.annotation.Configuration; 8 | import org.springframework.security.config.Customizer; 9 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; 10 | import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; 11 | import org.springframework.security.core.session.SessionRegistry; 12 | import org.springframework.security.core.session.SessionRegistryImpl; 13 | import org.springframework.security.core.userdetails.User; 14 | import org.springframework.security.core.userdetails.UserDetails; 15 | import org.springframework.security.core.userdetails.UserDetailsService; 16 | import org.springframework.security.crypto.factory.PasswordEncoderFactories; 17 | import org.springframework.security.crypto.password.PasswordEncoder; 18 | import org.springframework.security.provisioning.InMemoryUserDetailsManager; 19 | import org.springframework.security.web.SecurityFilterChain; 20 | import org.springframework.security.web.authentication.AuthenticationSuccessHandler; 21 | import org.springframework.security.web.csrf.*; 22 | import org.springframework.security.web.session.HttpSessionEventPublisher; 23 | import org.springframework.util.StringUtils; 24 | import org.springframework.web.cors.CorsConfiguration; 25 | import org.springframework.web.cors.CorsConfigurationSource; 26 | import org.springframework.web.cors.UrlBasedCorsConfigurationSource; 27 | 28 | import java.util.function.Supplier; 29 | 30 | @EnableWebSecurity 31 | @Configuration(proxyBeanMethods = false) 32 | public class DefaultSecurityConfig { 33 | 34 | @Bean 35 | public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception { 36 | http 37 | .authorizeHttpRequests(authorize -> 38 | authorize 39 | .requestMatchers("/assets/**", "/login").permitAll() 40 | .anyRequest().authenticated() 41 | ) 42 | .cors(Customizer.withDefaults()) 43 | .csrf(csrf -> csrf 44 | .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) 45 | .csrfTokenRequestHandler(new SpaCsrfTokenRequestHandler()) 46 | ) 47 | .formLogin(formLogin -> 48 | formLogin 49 | .loginPage("/login") 50 | ) 51 | .oauth2Login(oauth2Login -> 52 | oauth2Login 53 | .loginPage("/login") 54 | .successHandler(authenticationSuccessHandler()) 55 | ) 56 | // .logout(logout -> logout 57 | // .logoutSuccessHandler(new HttpStatusReturningLogoutSuccessHandler()).permitAll() 58 | // ) 59 | // .exceptionHandling(customizer -> customizer 60 | // .authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED))); 61 | ; 62 | 63 | return http.build(); 64 | } 65 | 66 | private AuthenticationSuccessHandler authenticationSuccessHandler() { 67 | return new FederatedIdentityAuthenticationSuccessHandler(); 68 | } 69 | 70 | @Bean 71 | public UserDetailsService users(PasswordEncoder passwordEncoder) { 72 | UserDetails user = User.withUsername("user") 73 | .password(passwordEncoder.encode("111111")) 74 | .roles("USER") 75 | .build(); 76 | return new InMemoryUserDetailsManager(user); 77 | } 78 | 79 | @Bean 80 | PasswordEncoder passwordEncoder() { 81 | return PasswordEncoderFactories.createDelegatingPasswordEncoder(); 82 | } 83 | 84 | @Bean 85 | public CorsConfigurationSource corsConfigurationSource() { 86 | UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); 87 | CorsConfiguration config = new CorsConfiguration(); 88 | config.addAllowedHeader("*"); 89 | config.addAllowedMethod("*"); 90 | config.addAllowedOrigin("http://localhost:4200"); 91 | config.setAllowCredentials(true); 92 | source.registerCorsConfiguration("/**", config); 93 | return source; 94 | } 95 | 96 | @Bean 97 | public SessionRegistry sessionRegistry() { 98 | return new SessionRegistryImpl(); 99 | } 100 | 101 | @Bean 102 | public HttpSessionEventPublisher httpSessionEventPublisher() { 103 | return new HttpSessionEventPublisher(); 104 | } 105 | 106 | } 107 | 108 | final class SpaCsrfTokenRequestHandler implements CsrfTokenRequestHandler { 109 | private final CsrfTokenRequestHandler plain = new CsrfTokenRequestAttributeHandler(); 110 | private final CsrfTokenRequestHandler xor = new XorCsrfTokenRequestAttributeHandler(); 111 | 112 | @Override 113 | public void handle(HttpServletRequest request, HttpServletResponse response, Supplier csrfToken) { 114 | this.xor.handle(request, response, csrfToken); 115 | csrfToken.get(); 116 | } 117 | 118 | @Override 119 | public String resolveCsrfTokenValue(HttpServletRequest request, CsrfToken csrfToken) { 120 | String headerValue = request.getHeader(csrfToken.getHeaderName()); 121 | return (StringUtils.hasText(headerValue) ? this.plain : this.xor).resolveCsrfTokenValue(request, csrfToken); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /authserver/src/main/java/demo/federation/FederatedIdentityAuthenticationSuccessHandler.java: -------------------------------------------------------------------------------- 1 | package demo.federation; 2 | 3 | import jakarta.servlet.ServletException; 4 | import jakarta.servlet.http.HttpServletRequest; 5 | import jakarta.servlet.http.HttpServletResponse; 6 | import org.springframework.security.core.Authentication; 7 | import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; 8 | import org.springframework.security.oauth2.core.oidc.user.OidcUser; 9 | import org.springframework.security.oauth2.core.user.OAuth2User; 10 | import org.springframework.security.web.authentication.AuthenticationSuccessHandler; 11 | import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler; 12 | 13 | import java.io.IOException; 14 | import java.util.function.Consumer; 15 | 16 | public final class FederatedIdentityAuthenticationSuccessHandler implements AuthenticationSuccessHandler { 17 | 18 | private final AuthenticationSuccessHandler delegate = new SavedRequestAwareAuthenticationSuccessHandler(); 19 | 20 | private Consumer oauth2UserHandler = (user) -> { 21 | }; 22 | 23 | private Consumer oidcUserHandler = (user) -> this.oauth2UserHandler.accept(user); 24 | 25 | @Override 26 | public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) 27 | throws IOException, ServletException { 28 | if (authentication instanceof OAuth2AuthenticationToken) { 29 | if (authentication.getPrincipal() instanceof OidcUser) { 30 | this.oidcUserHandler.accept((OidcUser) authentication.getPrincipal()); 31 | } else if (authentication.getPrincipal() instanceof OAuth2User) { 32 | this.oauth2UserHandler.accept((OAuth2User) authentication.getPrincipal()); 33 | } 34 | } 35 | 36 | this.delegate.onAuthenticationSuccess(request, response, authentication); 37 | } 38 | 39 | public void setOAuth2UserHandler(Consumer oauth2UserHandler) { 40 | this.oauth2UserHandler = oauth2UserHandler; 41 | } 42 | 43 | public void setOidcUserHandler(Consumer oidcUserHandler) { 44 | this.oidcUserHandler = oidcUserHandler; 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /authserver/src/main/java/demo/federation/FederatedIdentityIdTokenCustomizer.java: -------------------------------------------------------------------------------- 1 | package demo.federation; 2 | 3 | import org.springframework.security.core.Authentication; 4 | import org.springframework.security.oauth2.core.oidc.IdTokenClaimNames; 5 | import org.springframework.security.oauth2.core.oidc.OidcIdToken; 6 | import org.springframework.security.oauth2.core.oidc.endpoint.OidcParameterNames; 7 | import org.springframework.security.oauth2.core.oidc.user.OidcUser; 8 | import org.springframework.security.oauth2.core.user.OAuth2User; 9 | import org.springframework.security.oauth2.server.authorization.token.JwtEncodingContext; 10 | import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenCustomizer; 11 | 12 | import java.util.*; 13 | 14 | public final class FederatedIdentityIdTokenCustomizer implements OAuth2TokenCustomizer { 15 | 16 | private static final Set ID_TOKEN_CLAIMS = Collections.unmodifiableSet(new HashSet<>(Arrays.asList( 17 | IdTokenClaimNames.ISS, 18 | IdTokenClaimNames.SUB, 19 | IdTokenClaimNames.AUD, 20 | IdTokenClaimNames.EXP, 21 | IdTokenClaimNames.IAT, 22 | IdTokenClaimNames.AUTH_TIME, 23 | IdTokenClaimNames.NONCE, 24 | IdTokenClaimNames.ACR, 25 | IdTokenClaimNames.AMR, 26 | IdTokenClaimNames.AZP, 27 | IdTokenClaimNames.AT_HASH, 28 | IdTokenClaimNames.C_HASH 29 | ))); 30 | 31 | @Override 32 | public void customize(JwtEncodingContext context) { 33 | if (OidcParameterNames.ID_TOKEN.equals(context.getTokenType().getValue())) { 34 | Map thirdPartyClaims = extractClaims(context.getPrincipal()); 35 | context.getClaims().claims(existingClaims -> { 36 | // Remove conflicting claims set by this authorization server 37 | existingClaims.keySet().forEach(thirdPartyClaims::remove); 38 | 39 | // Remove standard id_token claims that could cause problems with clients 40 | ID_TOKEN_CLAIMS.forEach(thirdPartyClaims::remove); 41 | 42 | // Add all other claims directly to id_token 43 | existingClaims.putAll(thirdPartyClaims); 44 | }); 45 | } 46 | } 47 | 48 | private Map extractClaims(Authentication principal) { 49 | Map claims; 50 | if (principal.getPrincipal() instanceof OidcUser) { 51 | OidcUser oidcUser = (OidcUser) principal.getPrincipal(); 52 | OidcIdToken idToken = oidcUser.getIdToken(); 53 | claims = idToken.getClaims(); 54 | } else if (principal.getPrincipal() instanceof OAuth2User) { 55 | OAuth2User oauth2User = (OAuth2User) principal.getPrincipal(); 56 | claims = oauth2User.getAttributes(); 57 | } else { 58 | claims = Collections.emptyMap(); 59 | } 60 | 61 | return new HashMap<>(claims); 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /authserver/src/main/java/demo/federation/UserRepositoryOAuth2UserHandler.java: -------------------------------------------------------------------------------- 1 | package demo.federation; 2 | 3 | import org.springframework.security.oauth2.core.user.OAuth2User; 4 | 5 | import java.util.Map; 6 | import java.util.concurrent.ConcurrentHashMap; 7 | import java.util.function.Consumer; 8 | 9 | public final class UserRepositoryOAuth2UserHandler implements Consumer { 10 | 11 | private final UserRepository userRepository = new UserRepository(); 12 | 13 | @Override 14 | public void accept(OAuth2User user) { 15 | // Capture user in a local data store on first authentication 16 | if (this.userRepository.findByName(user.getName()) == null) { 17 | System.out.println("Saving first-time user: name=" + user.getName() + ", claims=" + user.getAttributes() + ", authorities=" + user.getAuthorities()); 18 | this.userRepository.save(user); 19 | } 20 | } 21 | 22 | static class UserRepository { 23 | 24 | private final Map userCache = new ConcurrentHashMap<>(); 25 | 26 | public OAuth2User findByName(String name) { 27 | return this.userCache.get(name); 28 | } 29 | 30 | public void save(OAuth2User oauth2User) { 31 | this.userCache.put(oauth2User.getName(), oauth2User); 32 | } 33 | 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /authserver/src/main/java/demo/jose/Jwks.java: -------------------------------------------------------------------------------- 1 | package demo.jose; 2 | 3 | import com.nimbusds.jose.jwk.Curve; 4 | import com.nimbusds.jose.jwk.ECKey; 5 | import com.nimbusds.jose.jwk.OctetSequenceKey; 6 | import com.nimbusds.jose.jwk.RSAKey; 7 | 8 | import javax.crypto.SecretKey; 9 | import java.security.KeyPair; 10 | import java.security.interfaces.ECPrivateKey; 11 | import java.security.interfaces.ECPublicKey; 12 | import java.security.interfaces.RSAPrivateKey; 13 | import java.security.interfaces.RSAPublicKey; 14 | import java.util.UUID; 15 | 16 | public final class Jwks { 17 | 18 | private Jwks() { 19 | } 20 | 21 | public static RSAKey generateRsa() { 22 | KeyPair keyPair = KeyGeneratorUtils.generateRsaKey(); 23 | RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic(); 24 | RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate(); 25 | return new RSAKey.Builder(publicKey) 26 | .privateKey(privateKey) 27 | .keyID(UUID.randomUUID().toString()) 28 | .build(); 29 | } 30 | 31 | public static ECKey generateEc() { 32 | KeyPair keyPair = KeyGeneratorUtils.generateEcKey(); 33 | ECPublicKey publicKey = (ECPublicKey) keyPair.getPublic(); 34 | ECPrivateKey privateKey = (ECPrivateKey) keyPair.getPrivate(); 35 | Curve curve = Curve.forECParameterSpec(publicKey.getParams()); 36 | return new ECKey.Builder(curve, publicKey) 37 | .privateKey(privateKey) 38 | .keyID(UUID.randomUUID().toString()) 39 | .build(); 40 | } 41 | 42 | public static OctetSequenceKey generateSecret() { 43 | SecretKey secretKey = KeyGeneratorUtils.generateSecretKey(); 44 | return new OctetSequenceKey.Builder(secretKey) 45 | .keyID(UUID.randomUUID().toString()) 46 | .build(); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /authserver/src/main/java/demo/jose/KeyGeneratorUtils.java: -------------------------------------------------------------------------------- 1 | package demo.jose; 2 | 3 | import javax.crypto.KeyGenerator; 4 | import javax.crypto.SecretKey; 5 | import java.math.BigInteger; 6 | import java.security.KeyPair; 7 | import java.security.KeyPairGenerator; 8 | import java.security.spec.ECFieldFp; 9 | import java.security.spec.ECParameterSpec; 10 | import java.security.spec.ECPoint; 11 | import java.security.spec.EllipticCurve; 12 | 13 | final class KeyGeneratorUtils { 14 | 15 | private KeyGeneratorUtils() { 16 | } 17 | 18 | static SecretKey generateSecretKey() { 19 | SecretKey hmacKey; 20 | try { 21 | hmacKey = KeyGenerator.getInstance("HmacSha256").generateKey(); 22 | } catch (Exception ex) { 23 | throw new IllegalStateException(ex); 24 | } 25 | return hmacKey; 26 | } 27 | 28 | static KeyPair generateRsaKey() { 29 | KeyPair keyPair; 30 | try { 31 | KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA"); 32 | keyPairGenerator.initialize(2048); 33 | keyPair = keyPairGenerator.generateKeyPair(); 34 | } catch (Exception ex) { 35 | throw new IllegalStateException(ex); 36 | } 37 | return keyPair; 38 | } 39 | 40 | static KeyPair generateEcKey() { 41 | EllipticCurve ellipticCurve = new EllipticCurve( 42 | new ECFieldFp( 43 | new BigInteger("115792089210356248762697446949407573530086143415290314195533631308867097853951")), 44 | new BigInteger("115792089210356248762697446949407573530086143415290314195533631308867097853948"), 45 | new BigInteger("41058363725152142129326129780047268409114441015993725554835256314039467401291")); 46 | ECPoint ecPoint = new ECPoint( 47 | new BigInteger("48439561293906451759052585252797914202762949526041747995844080717082404635286"), 48 | new BigInteger("36134250956749795798585127919587881956611106672985015071877198253568414405109")); 49 | ECParameterSpec ecParameterSpec = new ECParameterSpec( 50 | ellipticCurve, 51 | ecPoint, 52 | new BigInteger("115792089210356248762697446949407573529996955224135760342422259061068512044369"), 53 | 1); 54 | 55 | KeyPair keyPair; 56 | try { 57 | KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("EC"); 58 | keyPairGenerator.initialize(ecParameterSpec); 59 | keyPair = keyPairGenerator.generateKeyPair(); 60 | } catch (Exception ex) { 61 | throw new IllegalStateException(ex); 62 | } 63 | return keyPair; 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /authserver/src/main/java/demo/web/AuthorizationConsentController.java: -------------------------------------------------------------------------------- 1 | package demo.web; 2 | 3 | import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; 4 | import org.springframework.security.oauth2.core.oidc.OidcScopes; 5 | import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsent; 6 | import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService; 7 | import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; 8 | import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository; 9 | import org.springframework.stereotype.Controller; 10 | import org.springframework.ui.Model; 11 | import org.springframework.util.StringUtils; 12 | import org.springframework.web.bind.annotation.GetMapping; 13 | import org.springframework.web.bind.annotation.RequestParam; 14 | 15 | import java.security.Principal; 16 | import java.util.*; 17 | 18 | @Controller 19 | public class AuthorizationConsentController { 20 | private final RegisteredClientRepository registeredClientRepository; 21 | private final OAuth2AuthorizationConsentService authorizationConsentService; 22 | 23 | public AuthorizationConsentController(RegisteredClientRepository registeredClientRepository, 24 | OAuth2AuthorizationConsentService authorizationConsentService) { 25 | this.registeredClientRepository = registeredClientRepository; 26 | this.authorizationConsentService = authorizationConsentService; 27 | } 28 | 29 | @GetMapping(value = "/oauth2/consent") 30 | public String consent( 31 | Principal principal, Model model, 32 | @RequestParam(OAuth2ParameterNames.CLIENT_ID) String clientId, 33 | @RequestParam(OAuth2ParameterNames.SCOPE) String scope, 34 | @RequestParam(OAuth2ParameterNames.STATE) String state, 35 | @RequestParam(name = OAuth2ParameterNames.USER_CODE, required = false) String userCode) { 36 | 37 | // Remove scopes that were already approved 38 | Set scopesToApprove = new HashSet<>(); 39 | Set previouslyApprovedScopes = new HashSet<>(); 40 | RegisteredClient registeredClient = this.registeredClientRepository.findByClientId(clientId); 41 | OAuth2AuthorizationConsent currentAuthorizationConsent = 42 | this.authorizationConsentService.findById(registeredClient.getId(), principal.getName()); 43 | Set authorizedScopes; 44 | if (currentAuthorizationConsent != null) { 45 | authorizedScopes = currentAuthorizationConsent.getScopes(); 46 | } else { 47 | authorizedScopes = Collections.emptySet(); 48 | } 49 | for (String requestedScope : StringUtils.delimitedListToStringArray(scope, " ")) { 50 | if (OidcScopes.OPENID.equals(requestedScope)) { 51 | continue; 52 | } 53 | if (authorizedScopes.contains(requestedScope)) { 54 | previouslyApprovedScopes.add(requestedScope); 55 | } else { 56 | scopesToApprove.add(requestedScope); 57 | } 58 | } 59 | 60 | model.addAttribute("clientId", clientId); 61 | model.addAttribute("state", state); 62 | model.addAttribute("scopes", withDescription(scopesToApprove)); 63 | model.addAttribute("previouslyApprovedScopes", withDescription(previouslyApprovedScopes)); 64 | model.addAttribute("principalName", principal.getName()); 65 | model.addAttribute("userCode", userCode); 66 | if (StringUtils.hasText(userCode)) { 67 | model.addAttribute("requestURI", "/oauth2/device_verification"); 68 | } else { 69 | model.addAttribute("requestURI", "/oauth2/authorize"); 70 | } 71 | 72 | return "consent"; 73 | } 74 | 75 | private static Set withDescription(Set scopes) { 76 | Set scopeWithDescriptions = new HashSet<>(); 77 | for (String scope : scopes) { 78 | scopeWithDescriptions.add(new ScopeWithDescription(scope)); 79 | 80 | } 81 | return scopeWithDescriptions; 82 | } 83 | 84 | public static class ScopeWithDescription { 85 | private static final String DEFAULT_DESCRIPTION = 86 | "UNKNOWN SCOPE - We cannot provide information about this permission, use caution when granting this."; 87 | private static final Map scopeDescriptions = new HashMap<>(); 88 | 89 | static { 90 | scopeDescriptions.put( 91 | OidcScopes.PROFILE, 92 | "This application will be able to read your profile information." 93 | ); 94 | scopeDescriptions.put( 95 | "message.read", 96 | "This application will be able to read your message." 97 | ); 98 | scopeDescriptions.put( 99 | "message.write", 100 | "This application will be able to add new messages. It will also be able to edit and delete existing messages." 101 | ); 102 | scopeDescriptions.put( 103 | "user.read", 104 | "This application will be able to read your user information." 105 | ); 106 | scopeDescriptions.put( 107 | "other.scope", 108 | "This is another scope example of a scope description." 109 | ); 110 | } 111 | 112 | public final String scope; 113 | public final String description; 114 | 115 | ScopeWithDescription(String scope) { 116 | this.scope = scope; 117 | this.description = scopeDescriptions.getOrDefault(scope, DEFAULT_DESCRIPTION); 118 | } 119 | } 120 | 121 | } 122 | -------------------------------------------------------------------------------- /authserver/src/main/java/demo/web/DefaultErrorController.java: -------------------------------------------------------------------------------- 1 | package demo.web; 2 | 3 | import jakarta.servlet.RequestDispatcher; 4 | import jakarta.servlet.http.HttpServletRequest; 5 | import org.springframework.boot.web.servlet.error.ErrorController; 6 | import org.springframework.stereotype.Controller; 7 | import org.springframework.ui.Model; 8 | import org.springframework.util.StringUtils; 9 | import org.springframework.web.bind.annotation.RequestMapping; 10 | 11 | @Controller 12 | public class DefaultErrorController implements ErrorController { 13 | 14 | @RequestMapping("/error") 15 | public String handleError(Model model, HttpServletRequest request) { 16 | String errorMessage = getErrorMessage(request); 17 | if (errorMessage.startsWith("[access_denied]")) { 18 | model.addAttribute("errorTitle", "Access Denied"); 19 | model.addAttribute("errorMessage", "You have denied access."); 20 | } else { 21 | model.addAttribute("errorTitle", "Error"); 22 | model.addAttribute("errorMessage", errorMessage); 23 | } 24 | return "error"; 25 | } 26 | 27 | private String getErrorMessage(HttpServletRequest request) { 28 | String errorMessage = (String) request.getAttribute(RequestDispatcher.ERROR_MESSAGE); 29 | return StringUtils.hasText(errorMessage) ? errorMessage : ""; 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /authserver/src/main/java/demo/web/DeviceController.java: -------------------------------------------------------------------------------- 1 | package demo.web; 2 | 3 | import org.springframework.stereotype.Controller; 4 | import org.springframework.web.bind.annotation.GetMapping; 5 | import org.springframework.web.bind.annotation.RequestParam; 6 | 7 | @Controller 8 | public class DeviceController { 9 | 10 | @GetMapping("/activate") 11 | public String activate(@RequestParam(value = "user_code", required = false) String userCode) { 12 | if (userCode != null) { 13 | return "redirect:/oauth2/device_verification?user_code=" + userCode; 14 | } 15 | return "device-activate"; 16 | } 17 | 18 | @GetMapping("/activated") 19 | public String activated() { 20 | return "device-activated"; 21 | } 22 | 23 | @GetMapping(value = "/", params = "success") 24 | public String success() { 25 | return "device-activated"; 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /authserver/src/main/java/demo/web/LoginController.java: -------------------------------------------------------------------------------- 1 | package demo.web; 2 | 3 | import org.springframework.stereotype.Controller; 4 | import org.springframework.web.bind.annotation.GetMapping; 5 | 6 | @Controller 7 | public class LoginController { 8 | 9 | @GetMapping("/login") 10 | public String login() { 11 | return "login"; 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /authserver/src/main/java/demo/web/authentication/DeviceClientAuthenticationConverter.java: -------------------------------------------------------------------------------- 1 | package demo.web.authentication; 2 | 3 | import demo.authentication.DeviceClientAuthenticationToken; 4 | import jakarta.servlet.http.HttpServletRequest; 5 | import org.springframework.http.HttpMethod; 6 | import org.springframework.lang.Nullable; 7 | import org.springframework.security.core.Authentication; 8 | import org.springframework.security.oauth2.core.AuthorizationGrantType; 9 | import org.springframework.security.oauth2.core.ClientAuthenticationMethod; 10 | import org.springframework.security.oauth2.core.OAuth2AuthenticationException; 11 | import org.springframework.security.oauth2.core.OAuth2ErrorCodes; 12 | import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; 13 | import org.springframework.security.web.authentication.AuthenticationConverter; 14 | import org.springframework.security.web.util.matcher.AndRequestMatcher; 15 | import org.springframework.security.web.util.matcher.AntPathRequestMatcher; 16 | import org.springframework.security.web.util.matcher.RequestMatcher; 17 | import org.springframework.util.StringUtils; 18 | 19 | public final class DeviceClientAuthenticationConverter implements AuthenticationConverter { 20 | private final RequestMatcher deviceAuthorizationRequestMatcher; 21 | private final RequestMatcher deviceAccessTokenRequestMatcher; 22 | 23 | public DeviceClientAuthenticationConverter(String deviceAuthorizationEndpointUri) { 24 | RequestMatcher clientIdParameterMatcher = request -> 25 | request.getParameter(OAuth2ParameterNames.CLIENT_ID) != null; 26 | this.deviceAuthorizationRequestMatcher = new AndRequestMatcher( 27 | new AntPathRequestMatcher( 28 | deviceAuthorizationEndpointUri, HttpMethod.POST.name()), 29 | clientIdParameterMatcher); 30 | this.deviceAccessTokenRequestMatcher = request -> 31 | AuthorizationGrantType.DEVICE_CODE.getValue().equals(request.getParameter(OAuth2ParameterNames.GRANT_TYPE)) && 32 | request.getParameter(OAuth2ParameterNames.DEVICE_CODE) != null && 33 | request.getParameter(OAuth2ParameterNames.CLIENT_ID) != null; 34 | } 35 | 36 | @Nullable 37 | @Override 38 | public Authentication convert(HttpServletRequest request) { 39 | if (!this.deviceAuthorizationRequestMatcher.matches(request) && 40 | !this.deviceAccessTokenRequestMatcher.matches(request)) { 41 | return null; 42 | } 43 | 44 | String clientId = request.getParameter(OAuth2ParameterNames.CLIENT_ID); 45 | if (!StringUtils.hasText(clientId) || 46 | request.getParameterValues(OAuth2ParameterNames.CLIENT_ID).length != 1) { 47 | throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_REQUEST); 48 | } 49 | 50 | return new DeviceClientAuthenticationToken(clientId, ClientAuthenticationMethod.NONE, null, null); 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /authserver/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | server: 2 | port: 8080 3 | 4 | spring: 5 | security: 6 | oauth2: 7 | client: 8 | registration: 9 | google-idp: 10 | provider: google 11 | client-id: ${GOOGLE_CLIENT_ID:google-client-id} 12 | client-secret: ${GOOGLE_CLIENT_SECRET:google-client-secret} 13 | scope: openid, https://www.googleapis.com/auth/userinfo.profile, https://www.googleapis.com/auth/userinfo.email 14 | client-name: Sign in with Google 15 | github-idp: 16 | provider: github 17 | client-id: ${GITHUB_CLIENT_ID:github-client-id} 18 | client-secret: ${GITHUB_CLIENT_SECRET:github-client-secret} 19 | scope: user:email, read:user 20 | client-name: Sign in with GitHub 21 | provider: 22 | google: 23 | user-name-attribute: email 24 | github: 25 | user-name-attribute: login 26 | 27 | logging: 28 | level: 29 | root: INFO 30 | -------------------------------------------------------------------------------- /authserver/src/main/resources/static/assets/css/signin.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | height: 100%; 4 | } 5 | 6 | body { 7 | display: flex; 8 | align-items: start; 9 | padding-top: 100px; 10 | background-color: #f5f5f5; 11 | } 12 | 13 | .form-signin { 14 | max-width: 330px; 15 | padding: 15px; 16 | } 17 | 18 | .form-signin .form-floating:focus-within { 19 | z-index: 2; 20 | } 21 | 22 | .form-signin input[type="username"] { 23 | margin-bottom: -1px; 24 | border-bottom-right-radius: 0; 25 | border-bottom-left-radius: 0; 26 | } 27 | 28 | .form-signin input[type="password"] { 29 | margin-bottom: 10px; 30 | border-top-left-radius: 0; 31 | border-top-right-radius: 0; 32 | } 33 | -------------------------------------------------------------------------------- /authserver/src/main/resources/static/assets/img/devices.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qihaiyan/ng-boot-oauth/9411a8fe0c41f8f4aa4bce3015bbddb1d74c3917/authserver/src/main/resources/static/assets/img/devices.png -------------------------------------------------------------------------------- /authserver/src/main/resources/static/assets/img/github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qihaiyan/ng-boot-oauth/9411a8fe0c41f8f4aa4bce3015bbddb1d74c3917/authserver/src/main/resources/static/assets/img/github.png -------------------------------------------------------------------------------- /authserver/src/main/resources/static/assets/img/google.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qihaiyan/ng-boot-oauth/9411a8fe0c41f8f4aa4bce3015bbddb1d74c3917/authserver/src/main/resources/static/assets/img/google.png -------------------------------------------------------------------------------- /authserver/src/main/resources/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Demo 7 | 8 | 9 | 10 | 11 |

Demo

12 |
13 | 14 | -------------------------------------------------------------------------------- /authserver/src/main/resources/templates/authorize.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | hiease authserver 5 | 6 | 7 |
8 |

Please Confirm

9 | 10 |

11 | Do you authorize at to access your 13 | protected resources 14 | with scope . 15 |

16 |
18 | 19 | 20 |
21 |
23 | 24 | 25 |
26 |
27 | 28 | -------------------------------------------------------------------------------- /authserver/src/main/resources/templates/consent.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Custom consent page - Consent required 7 | 8 | 14 | 15 | 16 |
17 |
18 |

App permissions

19 |
20 |
21 |
22 |

23 | The application 24 | 25 | wants to access your account 26 | 27 |

28 |
29 |
30 |
31 |
32 |

33 | You have provided the code 34 | . 35 | Verify that this code matches what is shown on your device. 36 |

37 |
38 |
39 |
40 |
41 |

42 | The following permissions are requested by the above app.
43 | Please review these and consent if you approve. 44 |

45 |
46 |
47 |
48 |
49 |
50 | 51 | 52 | 53 | 54 |
55 | 61 | 62 |

63 |
64 | 65 |

66 | You have already granted the following permissions to the above app: 67 |

68 |
69 | 75 | 76 |

77 |
78 | 79 |
80 | 83 |
84 |
85 | 88 |
89 |
90 |
91 |
92 |
93 |
94 |

95 | 96 | Your consent to provide access is required.
97 | If you do not approve, click Cancel, in which case no information will be shared with the app. 98 |
99 |

100 |
101 |
102 |
103 | 104 | 105 | -------------------------------------------------------------------------------- /authserver/src/main/resources/templates/device-activate.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Spring Authorization Server sample 7 | 8 | 9 | 10 |
11 |
12 |
13 |

Device Activation

14 |

Enter the activation code to authorize the device.

15 |
16 |
17 |
18 | 19 | 20 |
21 |
22 | 23 |
24 |
25 |
26 |
27 |
28 | Devices 29 |
30 |
31 |
32 | 33 | 34 | -------------------------------------------------------------------------------- /authserver/src/main/resources/templates/device-activated.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Spring Authorization Server sample 7 | 8 | 9 | 10 |
11 |
12 |
13 |

Success!

14 |

15 | You have successfully activated your device.
16 | Please return to your device to continue. 17 |

18 |
19 |
20 | Devices 21 |
22 |
23 |
24 | 25 | 26 | -------------------------------------------------------------------------------- /authserver/src/main/resources/templates/error.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Authorization Server 7 | 8 | 9 | 10 |
11 |
12 |
13 |

14 |

15 |
16 |
17 |
18 | 19 | 20 | -------------------------------------------------------------------------------- /authserver/src/main/resources/templates/login.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Authorization Server 7 | 8 | 9 | 10 | 11 |
12 | 40 |
41 | 42 | 43 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qihaiyan/ng-boot-oauth/9411a8fe0c41f8f4aa4bce3015bbddb1d74c3917/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip 4 | networkTimeout=10000 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn ( ) { 37 | echo "$*" 38 | } 39 | 40 | die ( ) { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 158 | function splitJvmOpts() { 159 | JVM_OPTS=("$@") 160 | } 161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 163 | 164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 165 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /resourceserver/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext { 3 | springBootVersion = '3.3.6' 4 | } 5 | repositories { 6 | mavenCentral() 7 | } 8 | dependencies { 9 | classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}") 10 | } 11 | } 12 | 13 | apply plugin: 'java' 14 | apply plugin: 'org.springframework.boot' 15 | apply plugin: 'io.spring.dependency-management' 16 | 17 | sourceCompatibility = 17 18 | targetCompatibility = 17 19 | 20 | configurations { 21 | compileOnly { 22 | extendsFrom annotationProcessor 23 | } 24 | testCompileOnly { 25 | extendsFrom testAnnotationProcessor 26 | } 27 | } 28 | 29 | repositories { 30 | mavenCentral() 31 | } 32 | 33 | dependencies { 34 | implementation("org.springframework.boot:spring-boot-starter-actuator") 35 | implementation("org.springframework.boot:spring-boot-starter-web") 36 | implementation("org.springframework.boot:spring-boot-starter-security") 37 | implementation('org.springframework.boot:spring-boot-starter-oauth2-resource-server') 38 | implementation "org.springframework.boot:spring-boot-starter-oauth2-client" 39 | annotationProcessor 'org.projectlombok:lombok' 40 | testAnnotationProcessor 'org.projectlombok:lombok' 41 | testImplementation("org.springframework.boot:spring-boot-starter-test") 42 | } 43 | -------------------------------------------------------------------------------- /resourceserver/src/main/java/demo/UiApplication.java: -------------------------------------------------------------------------------- 1 | package demo; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.web.bind.annotation.GetMapping; 6 | import org.springframework.web.bind.annotation.RestController; 7 | 8 | @SpringBootApplication 9 | @RestController 10 | public class UiApplication { 11 | 12 | @GetMapping("/messages") 13 | public String[] getMessages() { 14 | return new String[]{"Message 1", "Message 2", "Message 3"}; 15 | } 16 | 17 | public static void main(String[] args) { 18 | SpringApplication.run(UiApplication.class, args); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /resourceserver/src/main/java/demo/config/SecurityConfig.java: -------------------------------------------------------------------------------- 1 | package demo.config; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.security.config.Customizer; 6 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; 7 | import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; 8 | import org.springframework.security.web.SecurityFilterChain; 9 | import org.springframework.web.cors.CorsConfiguration; 10 | import org.springframework.web.cors.CorsConfigurationSource; 11 | import org.springframework.web.cors.UrlBasedCorsConfigurationSource; 12 | 13 | @Configuration 14 | @EnableWebSecurity 15 | public class SecurityConfig { 16 | @Bean 17 | SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { 18 | http 19 | .securityMatcher("/messages/**") 20 | .authorizeHttpRequests(authorize -> 21 | authorize.requestMatchers("/messages/**").authenticated() 22 | ) 23 | .cors(Customizer.withDefaults()) 24 | .oauth2ResourceServer(oauth2ResourceServer -> 25 | oauth2ResourceServer.jwt(Customizer.withDefaults()) 26 | ); 27 | return http.build(); 28 | } 29 | 30 | @Bean 31 | public CorsConfigurationSource corsConfigurationSource() { 32 | UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); 33 | CorsConfiguration config = new CorsConfiguration(); 34 | config.addAllowedHeader("*"); 35 | config.addAllowedMethod("*"); 36 | config.addAllowedOrigin("http://localhost:4200"); 37 | config.setAllowCredentials(true); 38 | source.registerCorsConfiguration("/**", config); 39 | return source; 40 | } 41 | } -------------------------------------------------------------------------------- /resourceserver/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | server: 2 | port: 8090 3 | 4 | logging: 5 | level: 6 | root: INFO 7 | 8 | spring: 9 | security: 10 | oauth2: 11 | resourceserver: 12 | jwt: 13 | jwk-set-uri: http://localhost:8080/oauth2/jwks 14 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include 'authserver' 2 | include 'ui' 3 | include 'resourceserver' 4 | 5 | -------------------------------------------------------------------------------- /ui-spa/.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.ts] 12 | quote_type = single 13 | ij_typescript_use_double_quotes = false 14 | 15 | [*.md] 16 | max_line_length = off 17 | trim_trailing_whitespace = false 18 | -------------------------------------------------------------------------------- /ui-spa/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["angular.ng-template"] 3 | } 4 | -------------------------------------------------------------------------------- /ui-spa/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "ng serve", 6 | "type": "chrome", 7 | "request": "launch", 8 | "preLaunchTask": "npm: start", 9 | "url": "http://localhost:4200/" 10 | }, 11 | { 12 | "name": "ng test", 13 | "type": "chrome", 14 | "request": "launch", 15 | "preLaunchTask": "npm: test", 16 | "url": "http://localhost:9876/debug.html" 17 | } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /ui-spa/.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "type": "npm", 6 | "script": "start", 7 | "isBackground": true, 8 | "problemMatcher": { 9 | "owner": "typescript", 10 | "pattern": "$tsc", 11 | "background": { 12 | "activeOnStart": true, 13 | "beginsPattern": { 14 | "regexp": "(.*?)" 15 | }, 16 | "endsPattern": { 17 | "regexp": "bundle generation complete" 18 | } 19 | } 20 | } 21 | }, 22 | { 23 | "type": "npm", 24 | "script": "test", 25 | "isBackground": true, 26 | "problemMatcher": { 27 | "owner": "typescript", 28 | "pattern": "$tsc", 29 | "background": { 30 | "activeOnStart": true, 31 | "beginsPattern": { 32 | "regexp": "(.*?)" 33 | }, 34 | "endsPattern": { 35 | "regexp": "bundle generation complete" 36 | } 37 | } 38 | } 39 | } 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /ui-spa/README.md: -------------------------------------------------------------------------------- 1 | # ui-spa 2 | 3 | This project was generated using [Angular CLI](https://github.com/angular/angular-cli) version 19.1.5. 4 | 5 | ## Development server 6 | 7 | To start a local development server, run: 8 | 9 | ```bash 10 | ng serve 11 | ``` 12 | 13 | Once the server is running, open your browser and navigate to `http://localhost:4200/`. The application will automatically reload whenever you modify any of the source files. 14 | 15 | ## Code scaffolding 16 | 17 | Angular CLI includes powerful code scaffolding tools. To generate a new component, run: 18 | 19 | ```bash 20 | ng generate component component-name 21 | ``` 22 | 23 | For a complete list of available schematics (such as `components`, `directives`, or `pipes`), run: 24 | 25 | ```bash 26 | ng generate --help 27 | ``` 28 | 29 | ## Building 30 | 31 | To build the project run: 32 | 33 | ```bash 34 | ng build 35 | ``` 36 | 37 | This will compile your project and store the build artifacts in the `dist/` directory. By default, the production build optimizes your application for performance and speed. 38 | 39 | ## Running unit tests 40 | 41 | To execute unit tests with the [Karma](https://karma-runner.github.io) test runner, use the following command: 42 | 43 | ```bash 44 | ng test 45 | ``` 46 | 47 | ## Running end-to-end tests 48 | 49 | For end-to-end (e2e) testing, run: 50 | 51 | ```bash 52 | ng e2e 53 | ``` 54 | 55 | Angular CLI does not come with an end-to-end testing framework by default. You can choose one that suits your needs. 56 | 57 | ## Additional Resources 58 | 59 | For more information on using the Angular CLI, including detailed command references, visit the [Angular CLI Overview and Command Reference](https://angular.dev/tools/cli) page. 60 | -------------------------------------------------------------------------------- /ui-spa/angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "ui-spa": { 7 | "projectType": "application", 8 | "schematics": {}, 9 | "root": "", 10 | "sourceRoot": "src", 11 | "prefix": "app", 12 | "architect": { 13 | "build": { 14 | "builder": "@angular-devkit/build-angular:application", 15 | "options": { 16 | "outputPath": "dist/ui-spa", 17 | "index": "src/index.html", 18 | "browser": "src/main.ts", 19 | "polyfills": [ 20 | "zone.js" 21 | ], 22 | "tsConfig": "tsconfig.app.json", 23 | "assets": [ 24 | { 25 | "glob": "**/*", 26 | "input": "public" 27 | } 28 | ], 29 | "styles": [ 30 | "src/styles.css" 31 | ], 32 | "scripts": [] 33 | }, 34 | "configurations": { 35 | "production": { 36 | "budgets": [ 37 | { 38 | "type": "initial", 39 | "maximumWarning": "500kB", 40 | "maximumError": "1MB" 41 | }, 42 | { 43 | "type": "anyComponentStyle", 44 | "maximumWarning": "4kB", 45 | "maximumError": "8kB" 46 | } 47 | ], 48 | "outputHashing": "all" 49 | }, 50 | "development": { 51 | "optimization": false, 52 | "extractLicenses": false, 53 | "sourceMap": true 54 | } 55 | }, 56 | "defaultConfiguration": "production" 57 | }, 58 | "serve": { 59 | "builder": "@angular-devkit/build-angular:dev-server", 60 | "configurations": { 61 | "production": { 62 | "buildTarget": "ui-spa:build:production" 63 | }, 64 | "development": { 65 | "buildTarget": "ui-spa:build:development" 66 | } 67 | }, 68 | "defaultConfiguration": "development" 69 | }, 70 | "extract-i18n": { 71 | "builder": "@angular-devkit/build-angular:extract-i18n" 72 | }, 73 | "test": { 74 | "builder": "@angular-devkit/build-angular:karma", 75 | "options": { 76 | "polyfills": [ 77 | "zone.js", 78 | "zone.js/testing" 79 | ], 80 | "tsConfig": "tsconfig.spec.json", 81 | "assets": [ 82 | { 83 | "glob": "**/*", 84 | "input": "public" 85 | } 86 | ], 87 | "styles": [ 88 | "src/styles.css" 89 | ], 90 | "scripts": [] 91 | } 92 | } 93 | } 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /ui-spa/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ui-spa", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "ng serve", 7 | "build": "ng build", 8 | "watch": "ng build --watch --configuration development", 9 | "test": "ng test" 10 | }, 11 | "private": true, 12 | "dependencies": { 13 | "@angular/animations": "^19.1.0", 14 | "@angular/common": "^19.1.0", 15 | "@angular/compiler": "^19.1.0", 16 | "@angular/core": "^19.1.0", 17 | "@angular/forms": "^19.1.0", 18 | "@angular/platform-browser": "^19.1.0", 19 | "@angular/platform-browser-dynamic": "^19.1.0", 20 | "@angular/router": "^19.1.0", 21 | "angular-auth-oidc-client": "19.0.0", 22 | "rxjs": "~7.8.0", 23 | "tslib": "^2.3.0", 24 | "zone.js": "~0.15.0" 25 | }, 26 | "devDependencies": { 27 | "@angular-devkit/build-angular": "^19.1.5", 28 | "@angular/cli": "^19.1.5", 29 | "@angular/compiler-cli": "^19.1.0", 30 | "@types/jasmine": "~5.1.0", 31 | "jasmine-core": "~5.5.0", 32 | "karma": "~6.4.0", 33 | "karma-chrome-launcher": "~3.2.0", 34 | "karma-coverage": "~2.2.0", 35 | "karma-jasmine": "~5.1.0", 36 | "karma-jasmine-html-reporter": "~2.1.0", 37 | "typescript": "~5.7.2" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /ui-spa/public/spring-security.svg: -------------------------------------------------------------------------------- 1 | logo-security -------------------------------------------------------------------------------- /ui-spa/src/app/app.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qihaiyan/ng-boot-oauth/9411a8fe0c41f8f4aa4bce3015bbddb1d74c3917/ui-spa/src/app/app.component.css -------------------------------------------------------------------------------- /ui-spa/src/app/app.component.html: -------------------------------------------------------------------------------- 1 |
2 | 34 |
35 |
36 |
37 |
38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 |
Messages
#Message
{{ index + 1 }}{{ message }}
53 |
54 |
55 |
56 | -------------------------------------------------------------------------------- /ui-spa/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, inject } from '@angular/core'; 2 | import { NgIf, NgForOf } from '@angular/common'; 3 | import { HttpClient } from '@angular/common/http'; 4 | import { catchError, of } from 'rxjs'; 5 | import { environment } from "./environment"; 6 | import { OidcSecurityService } from 'angular-auth-oidc-client'; 7 | 8 | @Component({ 9 | selector: 'app-root', 10 | standalone: true, 11 | imports: [NgIf, NgForOf], 12 | templateUrl: './app.component.html', 13 | styleUrl: './app.component.scss' 14 | }) 15 | export class AppComponent implements OnInit { 16 | private readonly oidcSecurityService = inject(OidcSecurityService); 17 | isAuthenticated: boolean = false; 18 | userName: string = ''; 19 | messages: string[] = []; 20 | 21 | constructor(private http: HttpClient) { 22 | } 23 | 24 | ngOnInit(): void { 25 | this.oidcSecurityService 26 | .checkAuth() 27 | .subscribe(({ isAuthenticated, accessToken }) => { 28 | console.log('app authenticated', isAuthenticated); 29 | console.log(`Current access token is '${accessToken}'`); 30 | this.isAuthenticated = isAuthenticated; 31 | }); 32 | // this.getUserInfo(); 33 | // this.getMessages(); 34 | } 35 | 36 | login(): void { 37 | // The Backend is configured to trigger login when unauthenticated 38 | // window.location.href = environment.backendBaseUrl; 39 | this.oidcSecurityService.authorize(); 40 | } 41 | 42 | logout(): void { 43 | this.oidcSecurityService.logoff().subscribe((result) => { 44 | console.log(result); 45 | this.isAuthenticated = false; 46 | }); 47 | } 48 | 49 | getUserInfo(): void { 50 | this.http.get('/userinfo') 51 | .pipe(catchError((error) => { 52 | console.error(error); 53 | return of(null); 54 | })) 55 | .subscribe((userInfo) => { 56 | if (userInfo) { 57 | this.isAuthenticated = true; 58 | this.userName = userInfo.sub; 59 | } 60 | }); 61 | } 62 | 63 | authorizeMessages(): void { 64 | // Trigger the Backend to perform the authorization_code grant flow. 65 | // After authorization is complete, the Backend will redirect back to this app. 66 | window.location.href = environment.backendBaseUrl + "/oauth2/authorization/messaging-client"; 67 | } 68 | 69 | getMessages(): void { 70 | this.http.get('http://localhost:8090/messages') 71 | .pipe(catchError((error) => { 72 | console.error(error); 73 | return of([]); 74 | })) 75 | .subscribe((messages) => { 76 | this.messages = messages; 77 | }); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /ui-spa/src/app/app.config.ts: -------------------------------------------------------------------------------- 1 | import { provideHttpClient, withInterceptors } from '@angular/common/http'; 2 | import { ApplicationConfig } from '@angular/core'; 3 | import { 4 | provideRouter, 5 | withEnabledBlockingInitialNavigation, 6 | } from '@angular/router'; 7 | import { 8 | LogLevel, 9 | authInterceptor, 10 | autoLoginPartialRoutesGuard, 11 | provideAuth, 12 | } from 'angular-auth-oidc-client'; 13 | import { routes } from './app.routes'; 14 | 15 | export const appConfig: ApplicationConfig = { 16 | providers: [ 17 | provideHttpClient(withInterceptors([authInterceptor()])), 18 | provideAuth({ 19 | config: { 20 | triggerAuthorizationResultEvent: true, 21 | postLoginRoute: '/', 22 | forbiddenRoute: '/forbidden', 23 | unauthorizedRoute: '/unauthorized', 24 | logLevel: LogLevel.Debug, 25 | historyCleanupOff: true, 26 | authority: 'http://localhost:8080', 27 | authWellknownEndpointUrl: 'http://localhost:8080/.well-known/openid-configuration', 28 | redirectUrl: window.location.origin, 29 | postLogoutRedirectUri: window.location.origin, 30 | clientId: 'spa-client', 31 | scope: 'openid profile email', 32 | responseType: 'code', 33 | silentRenew: true, 34 | useRefreshToken: true, 35 | secureRoutes: ['http://localhost:8090/'], 36 | }, 37 | }), 38 | provideRouter( 39 | routes, 40 | withEnabledBlockingInitialNavigation() 41 | ), 42 | ], 43 | }; 44 | -------------------------------------------------------------------------------- /ui-spa/src/app/app.routes.ts: -------------------------------------------------------------------------------- 1 | import { Routes } from '@angular/router'; 2 | 3 | export const routes: Routes = []; 4 | -------------------------------------------------------------------------------- /ui-spa/src/app/environment.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | backendBaseUrl: 'http://localhost:8080', 3 | }; 4 | -------------------------------------------------------------------------------- /ui-spa/src/app/http.interceptors.ts: -------------------------------------------------------------------------------- 1 | import {HttpRequest, HttpHandlerFn, HttpEvent} from '@angular/common/http'; 2 | import {Observable} from 'rxjs'; 3 | import {environment} from "./environment"; 4 | 5 | /* 6 | IMPORTANT: 7 | 8 | By default, the HttpClient passes the CSRF token via the X-XSRF-TOKEN header using its built-in interceptor. 9 | However, this DOES NOT WORK when absolute URLs are used in HttpClient calls. 10 | Hence, the reason for this interceptor, as it prepends the Backend base URL to the relative URL. 11 | Ensure you only use relative URLs in HttpClient calls for mutating requests (e.g. POST), 12 | otherwise operations such as /logout will not work. 13 | 14 | See the reference for further information: 15 | https://angular.dev/best-practices/security#httpclient-xsrf-csrf-security 16 | */ 17 | 18 | export function withCredentialsInterceptor(request: HttpRequest, next: HttpHandlerFn): Observable> { 19 | request = request.clone({ 20 | url: environment.backendBaseUrl + request.url, 21 | withCredentials: true // This is required to ensure the Session Cookie is passed in every request to the Backend 22 | }); 23 | return next(request); 24 | } 25 | -------------------------------------------------------------------------------- /ui-spa/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | SPA 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /ui-spa/src/main.ts: -------------------------------------------------------------------------------- 1 | import { bootstrapApplication } from '@angular/platform-browser'; 2 | import { appConfig } from './app/app.config'; 3 | import { AppComponent } from './app/app.component'; 4 | 5 | bootstrapApplication(AppComponent, appConfig) 6 | .catch((err) => console.error(err)); 7 | -------------------------------------------------------------------------------- /ui-spa/src/styles.css: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | -------------------------------------------------------------------------------- /ui-spa/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | /* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ 2 | /* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ 3 | { 4 | "extends": "./tsconfig.json", 5 | "compilerOptions": { 6 | "outDir": "./out-tsc/app", 7 | "types": [] 8 | }, 9 | "files": [ 10 | "src/main.ts" 11 | ], 12 | "include": [ 13 | "src/**/*.d.ts" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /ui-spa/tsconfig.json: -------------------------------------------------------------------------------- 1 | /* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ 2 | /* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ 3 | { 4 | "compileOnSave": false, 5 | "compilerOptions": { 6 | "outDir": "./dist/out-tsc", 7 | "strict": true, 8 | "noImplicitOverride": true, 9 | "noPropertyAccessFromIndexSignature": true, 10 | "noImplicitReturns": true, 11 | "noFallthroughCasesInSwitch": true, 12 | "skipLibCheck": true, 13 | "isolatedModules": true, 14 | "esModuleInterop": true, 15 | "experimentalDecorators": true, 16 | "moduleResolution": "bundler", 17 | "importHelpers": true, 18 | "target": "ES2022", 19 | "module": "ES2022" 20 | }, 21 | "angularCompilerOptions": { 22 | "enableI18nLegacyMessageIdFormat": false, 23 | "strictInjectionParameters": true, 24 | "strictInputAccessModifiers": true, 25 | "strictTemplates": true 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /ui-spa/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | /* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */ 2 | /* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */ 3 | { 4 | "extends": "./tsconfig.json", 5 | "compilerOptions": { 6 | "outDir": "./out-tsc/spec", 7 | "types": [ 8 | "jasmine" 9 | ] 10 | }, 11 | "include": [ 12 | "src/**/*.spec.ts", 13 | "src/**/*.d.ts" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /ui-spa/ui-spa.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /ui/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext { 3 | springBootVersion = '3.3.6' 4 | } 5 | repositories { 6 | mavenCentral() 7 | } 8 | dependencies { 9 | classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}") 10 | } 11 | } 12 | 13 | apply plugin: 'java' 14 | apply plugin: 'org.springframework.boot' 15 | apply plugin: 'io.spring.dependency-management' 16 | 17 | configurations { 18 | compileOnly { 19 | extendsFrom annotationProcessor 20 | } 21 | testCompileOnly { 22 | extendsFrom testAnnotationProcessor 23 | } 24 | } 25 | 26 | repositories { 27 | mavenCentral() 28 | } 29 | 30 | dependencies { 31 | implementation("org.springframework.boot:spring-boot-starter-actuator") 32 | implementation("org.springframework.boot:spring-boot-starter-web") 33 | implementation("org.springframework.boot:spring-boot-starter-security") 34 | implementation("org.springframework.boot:spring-boot-starter-thymeleaf") 35 | implementation('org.springframework.boot:spring-boot-starter-oauth2-resource-server') 36 | annotationProcessor 'org.projectlombok:lombok' 37 | testAnnotationProcessor 'org.projectlombok:lombok' 38 | testImplementation("org.springframework.boot:spring-boot-starter-test") 39 | } 40 | -------------------------------------------------------------------------------- /ui/src/main/java/demo/UiApplication.java: -------------------------------------------------------------------------------- 1 | package demo; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.boot.autoconfigure.security.oauth2.client.EnableOAuth2Sso; 6 | import org.springframework.web.bind.annotation.RequestMapping; 7 | import org.springframework.web.bind.annotation.RestController; 8 | 9 | import java.security.Principal; 10 | import java.util.LinkedHashMap; 11 | import java.util.Map; 12 | 13 | @SpringBootApplication 14 | @EnableOAuth2Sso 15 | @RestController 16 | public class UiApplication { 17 | 18 | @RequestMapping("/user") 19 | public Map user(Principal principal) { 20 | Map map = new LinkedHashMap<>(); 21 | map.put("name", principal.getName()); 22 | return map; 23 | } 24 | 25 | public static void main(String[] args) { 26 | SpringApplication.run(UiApplication.class, args); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /ui/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | aop: 3 | proxy-target-class: true 4 | security: 5 | basic: 6 | enabled: false 7 | oauth2: 8 | client: 9 | accessTokenUri: http://localhost:9999/uaa/oauth/token 10 | userAuthorizationUri: http://localhost:9999/uaa/oauth/authorize 11 | clientId: acme 12 | clientSecret: acmesecret 13 | resource: 14 | jwt: 15 | keyValue: | 16 | -----BEGIN PUBLIC KEY----- 17 | MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnGp/Q5lh0P8nPL21oMMrt2RrkT9AW5jgYwLfSUnJVc9G6uR3cXRRDCjHqWU5WYwivcF180A6CWp/ireQFFBNowgc5XaA0kPpzEtgsA5YsNX7iSnUibB004iBTfU9hZ2Rbsc8cWqynT0RyN4TP1RYVSeVKvMQk4GT1r7JCEC+TNu1ELmbNwMQyzKjsfBXyIOCFU/E94ktvsTZUHF4Oq44DBylCDsS1k7/sfZC2G5EU7Oz0mhG8+Uz6MSEQHtoIi6mc8u64Rwi3Z3tscuWG2ShtsUFuNSAFNkY7LkLn+/hxLCu2bNISMaESa8dG22CIMuIeRLVcAmEWEWH5EEforTg+QIDAQAB 18 | -----END PUBLIC KEY----- -------------------------------------------------------------------------------- /ui/src/main/resources/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | oauth2 demo 7 | 8 | 9 | 10 | 12 | 13 | 15 | 16 | 17 |
18 | Login 19 |
20 |
21 | Logged in as: 22 |
23 | 24 |
25 |
26 | 27 | 53 | 54 | --------------------------------------------------------------------------------