From 1e629d022c14922b313f9be17e8410df2c7fdb55 Mon Sep 17 00:00:00 2001 From: "chong-de.fang" Date: Mon, 11 Sep 2023 17:22:47 +0800 Subject: [PATCH] init --- auth-server/pom.xml | 60 ----- .../authserver/AuthServerApplication.java | 13 -- .../authserver/AuthorizationServerConfig.java | 14 -- .../authserver/ClientStoreConfig.java | 36 --- .../authserver/SecurityFilterConfig.java | 41 ---- .../authserver/TokenStoreConfig.java | 53 ----- .../authserver/UserStoreConfig.java | 23 -- .../src/main/resources/application.properties | 2 - .../AuthServerApplicationTests.java | 13 -- client-server/pom.xml | 56 ----- .../clientserver/ClientController.java | 52 ----- .../clientserver/ClientServerApplication.java | 13 -- .../src/main/resources/application.yml | 16 -- .../ClientServerApplicationTests.java | 13 -- .../.gitignore | 0 .../mvnw | 0 .../mvnw.cmd | 0 demo-authorizationserver/pom.xml | 90 ++++++++ .../DemoAuthorizationServerApplication.java | 32 +++ .../DeviceClientAuthenticationProvider.java | 103 +++++++++ .../DeviceClientAuthenticationToken.java | 44 ++++ .../config/AuthorizationServerConfig.java | 215 ++++++++++++++++++ .../sample/config/DefaultSecurityConfig.java | 114 ++++++++++ ...dIdentityAuthenticationSuccessHandler.java | 72 ++++++ .../FederatedIdentityIdTokenCustomizer.java | 95 ++++++++ .../UserRepositoryOAuth2UserHandler.java | 61 +++++ .../src/main/java/sample/jose/Jwks.java | 75 ++++++ .../java/sample/jose/KeyGeneratorUtils.java | 86 +++++++ .../web/AuthorizationConsentController.java | 136 +++++++++++ .../sample/web/DefaultErrorController.java | 52 +++++ .../java/sample/web/DeviceController.java | 47 ++++ .../main/java/sample/web/LoginController.java | 33 +++ .../DeviceClientAuthenticationConverter.java | 76 +++++++ .../src/main/resources/application.yml | 52 +++++ .../resources/static/assets/css/signin.css | 32 +++ .../resources/static/assets/img/devices.png | Bin 0 -> 19071 bytes .../resources/static/assets/img/gitee.png | Bin 0 -> 14158 bytes .../resources/static/assets/img/github.png | Bin 0 -> 7249 bytes .../resources/static/assets/img/google.png | Bin 0 -> 27629 bytes .../src/main/resources/templates/consent.html | 104 +++++++++ .../resources/templates/device-activate.html | 33 +++ .../resources/templates/device-activated.html | 25 ++ .../src/main/resources/templates/error.html | 19 ++ .../src/main/resources/templates/login.html | 42 ++++ {client-server => demo-client}/.gitignore | 0 {client-server => demo-client}/mvnw | 0 {client-server => demo-client}/mvnw.cmd | 0 demo-client/pom.xml | 89 ++++++++ .../java/sample/DemoClientApplication.java | 32 +++ ...iceCodeOAuth2AuthorizedClientProvider.java | 119 ++++++++++ ...OAuth2DeviceAccessTokenResponseClient.java | 99 ++++++++ .../OAuth2DeviceGrantRequest.java | 41 ++++ .../java/sample/config/SecurityConfig.java | 78 +++++++ .../java/sample/config/WebClientConfig.java | 76 +++++++ .../sample/web/AuthorizationController.java | 110 +++++++++ .../java/sample/web/DefaultController.java | 44 ++++ .../java/sample/web/DeviceController.java | 192 ++++++++++++++++ .../src/main/resources/application.yml | 54 +++++ .../resources/static/assets/img/devices.png | Bin 0 -> 19071 bytes .../static/assets/img/spring-security.svg | 1 + .../resources/templates/device-activate.html | 27 +++ .../resources/templates/device-authorize.html | 87 +++++++ .../src/main/resources/templates/index.html | 19 ++ .../main/resources/templates/logged-out.html | 22 ++ .../resources/templates/page-templates.html | 69 ++++++ .../.gitignore | 0 {resource-server => messages-resource}/mvnw | 0 .../mvnw.cmd | 0 messages-resource/pom.xml | 59 +++++ .../sample/MessagesResourceApplication.java | 32 +++ .../sample/config/ResourceServerConfig.java | 63 +++++ .../java/sample/web/MessagesController.java | 32 +++ .../src/main/resources/application.yml | 17 ++ pom.xml | 44 ++++ resource-server/pom.xml | 60 ----- .../ResourceServerApplication.java | 13 -- .../resourceserver/TasksController.java | 27 --- .../src/main/resources/application.yml | 8 - .../ResourceServerApplicationTests.java | 13 -- 79 files changed, 3044 insertions(+), 526 deletions(-) delete mode 100644 auth-server/pom.xml delete mode 100644 auth-server/src/main/java/com/leonardozw/authserver/AuthServerApplication.java delete mode 100644 auth-server/src/main/java/com/leonardozw/authserver/AuthorizationServerConfig.java delete mode 100644 auth-server/src/main/java/com/leonardozw/authserver/ClientStoreConfig.java delete mode 100644 auth-server/src/main/java/com/leonardozw/authserver/SecurityFilterConfig.java delete mode 100644 auth-server/src/main/java/com/leonardozw/authserver/TokenStoreConfig.java delete mode 100644 auth-server/src/main/java/com/leonardozw/authserver/UserStoreConfig.java delete mode 100644 auth-server/src/main/resources/application.properties delete mode 100644 auth-server/src/test/java/com/leonardozw/authserver/AuthServerApplicationTests.java delete mode 100644 client-server/pom.xml delete mode 100644 client-server/src/main/java/com/leonardozw/clientserver/ClientController.java delete mode 100644 client-server/src/main/java/com/leonardozw/clientserver/ClientServerApplication.java delete mode 100644 client-server/src/main/resources/application.yml delete mode 100644 client-server/src/test/java/com/leonardozw/clientserver/ClientServerApplicationTests.java rename {auth-server => demo-authorizationserver}/.gitignore (100%) rename {auth-server => demo-authorizationserver}/mvnw (100%) rename {auth-server => demo-authorizationserver}/mvnw.cmd (100%) create mode 100644 demo-authorizationserver/pom.xml create mode 100644 demo-authorizationserver/src/main/java/sample/DemoAuthorizationServerApplication.java create mode 100644 demo-authorizationserver/src/main/java/sample/authentication/DeviceClientAuthenticationProvider.java create mode 100644 demo-authorizationserver/src/main/java/sample/authentication/DeviceClientAuthenticationToken.java create mode 100644 demo-authorizationserver/src/main/java/sample/config/AuthorizationServerConfig.java create mode 100644 demo-authorizationserver/src/main/java/sample/config/DefaultSecurityConfig.java create mode 100644 demo-authorizationserver/src/main/java/sample/federation/FederatedIdentityAuthenticationSuccessHandler.java create mode 100644 demo-authorizationserver/src/main/java/sample/federation/FederatedIdentityIdTokenCustomizer.java create mode 100644 demo-authorizationserver/src/main/java/sample/federation/UserRepositoryOAuth2UserHandler.java create mode 100644 demo-authorizationserver/src/main/java/sample/jose/Jwks.java create mode 100644 demo-authorizationserver/src/main/java/sample/jose/KeyGeneratorUtils.java create mode 100644 demo-authorizationserver/src/main/java/sample/web/AuthorizationConsentController.java create mode 100644 demo-authorizationserver/src/main/java/sample/web/DefaultErrorController.java create mode 100644 demo-authorizationserver/src/main/java/sample/web/DeviceController.java create mode 100644 demo-authorizationserver/src/main/java/sample/web/LoginController.java create mode 100644 demo-authorizationserver/src/main/java/sample/web/authentication/DeviceClientAuthenticationConverter.java create mode 100644 demo-authorizationserver/src/main/resources/application.yml create mode 100644 demo-authorizationserver/src/main/resources/static/assets/css/signin.css create mode 100644 demo-authorizationserver/src/main/resources/static/assets/img/devices.png create mode 100644 demo-authorizationserver/src/main/resources/static/assets/img/gitee.png create mode 100644 demo-authorizationserver/src/main/resources/static/assets/img/github.png create mode 100644 demo-authorizationserver/src/main/resources/static/assets/img/google.png create mode 100644 demo-authorizationserver/src/main/resources/templates/consent.html create mode 100644 demo-authorizationserver/src/main/resources/templates/device-activate.html create mode 100644 demo-authorizationserver/src/main/resources/templates/device-activated.html create mode 100644 demo-authorizationserver/src/main/resources/templates/error.html create mode 100644 demo-authorizationserver/src/main/resources/templates/login.html rename {client-server => demo-client}/.gitignore (100%) rename {client-server => demo-client}/mvnw (100%) rename {client-server => demo-client}/mvnw.cmd (100%) create mode 100644 demo-client/pom.xml create mode 100644 demo-client/src/main/java/sample/DemoClientApplication.java create mode 100644 demo-client/src/main/java/sample/authorization/DeviceCodeOAuth2AuthorizedClientProvider.java create mode 100644 demo-client/src/main/java/sample/authorization/OAuth2DeviceAccessTokenResponseClient.java create mode 100644 demo-client/src/main/java/sample/authorization/OAuth2DeviceGrantRequest.java create mode 100644 demo-client/src/main/java/sample/config/SecurityConfig.java create mode 100644 demo-client/src/main/java/sample/config/WebClientConfig.java create mode 100644 demo-client/src/main/java/sample/web/AuthorizationController.java create mode 100644 demo-client/src/main/java/sample/web/DefaultController.java create mode 100644 demo-client/src/main/java/sample/web/DeviceController.java create mode 100644 demo-client/src/main/resources/application.yml create mode 100644 demo-client/src/main/resources/static/assets/img/devices.png create mode 100644 demo-client/src/main/resources/static/assets/img/spring-security.svg create mode 100644 demo-client/src/main/resources/templates/device-activate.html create mode 100644 demo-client/src/main/resources/templates/device-authorize.html create mode 100644 demo-client/src/main/resources/templates/index.html create mode 100644 demo-client/src/main/resources/templates/logged-out.html create mode 100644 demo-client/src/main/resources/templates/page-templates.html rename {resource-server => messages-resource}/.gitignore (100%) rename {resource-server => messages-resource}/mvnw (100%) rename {resource-server => messages-resource}/mvnw.cmd (100%) create mode 100644 messages-resource/pom.xml create mode 100644 messages-resource/src/main/java/sample/MessagesResourceApplication.java create mode 100644 messages-resource/src/main/java/sample/config/ResourceServerConfig.java create mode 100644 messages-resource/src/main/java/sample/web/MessagesController.java create mode 100644 messages-resource/src/main/resources/application.yml create mode 100644 pom.xml delete mode 100644 resource-server/pom.xml delete mode 100644 resource-server/src/main/java/com/leonardozw/resourceserver/ResourceServerApplication.java delete mode 100644 resource-server/src/main/java/com/leonardozw/resourceserver/TasksController.java delete mode 100644 resource-server/src/main/resources/application.yml delete mode 100644 resource-server/src/test/java/com/leonardozw/resourceserver/ResourceServerApplicationTests.java diff --git a/auth-server/pom.xml b/auth-server/pom.xml deleted file mode 100644 index 463afe6..0000000 --- a/auth-server/pom.xml +++ /dev/null @@ -1,60 +0,0 @@ - - - 4.0.0 - - org.springframework.boot - spring-boot-starter-parent - 3.1.3 - - - com.leonardozw - auth-server - 0.0.1-SNAPSHOT - auth-server - Demo project for Spring Boot - - 17 - - - - org.springframework.boot - spring-boot-starter-security - - - org.springframework.boot - spring-boot-starter-web - - - org.springframework.boot - spring-boot-starter-oauth2-authorization-server - - - - org.springframework.boot - spring-boot-devtools - runtime - true - - - org.springframework.boot - spring-boot-starter-test - test - - - org.springframework.security - spring-security-test - test - - - - - - - org.springframework.boot - spring-boot-maven-plugin - - - - - diff --git a/auth-server/src/main/java/com/leonardozw/authserver/AuthServerApplication.java b/auth-server/src/main/java/com/leonardozw/authserver/AuthServerApplication.java deleted file mode 100644 index 6dde66a..0000000 --- a/auth-server/src/main/java/com/leonardozw/authserver/AuthServerApplication.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.leonardozw.authserver; - -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; - -@SpringBootApplication -public class AuthServerApplication { - - public static void main(String[] args) { - SpringApplication.run(AuthServerApplication.class, args); - } - -} diff --git a/auth-server/src/main/java/com/leonardozw/authserver/AuthorizationServerConfig.java b/auth-server/src/main/java/com/leonardozw/authserver/AuthorizationServerConfig.java deleted file mode 100644 index 5134505..0000000 --- a/auth-server/src/main/java/com/leonardozw/authserver/AuthorizationServerConfig.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.leonardozw.authserver; - -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings; - -@Configuration -public class AuthorizationServerConfig { - - @Bean - AuthorizationServerSettings authorizationServerSettings(){ - return AuthorizationServerSettings.builder().build(); - } -} diff --git a/auth-server/src/main/java/com/leonardozw/authserver/ClientStoreConfig.java b/auth-server/src/main/java/com/leonardozw/authserver/ClientStoreConfig.java deleted file mode 100644 index 3bedade..0000000 --- a/auth-server/src/main/java/com/leonardozw/authserver/ClientStoreConfig.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.leonardozw.authserver; - -import java.util.UUID; - -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.security.oauth2.core.AuthorizationGrantType; -import org.springframework.security.oauth2.core.ClientAuthenticationMethod; -import org.springframework.security.oauth2.core.oidc.OidcScopes; -import org.springframework.security.oauth2.server.authorization.client.InMemoryRegisteredClientRepository; -import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; -import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository; -import org.springframework.security.oauth2.server.authorization.settings.ClientSettings; - -@Configuration -public class ClientStoreConfig { - - @Bean - RegisteredClientRepository registeredClientRepository(){ - var registerClient = RegisteredClient. - withId(UUID.randomUUID().toString()) - .clientId("client-server") - .clientSecret("{noop}secret") - .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC) - .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) - .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN) - .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS) - .redirectUri("http://127.0.0.1:8080/login/oauth2/code/client-server-oidc") - .scope(OidcScopes.OPENID) - .scope(OidcScopes.PROFILE) - .clientSettings(ClientSettings.builder() - .requireAuthorizationConsent(true).build()) - .build(); - return new InMemoryRegisteredClientRepository(registerClient); - } -} diff --git a/auth-server/src/main/java/com/leonardozw/authserver/SecurityFilterConfig.java b/auth-server/src/main/java/com/leonardozw/authserver/SecurityFilterConfig.java deleted file mode 100644 index f6cbc76..0000000 --- a/auth-server/src/main/java/com/leonardozw/authserver/SecurityFilterConfig.java +++ /dev/null @@ -1,41 +0,0 @@ -package com.leonardozw.authserver; - -import static org.springframework.security.config.Customizer.*; - -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.core.annotation.Order; -import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer; -import org.springframework.security.oauth2.server.authorization.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration; -import org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer; -import org.springframework.security.web.SecurityFilterChain; -import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint; - -@Configuration -public class SecurityFilterConfig { - - - @Bean - @Order(1) - SecurityFilterChain authServerSecurityFilterChain(HttpSecurity http) throws Exception{ - OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http); - - http.getConfigurer(OAuth2AuthorizationServerConfigurer.class).oidc( - withDefaults()) - .and() - .exceptionHandling((exceptions) -> exceptions. - authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/login"))) - .oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt); - - return http.build(); - } - - @Bean - @Order(2) - SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception { - http.authorizeHttpRequests((authorize) -> authorize.anyRequest().authenticated()) - .formLogin(withDefaults()); - return http.build(); - } -} diff --git a/auth-server/src/main/java/com/leonardozw/authserver/TokenStoreConfig.java b/auth-server/src/main/java/com/leonardozw/authserver/TokenStoreConfig.java deleted file mode 100644 index b0e8972..0000000 --- a/auth-server/src/main/java/com/leonardozw/authserver/TokenStoreConfig.java +++ /dev/null @@ -1,53 +0,0 @@ -package com.leonardozw.authserver; - -import java.security.KeyPair; -import java.security.KeyPairGenerator; -import java.security.interfaces.RSAPrivateKey; -import java.security.interfaces.RSAPublicKey; -import java.util.UUID; - -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.security.oauth2.jwt.JwtDecoder; -import org.springframework.security.oauth2.server.authorization.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration; - -import com.nimbusds.jose.jwk.JWKSet; -import com.nimbusds.jose.jwk.RSAKey; -import com.nimbusds.jose.jwk.source.ImmutableJWKSet; -import com.nimbusds.jose.jwk.source.JWKSource; -import com.nimbusds.jose.proc.SecurityContext; - -@Configuration -public class TokenStoreConfig { - - @Bean - JWKSource jwkSource() { - KeyPair keyPair = generateRsaKey(); - RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic(); - RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate(); - RSAKey rsaKey = new RSAKey.Builder(publicKey) - .privateKey(privateKey) - .keyID(UUID.randomUUID().toString()) - .build(); - JWKSet jwkSet = new JWKSet(rsaKey); - return new ImmutableJWKSet<>(jwkSet); - } - - @Bean - JwtDecoder jwtDecoder(JWKSource jwkSource) { - return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource); - } - - private static KeyPair generateRsaKey() { - KeyPair keyPair; - try { - KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA"); - keyPairGenerator.initialize(2048); - keyPair = keyPairGenerator.generateKeyPair(); - } catch (Exception ex) { - throw new IllegalStateException(ex); - } - return keyPair; - } - -} diff --git a/auth-server/src/main/java/com/leonardozw/authserver/UserStoreConfig.java b/auth-server/src/main/java/com/leonardozw/authserver/UserStoreConfig.java deleted file mode 100644 index c3f8b04..0000000 --- a/auth-server/src/main/java/com/leonardozw/authserver/UserStoreConfig.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.leonardozw.authserver; - -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.security.core.userdetails.User; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.provisioning.InMemoryUserDetailsManager; - -@Configuration -public class UserStoreConfig { - - @Bean - UserDetailsService userDetailsService(){ - var userDatailsManager = new InMemoryUserDetailsManager(); - userDatailsManager.createUser( - User.withUsername("user") - .password("{noop}password") - .roles("USER") - .build() - ); - return userDatailsManager; - } -} diff --git a/auth-server/src/main/resources/application.properties b/auth-server/src/main/resources/application.properties deleted file mode 100644 index f31ae76..0000000 --- a/auth-server/src/main/resources/application.properties +++ /dev/null @@ -1,2 +0,0 @@ -server.port=9000 -logging.level.org.springframework.security=trace diff --git a/auth-server/src/test/java/com/leonardozw/authserver/AuthServerApplicationTests.java b/auth-server/src/test/java/com/leonardozw/authserver/AuthServerApplicationTests.java deleted file mode 100644 index 04ce85d..0000000 --- a/auth-server/src/test/java/com/leonardozw/authserver/AuthServerApplicationTests.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.leonardozw.authserver; - -import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; - -@SpringBootTest -class AuthServerApplicationTests { - - @Test - void contextLoads() { - } - -} diff --git a/client-server/pom.xml b/client-server/pom.xml deleted file mode 100644 index a2babb2..0000000 --- a/client-server/pom.xml +++ /dev/null @@ -1,56 +0,0 @@ - - - 4.0.0 - - org.springframework.boot - spring-boot-starter-parent - 3.1.3 - - - com.leonardozw - client-server - 0.0.1-SNAPSHOT - client-server - Demo project for Spring Boot - - 17 - - - - org.springframework.boot - spring-boot-starter-oauth2-client - - - org.springframework.boot - spring-boot-starter-webflux - - - - org.springframework.boot - spring-boot-devtools - runtime - true - - - org.springframework.boot - spring-boot-starter-test - test - - - io.projectreactor - reactor-test - test - - - - - - - org.springframework.boot - spring-boot-maven-plugin - - - - - diff --git a/client-server/src/main/java/com/leonardozw/clientserver/ClientController.java b/client-server/src/main/java/com/leonardozw/clientserver/ClientController.java deleted file mode 100644 index 353903f..0000000 --- a/client-server/src/main/java/com/leonardozw/clientserver/ClientController.java +++ /dev/null @@ -1,52 +0,0 @@ -package com.leonardozw.clientserver; - -import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; -import org.springframework.security.oauth2.client.annotation.RegisteredOAuth2AuthorizedClient; -import org.springframework.security.oauth2.core.oidc.user.OidcUser; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RestController; -import org.springframework.web.reactive.function.client.WebClient; - -import reactor.core.publisher.Mono; - -@RestController -public class ClientController { - - WebClient webClient; - - public ClientController(WebClient.Builder builder) { - this.webClient = builder - .baseUrl("http://127.0.0.1:9090") - .build(); - } - - @GetMapping("home") - public Mono home( - @RegisteredOAuth2AuthorizedClient OAuth2AuthorizedClient client, - @AuthenticationPrincipal OidcUser user){ - return Mono.just(""" -

Access Token: %s

-

Refresh Token: %s

-

Id Token: %s

-

Claims: %s

- """.formatted( - client.getAccessToken().getTokenValue(), - client.getRefreshToken().getTokenValue(), - user.getIdToken().getTokenValue(), - user.getClaims() - )); - } - - @GetMapping("tasks") - public Mono getTasks( - @RegisteredOAuth2AuthorizedClient OAuth2AuthorizedClient client){ - return webClient.get() - .uri("tasks") - .header("Authorization", "Bearer %s".formatted( - client.getAccessToken().getTokenValue() - )) - .retrieve() - .bodyToMono(String.class); - } -} diff --git a/client-server/src/main/java/com/leonardozw/clientserver/ClientServerApplication.java b/client-server/src/main/java/com/leonardozw/clientserver/ClientServerApplication.java deleted file mode 100644 index 2915284..0000000 --- a/client-server/src/main/java/com/leonardozw/clientserver/ClientServerApplication.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.leonardozw.clientserver; - -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; - -@SpringBootApplication -public class ClientServerApplication { - - public static void main(String[] args) { - SpringApplication.run(ClientServerApplication.class, args); - } - -} diff --git a/client-server/src/main/resources/application.yml b/client-server/src/main/resources/application.yml deleted file mode 100644 index 512f2ed..0000000 --- a/client-server/src/main/resources/application.yml +++ /dev/null @@ -1,16 +0,0 @@ -spring: - security: - oauth2: - client: - registration: - client-server-oidc: - provider: spring - client-id: client-server - client-secret: secret - authorization-grant-type: authorization_code - redirect-uri: "http://127.0.0.1:8080/login/oauth2/code/{registrationId}" - scope: openid,profile - client-name: client-server-oidc - provider: - spring: - issuer-uri: http://localhost:9000 diff --git a/client-server/src/test/java/com/leonardozw/clientserver/ClientServerApplicationTests.java b/client-server/src/test/java/com/leonardozw/clientserver/ClientServerApplicationTests.java deleted file mode 100644 index ad6b7ec..0000000 --- a/client-server/src/test/java/com/leonardozw/clientserver/ClientServerApplicationTests.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.leonardozw.clientserver; - -import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; - -@SpringBootTest -class ClientServerApplicationTests { - - @Test - void contextLoads() { - } - -} diff --git a/auth-server/.gitignore b/demo-authorizationserver/.gitignore similarity index 100% rename from auth-server/.gitignore rename to demo-authorizationserver/.gitignore diff --git a/auth-server/mvnw b/demo-authorizationserver/mvnw similarity index 100% rename from auth-server/mvnw rename to demo-authorizationserver/mvnw diff --git a/auth-server/mvnw.cmd b/demo-authorizationserver/mvnw.cmd similarity index 100% rename from auth-server/mvnw.cmd rename to demo-authorizationserver/mvnw.cmd diff --git a/demo-authorizationserver/pom.xml b/demo-authorizationserver/pom.xml new file mode 100644 index 0000000..574932c --- /dev/null +++ b/demo-authorizationserver/pom.xml @@ -0,0 +1,90 @@ + + + + sample-demo + com.sample.demo + 1.0-SNAPSHOT + + 4.0.0 + + demo-authorizationserver + + + + + org.springframework.security + spring-security-oauth2-authorization-server + + + + org.springframework.boot + spring-boot-starter-oauth2-client + + + + org.springframework.boot + spring-boot-starter-security + + + + org.springframework.boot + spring-boot-starter-thymeleaf + + + + org.springframework.boot + spring-boot-starter-jdbc + + + + org.webjars + webjars-locator-core + + + + + org.webjars + bootstrap + 5.2.3 + + + + org.webjars + jquery + 3.6.4 + + + + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-tomcat + + + + + + + org.springframework.boot + spring-boot-starter-undertow + + + + com.h2database + h2 + + + + + + + + \ No newline at end of file diff --git a/demo-authorizationserver/src/main/java/sample/DemoAuthorizationServerApplication.java b/demo-authorizationserver/src/main/java/sample/DemoAuthorizationServerApplication.java new file mode 100644 index 0000000..88d788b --- /dev/null +++ b/demo-authorizationserver/src/main/java/sample/DemoAuthorizationServerApplication.java @@ -0,0 +1,32 @@ +/* + * Copyright 2020-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package sample; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * @author Joe Grandja + * @since 1.1 + */ +@SpringBootApplication +public class DemoAuthorizationServerApplication { + + public static void main(String[] args) { + SpringApplication.run(DemoAuthorizationServerApplication.class, args); + } + +} diff --git a/demo-authorizationserver/src/main/java/sample/authentication/DeviceClientAuthenticationProvider.java b/demo-authorizationserver/src/main/java/sample/authentication/DeviceClientAuthenticationProvider.java new file mode 100644 index 0000000..2ba2668 --- /dev/null +++ b/demo-authorizationserver/src/main/java/sample/authentication/DeviceClientAuthenticationProvider.java @@ -0,0 +1,103 @@ +/* + * Copyright 2020-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package sample.authentication; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import sample.web.authentication.DeviceClientAuthenticationConverter; + +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.oauth2.core.ClientAuthenticationMethod; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.OAuth2Error; +import org.springframework.security.oauth2.core.OAuth2ErrorCodes; +import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; +import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; +import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository; +import org.springframework.security.oauth2.server.authorization.web.OAuth2ClientAuthenticationFilter; +import org.springframework.util.Assert; + +/** + * @author Joe Grandja + * @author Steve Riesenberg + * @since 1.1 + * @see DeviceClientAuthenticationToken + * @see DeviceClientAuthenticationConverter + * @see OAuth2ClientAuthenticationFilter + */ +public final class DeviceClientAuthenticationProvider implements AuthenticationProvider { + private static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-3.2.1"; + private final Log logger = LogFactory.getLog(getClass()); + private final RegisteredClientRepository registeredClientRepository; + + public DeviceClientAuthenticationProvider(RegisteredClientRepository registeredClientRepository) { + Assert.notNull(registeredClientRepository, "registeredClientRepository cannot be null"); + this.registeredClientRepository = registeredClientRepository; + } + + @Override + public Authentication authenticate(Authentication authentication) throws AuthenticationException { + DeviceClientAuthenticationToken deviceClientAuthentication = + (DeviceClientAuthenticationToken) authentication; + + if (!ClientAuthenticationMethod.NONE.equals(deviceClientAuthentication.getClientAuthenticationMethod())) { + return null; + } + + String clientId = deviceClientAuthentication.getPrincipal().toString(); + RegisteredClient registeredClient = this.registeredClientRepository.findByClientId(clientId); + if (registeredClient == null) { + throwInvalidClient(OAuth2ParameterNames.CLIENT_ID); + } + + if (this.logger.isTraceEnabled()) { + this.logger.trace("Retrieved registered client"); + } + + if (!registeredClient.getClientAuthenticationMethods().contains( + deviceClientAuthentication.getClientAuthenticationMethod())) { + throwInvalidClient("authentication_method"); + } + + if (this.logger.isTraceEnabled()) { + this.logger.trace("Validated device client authentication parameters"); + } + + if (this.logger.isTraceEnabled()) { + this.logger.trace("Authenticated device client"); + } + + return new DeviceClientAuthenticationToken(registeredClient, + deviceClientAuthentication.getClientAuthenticationMethod(), null); + } + + @Override + public boolean supports(Class authentication) { + return DeviceClientAuthenticationToken.class.isAssignableFrom(authentication); + } + + private static void throwInvalidClient(String parameterName) { + OAuth2Error error = new OAuth2Error( + OAuth2ErrorCodes.INVALID_CLIENT, + "Device client authentication failed: " + parameterName, + ERROR_URI + ); + throw new OAuth2AuthenticationException(error); + } + +} diff --git a/demo-authorizationserver/src/main/java/sample/authentication/DeviceClientAuthenticationToken.java b/demo-authorizationserver/src/main/java/sample/authentication/DeviceClientAuthenticationToken.java new file mode 100644 index 0000000..4e9a3d2 --- /dev/null +++ b/demo-authorizationserver/src/main/java/sample/authentication/DeviceClientAuthenticationToken.java @@ -0,0 +1,44 @@ +/* + * Copyright 2020-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package sample.authentication; + +import java.util.Map; + +import org.springframework.lang.Nullable; +import org.springframework.security.core.Transient; +import org.springframework.security.oauth2.core.ClientAuthenticationMethod; +import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken; +import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; + +/** + * @author Joe Grandja + * @author Steve Riesenberg + * @since 1.1 + */ +@Transient +public class DeviceClientAuthenticationToken extends OAuth2ClientAuthenticationToken { + + public DeviceClientAuthenticationToken(String clientId, ClientAuthenticationMethod clientAuthenticationMethod, + @Nullable Object credentials, @Nullable Map additionalParameters) { + super(clientId, clientAuthenticationMethod, credentials, additionalParameters); + } + + public DeviceClientAuthenticationToken(RegisteredClient registeredClient, ClientAuthenticationMethod clientAuthenticationMethod, + @Nullable Object credentials) { + super(registeredClient, clientAuthenticationMethod, credentials); + } + +} diff --git a/demo-authorizationserver/src/main/java/sample/config/AuthorizationServerConfig.java b/demo-authorizationserver/src/main/java/sample/config/AuthorizationServerConfig.java new file mode 100644 index 0000000..c2d04f7 --- /dev/null +++ b/demo-authorizationserver/src/main/java/sample/config/AuthorizationServerConfig.java @@ -0,0 +1,215 @@ +/* + * Copyright 2020-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package sample.config; + +import java.util.UUID; + +import com.nimbusds.jose.jwk.JWKSet; +import com.nimbusds.jose.jwk.RSAKey; +import com.nimbusds.jose.jwk.source.JWKSource; +import com.nimbusds.jose.proc.SecurityContext; +import sample.authentication.DeviceClientAuthenticationProvider; +import sample.federation.FederatedIdentityIdTokenCustomizer; +import sample.jose.Jwks; +import sample.web.authentication.DeviceClientAuthenticationConverter; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.http.MediaType; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabase; +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder; +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType; +import org.springframework.security.config.Customizer; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.ClientAuthenticationMethod; +import org.springframework.security.oauth2.core.oidc.OidcScopes; +import org.springframework.security.oauth2.jwt.JwtDecoder; +import org.springframework.security.oauth2.server.authorization.JdbcOAuth2AuthorizationConsentService; +import org.springframework.security.oauth2.server.authorization.JdbcOAuth2AuthorizationService; +import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService; +import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService; +import org.springframework.security.oauth2.server.authorization.client.JdbcRegisteredClientRepository; +import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; +import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository; +import org.springframework.security.oauth2.server.authorization.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration; +import org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer; +import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings; +import org.springframework.security.oauth2.server.authorization.settings.ClientSettings; +import org.springframework.security.oauth2.server.authorization.token.JwtEncodingContext; +import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenCustomizer; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint; +import org.springframework.security.web.util.matcher.MediaTypeRequestMatcher; + +/** + * @author Joe Grandja + * @author Daniel Garnier-Moiroux + * @author Steve Riesenberg + * @since 1.1 + */ +@Configuration(proxyBeanMethods = false) +public class AuthorizationServerConfig { + + + private static final String CUSTOM_CONSENT_PAGE_URI = "/oauth2/consent";//这个是授权页 + + //这个就是oauth2 授权服务的一个配置核心了 + // 官方网站的说明更具体 https://docs.spring.io/spring-authorization-server/docs/current/reference/html/protocol-endpoints.html + @Bean + @Order(Ordered.HIGHEST_PRECEDENCE) + public SecurityFilterChain authorizationServerSecurityFilterChain( + HttpSecurity http, RegisteredClientRepository registeredClientRepository, + AuthorizationServerSettings authorizationServerSettings) throws Exception { + + OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http); + + DeviceClientAuthenticationConverter deviceClientAuthenticationConverter = + new DeviceClientAuthenticationConverter( + authorizationServerSettings.getDeviceAuthorizationEndpoint()); + DeviceClientAuthenticationProvider deviceClientAuthenticationProvider = + new DeviceClientAuthenticationProvider(registeredClientRepository); + + // @formatter:off + http.getConfigurer(OAuth2AuthorizationServerConfigurer.class) + .deviceAuthorizationEndpoint(deviceAuthorizationEndpoint -> + deviceAuthorizationEndpoint.verificationUri("/activate") + ) + .deviceVerificationEndpoint(deviceVerificationEndpoint -> + deviceVerificationEndpoint.consentPage(CUSTOM_CONSENT_PAGE_URI) + ) + .clientAuthentication(clientAuthentication -> + clientAuthentication + .authenticationConverter(deviceClientAuthenticationConverter) + .authenticationProvider(deviceClientAuthenticationProvider) + ) + .authorizationEndpoint(authorizationEndpoint -> + authorizationEndpoint.consentPage(CUSTOM_CONSENT_PAGE_URI)) + .oidc(Customizer.withDefaults()); // Enable OpenID Connect 1.0 + // @formatter:on + + // @formatter:off + http + .exceptionHandling((exceptions) -> exceptions + .defaultAuthenticationEntryPointFor( + new LoginUrlAuthenticationEntryPoint("/login"), + new MediaTypeRequestMatcher(MediaType.TEXT_HTML) + ) + ) + .oauth2ResourceServer(oauth2ResourceServer -> + oauth2ResourceServer.jwt(Customizer.withDefaults())); + // @formatter:on + return http.build(); + } + + // 这个就是客户端的获取方式了,授权服务内部会调用做一些验证 例如 redirectUri + // 官方给出的demo就先在内存里面初始化 也可以才有数据库的形式 实现 RegisteredClientRepository即可 + @Bean + public RegisteredClientRepository registeredClientRepository(JdbcTemplate jdbcTemplate) { + RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString()) + .clientId("messaging-client") + .clientSecret("{noop}secret") + .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC) + .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) + .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN) + .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS) + .redirectUri("http://127.0.0.1:8080/login/oauth2/code/messaging-client-oidc") + .redirectUri("http://127.0.0.1:8080/authorized") + .postLogoutRedirectUri("http://127.0.0.1:8080/logged-out") + .scope(OidcScopes.OPENID) + .scope(OidcScopes.PROFILE) + .scope("message.read") + .scope("message.write") + .clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build())//requireAuthorizationConsent(true) 授权页是有的 如果是false是没有的 + .build(); + + RegisteredClient deviceClient = RegisteredClient.withId(UUID.randomUUID().toString()) + .clientId("device-messaging-client") + .clientAuthenticationMethod(ClientAuthenticationMethod.NONE) + .authorizationGrantType(AuthorizationGrantType.DEVICE_CODE) + .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN) + .scope("message.read") + .scope("message.write") + .build(); + + // Save registered client's in db as if in-memory + JdbcRegisteredClientRepository registeredClientRepository = new JdbcRegisteredClientRepository(jdbcTemplate); + registeredClientRepository.save(registeredClient); + registeredClientRepository.save(deviceClient); + + return registeredClientRepository; + } + // @formatter:on + + //这个是oauth2的授权信息(包含了用户、token等其他信息) 这个也是可以扩展的 OAuth2AuthorizationService也是一个实现类 + @Bean + public OAuth2AuthorizationService authorizationService(JdbcTemplate jdbcTemplate, + RegisteredClientRepository registeredClientRepository) { + return new JdbcOAuth2AuthorizationService(jdbcTemplate, registeredClientRepository); + } + + //这个是oauth2授权记录的持久化存储方式 看 JdbcOAuth2AuthorizationConsentService 就知道是基于数据库的了,当然也可以进行扩展 基于redis 后面再将 你可以看看 JdbcOAuth2AuthorizationConsentService的是一个实现 + @Bean + public OAuth2AuthorizationConsentService authorizationConsentService(JdbcTemplate jdbcTemplate, + RegisteredClientRepository registeredClientRepository) { + // Will be used by the ConsentController + return new JdbcOAuth2AuthorizationConsentService(jdbcTemplate, registeredClientRepository); + } + + @Bean + public OAuth2TokenCustomizer idTokenCustomizer() { + return new FederatedIdentityIdTokenCustomizer(); + } + + @Bean + public JWKSource jwkSource() { + RSAKey rsaKey = Jwks.generateRsa(); + JWKSet jwkSet = new JWKSet(rsaKey); + return (jwkSelector, securityContext) -> jwkSelector.select(jwkSet); + } + + @Bean + public JwtDecoder jwtDecoder(JWKSource jwkSource) { + return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource); + } + + //授权服务器的配置 很多class 你看它命名就知道了 想研究的可以点进去看一看 + @Bean + public AuthorizationServerSettings authorizationServerSettings() { + return AuthorizationServerSettings.builder().build(); + } + + + //此时基于H2数据库(内存数据库) 需要使用mysql 就注释掉就可以了 demo这个地方我们用内存跑就行了 省事 + @Bean + public EmbeddedDatabase embeddedDatabase() { + // @formatter:off + return new EmbeddedDatabaseBuilder() + .generateUniqueName(true) + .setType(EmbeddedDatabaseType.H2) + .setScriptEncoding("UTF-8") + .addScript("org/springframework/security/oauth2/server/authorization/oauth2-authorization-schema.sql") + .addScript("org/springframework/security/oauth2/server/authorization/oauth2-authorization-consent-schema.sql") + .addScript("org/springframework/security/oauth2/server/authorization/client/oauth2-registered-client-schema.sql") + .build(); + // @formatter:on + } + +} + diff --git a/demo-authorizationserver/src/main/java/sample/config/DefaultSecurityConfig.java b/demo-authorizationserver/src/main/java/sample/config/DefaultSecurityConfig.java new file mode 100644 index 0000000..f22680a --- /dev/null +++ b/demo-authorizationserver/src/main/java/sample/config/DefaultSecurityConfig.java @@ -0,0 +1,114 @@ +/* + * Copyright 2020-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package sample.config; + +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import org.springframework.web.filter.CorsFilter; +import sample.federation.FederatedIdentityAuthenticationSuccessHandler; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.core.session.SessionRegistry; +import org.springframework.security.core.session.SessionRegistryImpl; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.provisioning.InMemoryUserDetailsManager; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.AuthenticationSuccessHandler; +import org.springframework.security.web.session.HttpSessionEventPublisher; + +/** + * @author Joe Grandja + * @author Steve Riesenberg + * @since 1.1 + */ +@EnableWebSecurity +@Configuration(proxyBeanMethods = false) +public class DefaultSecurityConfig { + + // 过滤器链 + @Bean + public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception { + http + .authorizeHttpRequests(authorize ->//① 配置鉴权的 + authorize + .requestMatchers("/assets/**", "/webjars/**", "/login","/oauth2/**","/oauth2/token").permitAll() //② 忽略鉴权的url + .anyRequest().authenticated()//③ 排除忽略的其他url就需要鉴权了 + ) + .csrf(AbstractHttpConfigurer::disable) + .formLogin(formLogin -> + formLogin + .loginPage("/login")//④ 授权服务认证页面(可以配置相对和绝对地址,前后端分离的情况下填前端的url) + ) + .oauth2Login(oauth2Login -> + oauth2Login + .loginPage("/login")//⑤ oauth2的认证页面(也可配置绝对地址) + .successHandler(authenticationSuccessHandler())//⑥ 登录成功后的处理 + ); + + return http.build(); + } + + + private AuthenticationSuccessHandler authenticationSuccessHandler() { + return new FederatedIdentityAuthenticationSuccessHandler(); + } + + // 初始化了一个用户在内存里面(这样就不会每次启动就再去生成密码了) + @Bean + public UserDetailsService users() { + UserDetails user = User.withDefaultPasswordEncoder() + .username("user1") + .password("password") + .roles("USER") + .build(); + return new InMemoryUserDetailsManager(user); + } + + + @Bean + public SessionRegistry sessionRegistry() { + return new SessionRegistryImpl(); + } + + @Bean + public HttpSessionEventPublisher httpSessionEventPublisher() { + return new HttpSessionEventPublisher(); + } + + + /** + * 跨域过滤器配置 + * @return + */ + @Bean + public CorsFilter corsFilter() { + CorsConfiguration configuration = new CorsConfiguration(); + configuration.addAllowedOrigin("*"); + configuration.setAllowCredentials(true); + configuration.addAllowedMethod("*"); + configuration.addAllowedHeader("*"); + UrlBasedCorsConfigurationSource configurationSource = new UrlBasedCorsConfigurationSource(); + configurationSource.registerCorsConfiguration("/**", configuration); + return new CorsFilter(configurationSource); + } + +} diff --git a/demo-authorizationserver/src/main/java/sample/federation/FederatedIdentityAuthenticationSuccessHandler.java b/demo-authorizationserver/src/main/java/sample/federation/FederatedIdentityAuthenticationSuccessHandler.java new file mode 100644 index 0000000..ed4c240 --- /dev/null +++ b/demo-authorizationserver/src/main/java/sample/federation/FederatedIdentityAuthenticationSuccessHandler.java @@ -0,0 +1,72 @@ +/* + * Copyright 2020-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package sample.federation; + +// tag::imports[] +import java.io.IOException; +import java.util.function.Consumer; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; +import org.springframework.security.oauth2.core.oidc.user.OidcUser; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.security.web.authentication.AuthenticationSuccessHandler; +import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler; +// end::imports[] + +/** + * An {@link AuthenticationSuccessHandler} for capturing the {@link OidcUser} or + * {@link OAuth2User} for Federated Account Linking or JIT Account Provisioning. + * + * @author Steve Riesenberg + * @since 1.1 + */ +// tag::class[] +public final class FederatedIdentityAuthenticationSuccessHandler implements AuthenticationSuccessHandler { + + private final AuthenticationSuccessHandler delegate = new SavedRequestAwareAuthenticationSuccessHandler(); + + private Consumer oauth2UserHandler = (user) -> {}; + + private Consumer oidcUserHandler = (user) -> this.oauth2UserHandler.accept(user); + + @Override + public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { + if (authentication instanceof OAuth2AuthenticationToken) { + if (authentication.getPrincipal() instanceof OidcUser) { + this.oidcUserHandler.accept((OidcUser) authentication.getPrincipal()); + } else if (authentication.getPrincipal() instanceof OAuth2User) { + this.oauth2UserHandler.accept((OAuth2User) authentication.getPrincipal()); + } + } + + this.delegate.onAuthenticationSuccess(request, response, authentication); + } + + public void setOAuth2UserHandler(Consumer oauth2UserHandler) { + this.oauth2UserHandler = oauth2UserHandler; + } + + public void setOidcUserHandler(Consumer oidcUserHandler) { + this.oidcUserHandler = oidcUserHandler; + } + +} +// end::class[] diff --git a/demo-authorizationserver/src/main/java/sample/federation/FederatedIdentityIdTokenCustomizer.java b/demo-authorizationserver/src/main/java/sample/federation/FederatedIdentityIdTokenCustomizer.java new file mode 100644 index 0000000..0929ed4 --- /dev/null +++ b/demo-authorizationserver/src/main/java/sample/federation/FederatedIdentityIdTokenCustomizer.java @@ -0,0 +1,95 @@ +/* + * Copyright 2020-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package sample.federation; + +// tag::imports[] +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.core.oidc.IdTokenClaimNames; +import org.springframework.security.oauth2.core.oidc.OidcIdToken; +import org.springframework.security.oauth2.core.oidc.endpoint.OidcParameterNames; +import org.springframework.security.oauth2.core.oidc.user.OidcUser; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.security.oauth2.server.authorization.token.JwtEncodingContext; +import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenCustomizer; +// end::imports[] + +/** + * An {@link OAuth2TokenCustomizer} to map claims from a federated identity to + * the {@code id_token} produced by this authorization server. + * + * @author Steve Riesenberg + * @since 1.1 + */ +// tag::class[] +public final class FederatedIdentityIdTokenCustomizer implements OAuth2TokenCustomizer { + + private static final Set ID_TOKEN_CLAIMS = Collections.unmodifiableSet(new HashSet<>(Arrays.asList( + IdTokenClaimNames.ISS, + IdTokenClaimNames.SUB, + IdTokenClaimNames.AUD, + IdTokenClaimNames.EXP, + IdTokenClaimNames.IAT, + IdTokenClaimNames.AUTH_TIME, + IdTokenClaimNames.NONCE, + IdTokenClaimNames.ACR, + IdTokenClaimNames.AMR, + IdTokenClaimNames.AZP, + IdTokenClaimNames.AT_HASH, + IdTokenClaimNames.C_HASH + ))); + + @Override + public void customize(JwtEncodingContext context) { + if (OidcParameterNames.ID_TOKEN.equals(context.getTokenType().getValue())) { + Map thirdPartyClaims = extractClaims(context.getPrincipal()); + context.getClaims().claims(existingClaims -> { + // Remove conflicting claims set by this authorization server + existingClaims.keySet().forEach(thirdPartyClaims::remove); + + // Remove standard id_token claims that could cause problems with clients + ID_TOKEN_CLAIMS.forEach(thirdPartyClaims::remove); + + // Add all other claims directly to id_token + existingClaims.putAll(thirdPartyClaims); + }); + } + } + + private Map extractClaims(Authentication principal) { + Map claims; + if (principal.getPrincipal() instanceof OidcUser) { + OidcUser oidcUser = (OidcUser) principal.getPrincipal(); + OidcIdToken idToken = oidcUser.getIdToken(); + claims = idToken.getClaims(); + } else if (principal.getPrincipal() instanceof OAuth2User) { + OAuth2User oauth2User = (OAuth2User) principal.getPrincipal(); + claims = oauth2User.getAttributes(); + } else { + claims = Collections.emptyMap(); + } + + return new HashMap<>(claims); + } + +} +// end::class[] diff --git a/demo-authorizationserver/src/main/java/sample/federation/UserRepositoryOAuth2UserHandler.java b/demo-authorizationserver/src/main/java/sample/federation/UserRepositoryOAuth2UserHandler.java new file mode 100644 index 0000000..95030de --- /dev/null +++ b/demo-authorizationserver/src/main/java/sample/federation/UserRepositoryOAuth2UserHandler.java @@ -0,0 +1,61 @@ +/* + * Copyright 2020-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package sample.federation; + +// tag::imports[] +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Consumer; + +import org.springframework.security.oauth2.core.user.OAuth2User; +// end::imports[] + +/** + * Example {@link Consumer} to perform JIT provisioning of an {@link OAuth2User}. + * + * @author Steve Riesenberg + * @since 1.1 + */ +// tag::class[] +public final class UserRepositoryOAuth2UserHandler implements Consumer { + + private final UserRepository userRepository = new UserRepository(); + + @Override + public void accept(OAuth2User user) { + // Capture user in a local data store on first authentication + if (this.userRepository.findByName(user.getName()) == null) { + System.out.println("Saving first-time user: name=" + user.getName() + ", claims=" + user.getAttributes() + ", authorities=" + user.getAuthorities()); + this.userRepository.save(user); + } + } + + static class UserRepository { + + private final Map userCache = new ConcurrentHashMap<>(); + + public OAuth2User findByName(String name) { + return this.userCache.get(name); + } + + public void save(OAuth2User oauth2User) { + this.userCache.put(oauth2User.getName(), oauth2User); + } + + } + +} +// end::class[] diff --git a/demo-authorizationserver/src/main/java/sample/jose/Jwks.java b/demo-authorizationserver/src/main/java/sample/jose/Jwks.java new file mode 100644 index 0000000..1f3c714 --- /dev/null +++ b/demo-authorizationserver/src/main/java/sample/jose/Jwks.java @@ -0,0 +1,75 @@ +/* + * Copyright 2020-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package sample.jose; + +import java.security.KeyPair; +import java.security.interfaces.ECPrivateKey; +import java.security.interfaces.ECPublicKey; +import java.security.interfaces.RSAPrivateKey; +import java.security.interfaces.RSAPublicKey; +import java.util.UUID; + +import javax.crypto.SecretKey; + +import com.nimbusds.jose.jwk.Curve; +import com.nimbusds.jose.jwk.ECKey; +import com.nimbusds.jose.jwk.OctetSequenceKey; +import com.nimbusds.jose.jwk.RSAKey; + +/** + * @author Joe Grandja + * @since 1.1 + */ +public final class Jwks { + + private Jwks() { + } + + public static RSAKey generateRsa() { + KeyPair keyPair = KeyGeneratorUtils.generateRsaKey(); + RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic(); + RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate(); + // @formatter:off + return new RSAKey.Builder(publicKey) + .privateKey(privateKey) + .keyID(UUID.randomUUID().toString()) + .build(); + // @formatter:on + } + + public static ECKey generateEc() { + KeyPair keyPair = KeyGeneratorUtils.generateEcKey(); + ECPublicKey publicKey = (ECPublicKey) keyPair.getPublic(); + ECPrivateKey privateKey = (ECPrivateKey) keyPair.getPrivate(); + Curve curve = Curve.forECParameterSpec(publicKey.getParams()); + // @formatter:off + return new ECKey.Builder(curve, publicKey) + .privateKey(privateKey) + .keyID(UUID.randomUUID().toString()) + .build(); + // @formatter:on + } + + public static OctetSequenceKey generateSecret() { + SecretKey secretKey = KeyGeneratorUtils.generateSecretKey(); + // @formatter:off + return new OctetSequenceKey.Builder(secretKey) + .keyID(UUID.randomUUID().toString()) + .build(); + // @formatter:on + } + +} diff --git a/demo-authorizationserver/src/main/java/sample/jose/KeyGeneratorUtils.java b/demo-authorizationserver/src/main/java/sample/jose/KeyGeneratorUtils.java new file mode 100644 index 0000000..ec55abd --- /dev/null +++ b/demo-authorizationserver/src/main/java/sample/jose/KeyGeneratorUtils.java @@ -0,0 +1,86 @@ +/* + * Copyright 2020-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package sample.jose; + +import java.math.BigInteger; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.spec.ECFieldFp; +import java.security.spec.ECParameterSpec; +import java.security.spec.ECPoint; +import java.security.spec.EllipticCurve; + +import javax.crypto.KeyGenerator; +import javax.crypto.SecretKey; + +/** + * @author Joe Grandja + * @since 1.1 + */ +final class KeyGeneratorUtils { + + private KeyGeneratorUtils() { + } + + static SecretKey generateSecretKey() { + SecretKey hmacKey; + try { + hmacKey = KeyGenerator.getInstance("HmacSha256").generateKey(); + } catch (Exception ex) { + throw new IllegalStateException(ex); + } + return hmacKey; + } + + static KeyPair generateRsaKey() { + KeyPair keyPair; + try { + KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA"); + keyPairGenerator.initialize(2048); + keyPair = keyPairGenerator.generateKeyPair(); + } catch (Exception ex) { + throw new IllegalStateException(ex); + } + return keyPair; + } + + static KeyPair generateEcKey() { + EllipticCurve ellipticCurve = new EllipticCurve( + new ECFieldFp( + new BigInteger("115792089210356248762697446949407573530086143415290314195533631308867097853951")), + new BigInteger("115792089210356248762697446949407573530086143415290314195533631308867097853948"), + new BigInteger("41058363725152142129326129780047268409114441015993725554835256314039467401291")); + ECPoint ecPoint = new ECPoint( + new BigInteger("48439561293906451759052585252797914202762949526041747995844080717082404635286"), + new BigInteger("36134250956749795798585127919587881956611106672985015071877198253568414405109")); + ECParameterSpec ecParameterSpec = new ECParameterSpec( + ellipticCurve, + ecPoint, + new BigInteger("115792089210356248762697446949407573529996955224135760342422259061068512044369"), + 1); + + KeyPair keyPair; + try { + KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("EC"); + keyPairGenerator.initialize(ecParameterSpec); + keyPair = keyPairGenerator.generateKeyPair(); + } catch (Exception ex) { + throw new IllegalStateException(ex); + } + return keyPair; + } + +} diff --git a/demo-authorizationserver/src/main/java/sample/web/AuthorizationConsentController.java b/demo-authorizationserver/src/main/java/sample/web/AuthorizationConsentController.java new file mode 100644 index 0000000..c21e6e8 --- /dev/null +++ b/demo-authorizationserver/src/main/java/sample/web/AuthorizationConsentController.java @@ -0,0 +1,136 @@ +/* + * Copyright 2020-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package sample.web; + +import java.security.Principal; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; +import org.springframework.security.oauth2.core.oidc.OidcScopes; +import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsent; +import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService; +import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; +import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.util.StringUtils; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; + +/** + * @author Daniel Garnier-Moiroux + */ +@Controller +public class AuthorizationConsentController { + private final RegisteredClientRepository registeredClientRepository; + private final OAuth2AuthorizationConsentService authorizationConsentService; + + public AuthorizationConsentController(RegisteredClientRepository registeredClientRepository, + OAuth2AuthorizationConsentService authorizationConsentService) { + this.registeredClientRepository = registeredClientRepository; + this.authorizationConsentService = authorizationConsentService; + } + + @GetMapping(value = "/oauth2/consent") + public String consent(Principal principal, Model model, + @RequestParam(OAuth2ParameterNames.CLIENT_ID) String clientId, + @RequestParam(OAuth2ParameterNames.SCOPE) String scope, + @RequestParam(OAuth2ParameterNames.STATE) String state, + @RequestParam(name = OAuth2ParameterNames.USER_CODE, required = false) String userCode) { + + // Remove scopes that were already approved + Set scopesToApprove = new HashSet<>(); + Set previouslyApprovedScopes = new HashSet<>(); + RegisteredClient registeredClient = this.registeredClientRepository.findByClientId(clientId); + OAuth2AuthorizationConsent currentAuthorizationConsent = + this.authorizationConsentService.findById(registeredClient.getId(), principal.getName()); + Set authorizedScopes; + if (currentAuthorizationConsent != null) { + authorizedScopes = currentAuthorizationConsent.getScopes(); + } else { + authorizedScopes = Collections.emptySet(); + } + for (String requestedScope : StringUtils.delimitedListToStringArray(scope, " ")) { + if (OidcScopes.OPENID.equals(requestedScope)) { + continue; + } + if (authorizedScopes.contains(requestedScope)) { + previouslyApprovedScopes.add(requestedScope); + } else { + scopesToApprove.add(requestedScope); + } + } + + model.addAttribute("clientId", clientId); + model.addAttribute("state", state); + model.addAttribute("scopes", withDescription(scopesToApprove)); + model.addAttribute("previouslyApprovedScopes", withDescription(previouslyApprovedScopes)); + model.addAttribute("principalName", principal.getName()); + model.addAttribute("userCode", userCode); + if (StringUtils.hasText(userCode)) { + model.addAttribute("requestURI", "/oauth2/device_verification"); + } else { + model.addAttribute("requestURI", "/oauth2/authorize"); + } + + return "consent"; + } + + private static Set withDescription(Set scopes) { + Set scopeWithDescriptions = new HashSet<>(); + for (String scope : scopes) { + scopeWithDescriptions.add(new ScopeWithDescription(scope)); + + } + return scopeWithDescriptions; + } + + public static class ScopeWithDescription { + private static final String DEFAULT_DESCRIPTION = "UNKNOWN SCOPE - We cannot provide information about this permission, use caution when granting this."; + private static final Map scopeDescriptions = new HashMap<>(); + static { + scopeDescriptions.put( + OidcScopes.PROFILE, + "This application will be able to read your profile information." + ); + scopeDescriptions.put( + "message.read", + "This application will be able to read your message." + ); + scopeDescriptions.put( + "message.write", + "This application will be able to add new messages. It will also be able to edit and delete existing messages." + ); + scopeDescriptions.put( + "other.scope", + "This is another scope example of a scope description." + ); + } + + public final String scope; + public final String description; + + ScopeWithDescription(String scope) { + this.scope = scope; + this.description = scopeDescriptions.getOrDefault(scope, DEFAULT_DESCRIPTION); + } + } + +} diff --git a/demo-authorizationserver/src/main/java/sample/web/DefaultErrorController.java b/demo-authorizationserver/src/main/java/sample/web/DefaultErrorController.java new file mode 100644 index 0000000..af4a3c0 --- /dev/null +++ b/demo-authorizationserver/src/main/java/sample/web/DefaultErrorController.java @@ -0,0 +1,52 @@ +/* + * Copyright 2020-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package sample.web; + +import jakarta.servlet.RequestDispatcher; +import jakarta.servlet.http.HttpServletRequest; + +import org.springframework.boot.web.servlet.error.ErrorController; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.util.StringUtils; +import org.springframework.web.bind.annotation.RequestMapping; + +/** + * @author Steve Riesenberg + * @since 1.1 + */ +@Controller +public class DefaultErrorController implements ErrorController { + + @RequestMapping("/error") + public String handleError(Model model, HttpServletRequest request) { + String errorMessage = getErrorMessage(request); + if (errorMessage.startsWith("[access_denied]")) { + model.addAttribute("errorTitle", "Access Denied"); + model.addAttribute("errorMessage", "You have denied access."); + } else { + model.addAttribute("errorTitle", "Error"); + model.addAttribute("errorMessage", errorMessage); + } + return "error"; + } + + private String getErrorMessage(HttpServletRequest request) { + String errorMessage = (String) request.getAttribute(RequestDispatcher.ERROR_MESSAGE); + return StringUtils.hasText(errorMessage) ? errorMessage : ""; + } + +} diff --git a/demo-authorizationserver/src/main/java/sample/web/DeviceController.java b/demo-authorizationserver/src/main/java/sample/web/DeviceController.java new file mode 100644 index 0000000..b9cc9ee --- /dev/null +++ b/demo-authorizationserver/src/main/java/sample/web/DeviceController.java @@ -0,0 +1,47 @@ +/* + * Copyright 2020-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package sample.web; + +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; + +/** + * @author Steve Riesenberg + * @since 1.1 + */ +@Controller +public class DeviceController { + + @GetMapping("/activate") + public String activate(@RequestParam(value = "user_code", required = false) String userCode) { + if (userCode != null) { + return "redirect:/oauth2/device_verification?user_code=" + userCode; + } + return "device-activate"; + } + + @GetMapping("/activated") + public String activated() { + return "device-activated"; + } + + @GetMapping(value = "/", params = "success") + public String success() { + return "device-activated"; + } + +} diff --git a/demo-authorizationserver/src/main/java/sample/web/LoginController.java b/demo-authorizationserver/src/main/java/sample/web/LoginController.java new file mode 100644 index 0000000..df193e0 --- /dev/null +++ b/demo-authorizationserver/src/main/java/sample/web/LoginController.java @@ -0,0 +1,33 @@ +/* + * Copyright 2020-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package sample.web; + +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; + +/** + * @author Steve Riesenberg + * @since 1.1 + */ +@Controller +public class LoginController { + + @GetMapping("/login") + public String login() { + return "login"; + } + +} diff --git a/demo-authorizationserver/src/main/java/sample/web/authentication/DeviceClientAuthenticationConverter.java b/demo-authorizationserver/src/main/java/sample/web/authentication/DeviceClientAuthenticationConverter.java new file mode 100644 index 0000000..aa1cbfe --- /dev/null +++ b/demo-authorizationserver/src/main/java/sample/web/authentication/DeviceClientAuthenticationConverter.java @@ -0,0 +1,76 @@ +/* + * Copyright 2020-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package sample.web.authentication; + +import jakarta.servlet.http.HttpServletRequest; + +import sample.authentication.DeviceClientAuthenticationToken; + +import org.springframework.http.HttpMethod; +import org.springframework.lang.Nullable; +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.ClientAuthenticationMethod; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.OAuth2ErrorCodes; +import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; +import org.springframework.security.web.authentication.AuthenticationConverter; +import org.springframework.security.web.util.matcher.AndRequestMatcher; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; +import org.springframework.security.web.util.matcher.RequestMatcher; +import org.springframework.util.StringUtils; + +/** + * @author Joe Grandja + * @author Steve Riesenberg + * @since 1.1 + */ +public final class DeviceClientAuthenticationConverter implements AuthenticationConverter { + private final RequestMatcher deviceAuthorizationRequestMatcher; + private final RequestMatcher deviceAccessTokenRequestMatcher; + + public DeviceClientAuthenticationConverter(String deviceAuthorizationEndpointUri) { + RequestMatcher clientIdParameterMatcher = request -> + request.getParameter(OAuth2ParameterNames.CLIENT_ID) != null; + this.deviceAuthorizationRequestMatcher = new AndRequestMatcher( + new AntPathRequestMatcher( + deviceAuthorizationEndpointUri, HttpMethod.POST.name()), + clientIdParameterMatcher); + this.deviceAccessTokenRequestMatcher = request -> + AuthorizationGrantType.DEVICE_CODE.getValue().equals(request.getParameter(OAuth2ParameterNames.GRANT_TYPE)) && + request.getParameter(OAuth2ParameterNames.DEVICE_CODE) != null && + request.getParameter(OAuth2ParameterNames.CLIENT_ID) != null; + } + + @Nullable + @Override + public Authentication convert(HttpServletRequest request) { + if (!this.deviceAuthorizationRequestMatcher.matches(request) && + !this.deviceAccessTokenRequestMatcher.matches(request)) { + return null; + } + + // client_id (REQUIRED) + String clientId = request.getParameter(OAuth2ParameterNames.CLIENT_ID); + if (!StringUtils.hasText(clientId) || + request.getParameterValues(OAuth2ParameterNames.CLIENT_ID).length != 1) { + throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_REQUEST); + } + + return new DeviceClientAuthenticationToken(clientId, ClientAuthenticationMethod.NONE, null, null); + } + +} diff --git a/demo-authorizationserver/src/main/resources/application.yml b/demo-authorizationserver/src/main/resources/application.yml new file mode 100644 index 0000000..75f0ce0 --- /dev/null +++ b/demo-authorizationserver/src/main/resources/application.yml @@ -0,0 +1,52 @@ +server: + port: 9000 + +spring: + security: + oauth2: + client: + registration: + github-idp: + provider: github + client-id: 2205af0f0cc93e3a22ea + client-secret: 649d88df840a57d2591c4832b438cc9af2727240 +# redirect-uri: http://192.168.56.1:9000/login/oauth2/code/github-idp # 这个地方可以不配置,配置就要与github的应用配置回调一致 + scope: user:email, read:user + client-name: Sign in with GitHub + gitee: + # 指定oauth登录提供者,该oauth登录由provider中的gitee来处理 + provider: gitee + # 客户端名字 + client-name: Sign in with Gitee + # 认证方式 + authorization-grant-type: authorization_code + # 客户端id,使用自己的gitee的客户端id + client-id: 29b85c97ed682910eaa4276d84a0c4532f00b962e1b9fe8552520129e65ae432 + # 客户端秘钥,使用自己的gitee的客户端秘钥 + client-secret: 8c6df920482a83d4662a34b76a9c3a62c8e80713e4f2957bb0459c3ceb70d73b + # 回调地址 与gitee 配置的回调地址一致才行 + redirect-uri: http://192.168.2.16:9000/login/oauth2/code/gitee + # 申请scope列表 + scope: + - emails + - user_info + provider: + github: + user-name-attribute: login + gitee: + # 设置用户信息名称对应的字段属性 + user-name-attribute: login + # 获取token的地址 + token-uri: https://gitee.com/oauth/token + # 获取用户信息的地址 + user-info-uri: https://gitee.com/api/v5/user + # 发起授权申请的地址 + authorization-uri: https://gitee.com/oauth/authorize + +logging: + level: + root: INFO + org.springframework.web: INFO + org.springframework.security: debug + org.springframework.security.oauth2: debug + org.springframework.security.oauth2.client: trace \ No newline at end of file diff --git a/demo-authorizationserver/src/main/resources/static/assets/css/signin.css b/demo-authorizationserver/src/main/resources/static/assets/css/signin.css new file mode 100644 index 0000000..2ee098f --- /dev/null +++ b/demo-authorizationserver/src/main/resources/static/assets/css/signin.css @@ -0,0 +1,32 @@ +html, +body { + height: 100%; +} + +body { + display: flex; + align-items: start; + padding-top: 100px; + background-color: #f5f5f5; +} + +.form-signin { + max-width: 330px; + padding: 15px; +} + +.form-signin .form-floating:focus-within { + z-index: 2; +} + +.form-signin input[type="username"] { + margin-bottom: -1px; + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; +} + +.form-signin input[type="password"] { + margin-bottom: 10px; + border-top-left-radius: 0; + border-top-right-radius: 0; +} diff --git a/demo-authorizationserver/src/main/resources/static/assets/img/devices.png b/demo-authorizationserver/src/main/resources/static/assets/img/devices.png new file mode 100644 index 0000000000000000000000000000000000000000..fda6b12e312ff705176efdb8db7f8400ba9fb1d6 GIT binary patch literal 19071 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 a1q~^)FZa(e1RnpylG+{3+ht0YA^!_vu?F@4 literal 0 HcmV?d00001 diff --git a/demo-authorizationserver/src/main/resources/static/assets/img/gitee.png b/demo-authorizationserver/src/main/resources/static/assets/img/gitee.png new file mode 100644 index 0000000000000000000000000000000000000000..ee329fefeb068ec23b6949aa7817e713a3a91f2b GIT binary patch literal 14158 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

)D^fK#(^T#hf1bi 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 literal 0 HcmV?d00001 diff --git a/demo-authorizationserver/src/main/resources/static/assets/img/google.png b/demo-authorizationserver/src/main/resources/static/assets/img/google.png new file mode 100644 index 0000000000000000000000000000000000000000..795dea3190ff1bf6bb0a6a178d8fbefefdcf756f GIT binary patch literal 27629 zcmY(r2Rzl$|3Ch|*Tubri|laiO7`BD?46M8ospHTaLw!;vdL;$NoKATl|puunU%e= z-QWB2`Fy_rf9~UPulqjdbzbN7dcNj4iq+RuBO_rX0RVtZLtWVb0HEMUC<&Yhd^4Kc z&ja5e-Uez)KvnORW$+h$2NMlP9Ub60_!$mBAyfbXehByjJaz_P=br(98+-=xoryNQlI>ZZp72b4I!Aip5HJP8Vgl6_$B zC~csu`k(3GZ*uHTK0cn(0s{X2{`~$T{2mYP2?$9^NeKuF3kVDIfg||51KoXW0{Gm$ zIWCC&myWW7x7`D0PakIwcNCtkjje~Tj~qKYexd*UyWrEu+428Ya`*nvw!jSv;GYNx z@e2z4k2W|}7XPiZ-UDX`aAiDwc_G>Jk^jH%{K1#J^vQmsyvCT!2iT1 zPr_h1Lks|DKtoy4FaWZek9cG_e7N7&B$Cdogh9gS(6=Bj(eM})5!}$t0*GkweLgZ$ z_VBf>o9Vmtc#nPKiT1DTw_a(zWk*mdMgrHvTT1GP;adNmHto0ek)?2_XgfCbNbR33 z>>xzEH>CP@9=^Yqz4NVUF|UV#1qDFAQX(C+Cyb1S`rwR*=9_1ETa$Tj%p+pWxdSMI zen%iOJD2EXd=kRH6}PYYbvXVw+-=wo-zyyrYuG2Yx>6(@G$GWHv+PU#)7_rYwRn-J z*Th87Sgm`1a0(LCR+z-vFB;^w*_OBSI|w~h7InD(D6=j1j*-g5>I@+qgK>o|;rQ`v z2vd$3n~zCcCb>ivEfgdDkNQ#vv5Wbi^scj^Cfa^)g(HzE*WHR=5w?|O0tFVFt*Y>b zr&I}RIY_g}u6GEF`(%V47g}4{%mM=saa0uZC6U(1ltO?CIGli!$f=?)(-Hgc98r_$ zFqM1R)gT`sqa?G>zTMlYCO$nprL@Tl?TQ>zMs#vB{uon6%NN_vL}VUKA9uet|M|xs z9m5JU;KIOAM&XqdPz&Ukp?O{^vy0>trdh*?t(jfd5c+$z*&Sp0N6A?uDq>gG3oxk6 zLQ?nEzDxh)6R)&tWhshB?ru&58hNa*yqDI*0Pt*B3NY;rKxvU<5?;2JTB=%X5&>+3 z%50~jj4JJx#Occ%)z8hQ$+(-@(Xk!(!Wo!ia6y_4BZEe1zQMf4q3TvXqA3_+I0pR? zst$|~VqK9j5!hS#ZPr6GZ`iyHRVe-8{eP5elSJGSr)|j`pspmDhM}`bmjehPfp8Qg zrGl?W*#O#7p2&#W?T2-JQf3s%MYnUQr5bYaOl!oAk=>Q~;{0~{5g!+E57V=sJ;Zm6 zp7LJV1RzsLG;}(*T(xKmDVR07W^O5|ME!`$!l?i~n%P$#)ZKNP&LM-_+V9L!Nnhh7 zOV%H?hpkETahmUVK6Wbt2=tW{{f`r{1|0@pC3RBWik}CAgw(f}6jVGs)Do6vQP%6O z`YNGXP;Ik;3fo~BuOUTBq<$Qz$Fdw!+t;X?weSzG(CL1(!3SEOEfHkPxucs~K$r>4 zZ(zRAVfX1>C8jHFWB0uF7EGeN$YF*k3=AD*9Nv|ISS_#&ctJ^Kt~|QcSEga@xR~q^ zaP9V9Eu=%-Gx2n^|5N*M{u@UwfoFuV0T_%esGqAN)+tO1VQcB}s!k9~&7LjIxddZ$x>-WkBuMML9ERHs^csdRC@T`cuW(~v(}DwR zDC)*y3q`HHZ&t^_PbevhL%8~t(&2?=`6Re%578B_8$!D)-RP>uRN(V4i#~M(cE}B|8ZENCKGj(bM+_1-&90_|`61Pck1?LOBO?Rco zvloNn+-vsPm6cIQx-jP5&}mj#rkH`J^mqa9CXev-woDedjR~-FEuDB;ci^7Sa z`L7DpLK}mtd;@GW+@K3rvA2B=SBk^qhIxm{3I959g0Sc^-U=(vnWdDH&GwCA8QsDA-2#06cEycg=`4wv`)_4SF)wYy3VNJ(V?0mzdiQH|X-5RH`gkwA2~m)`s3 z(|zm6a{&Ss6J_{U7*_f3vAogCh5!^x+$-7zZvVQ_c=9G{M)U+3L zm+O6`>d^Yyox5${=8D;6;}IxG4gJ+`Lfo2arNgV=-zSK#HpK_zLAgO+N{&R{Ck!se z$+p)WNfEbpL=l4~zlrAfO7$UWx#FH{(9s=@zWt>Qle<2O#PEXzEK_|UHsbKf%feED zfM!$BulVSKrn+eEDGGruxCgWKx<6z7?*5G8GWS0&EV+szRveo5nkIZ|wAyx0L>+ej z*{gaRPC!$lux0jp*(2?q_fh{uactL?=c^b4h#|%q zXqZmdnCG~M%YH>(+bet{41hN(WBlkH^%xo3pW$YQbIk{Ucl^|#={_L%yk6k9&iey) z%uL3blw!fV3y$Vg#hh8?`m>#AsVARER{#AT96PM)JPc|;zm3pspxsq-#;N12Rr;ZA zt~vGw7}PjnS1BvY4k7Jhg_g%H#R4Hi?6EgK*E|KSJQwoP@%>(At85VK^84ts8i#5Z zGI$#*g)uCdkk0?|jk`OBE{^(UgrHY9bmAfwZRlO~fd5j}I$B6+VIgmfBn3s|jEE^S zML&ABG5dBygO~Dy0cfHqhe#KuqGE;j4_$uH?4FRu*<;3kpyfbjqF_1CBLY0F#wQEs z>M&7;T*6T>bV-+|spj{3ubWT%ap;L?UH@^36L6Mz1u-4ie_q zp1f{PzE7M3RVZ=@ULX%yo{S^^0miT~<>^B-Ciq$3?BDO=KOf(H3<|`PzHwsRPv=@Q z+VdZBnf^}$?E$EN{UkI&BK>5phy7^|82X>wrs!Y>5xIovZ1HfC!y%78=`kzd()^z( z+vCwo8E~6=)-{-S^p)Rd)?Z_25GQ2%4#L;IW;Z+x{BdWw%DTSX>d)s=GE+1F%iuoq>|xRi*GP23g!$2i`N z>$8{cA=(i{;5kmBo=ylYXF4i7&8o7PZ}gVD4<=~nS&9z}TXQ0VZ%P*OkKbRf)mA6K z>)jqvum4GAoA(84NpB31Z#4$zyS-f7y?RIY0xD zOpA%%P+5+t&l$hZNJ4GpeT&FE0szg>4CV*#aHKIbZePd*6b@COC=FK+Uj9|4OHT8g z6(&>~Dd(CiyGVgK?@4B5-DcUg59@sB<`&^Ppp~ zQ%7qe?(<;uG^d-p1p%?+K9=)bB`YmbZHYqb#N3^<5wyRQB&;w8^7>l)!CS`Vj6gKf zo#>ANhk;*@b8sj_Q=wky(usu&aU&r(G6&14ll?H1(3uB-qHA>lhJgR zYEH=PNlXW#12r884S#cQPkU|~()ZeJ_fd^FVT=|a9wzpkrC`~E-0X*$&zX2#v@;02 zP|E3W3wo!aOm_tj!}sm%@Ayp<0F&;zh_ajh+qqiA_k1M7B=9r==m;fpRqi)D&tH3Z zrO@==5Nl=%FxKBI_KD*`0BXgV5puP*LsmN+R5t+k;jD=uozpse;yUUpo8VsypQb|J zkk-g^WlkcoZwv2LKm=5R76L%OP=1zZj4m5S)OEtq;aFRmsl87h?39(wzDagg8P%d^ zuHea;s9}o|jjAhhiBAiCxle5n2#m^F>zh(1xQ=Gv({E(8pEeEatfFjB0v!hgSrZLNhq_93)rZ4S zLh|*db18_a+FV?%pWjE5E6-Mm~3Lix^BW%!loxFCoov^3O$R zrVIxr5kr29(m|V4+-ZT>^U=K`&r26;J%AF29^R=^T=b9{UngUSNPcu0eW}u3X2$V- zm4yLcnc^jk20G7Z(b}J2R6xnQH}>E5mzVz;qD%NMWcv%0ZFl0=_u=oSh>OZgoy6U? zMGBD|40RjcL_K)Ldu9XDxK2*r&L zb<5P6xNETM%Qp5slPOveuVx^O15}biT8#b1vT&mUix^l)MfG35IxQNioN!eUuO%^1 zOBOd-+wH74fr;`2X|~Vd!}Wt!Atz4yROhlqA;1yom<3guJ}YSDHI~%Qfa$y8@x4=t zdi0M#%pb&p8;>%b*hvQhQ7e6-%NssX<1hm}akL6JmBv*}j|ULrvh$!$uqZRhMW2hi zQWeN=ci~eFl9M5}-p>%SBkRq&m9R|GK+1U@R`5W7Ibfi+wEG8+eIHA`Q@LY>t}WS7 zDCyWb2hR;o5IjoRy1CotP}J9K+FrPVE{a%&jQR|X_JSTeX?uE16wY>_-><=Znm-1) zb)*d^kFO)h@3T!e838SsiEZg#UhnzptuMp=aQAC(H~e@acSLO#IXxlP+W>c8rB2u& zrOzwa3yv;d)AG=rVxnZvX}49ekYyVNq3rVgsj1{xS`{HdU~}=JGh?4fq$17 zK=JJ3pUaxarKkDIR`O#RJ(JuuJ<`&lEuW?DkBT6$|8`X8i9dAgeXMTpyfi}&TV=0_ zy40?bf10aA?J*#5EOEs5d{P_W4t47Hy8^EgEAM1yZvf5jWRuy=Il zdBjWP2a^z#-Bb>~teExYR*UjTx;zA9jF%MQ_U@>F_x6@3JyoOm2x$Qk{VunL^xqRb zz%MIL>KpQi(Kc%ys?Z1vYee*`56cYAw10Fi(A2Wqd@^TTbw92}n52Q~V6!!S;GxP+ zZ)qUK;F1zN@xUfak64~@>4X`x8ckY84ea!wB=X*+VG|z^usjM!OMtjBZ%>eZp%R7) zEyouFzcad*VTjdidB2+Sc+zW4%_;A{ao_UodwpLM89V&x$Dx^yA^Ss39-u`tF60{Q zxIkH!?b9Nxl7nP0^aTlHmE~ofWLfWT@gA$&QdHd^BqV_N(sy}!Zl2eqR0P^lJ z{60S08ai+=AFL|rA)EuaZ9uD|RxP(AC&eL)d{`9pxp?d*OrADRq4&~hZTQ!> ztPij9>qXVM{vAsv#tAK zpXTtdV#$Hhny^t-^m7>YSF-GNQST+(SOa_$3R~scyuS7fmmAWi7m$>dHTWt-wpH00 zA3%z^*EDmqPlDrIE3<611Znf7BdY)0G3?W+VTt3_1}POe{vCVl{BbWRD)44ABkWSz zA=Hs<%vUz;mK7$L1p3RWdT%i>aQM-*VTcXGEYwXOH%WYOPAXg!>yTEmTEVhJqwU z%_LPHY5J5u>}49gO~JI#kMk0;?YM?i&%`tlZYYRSKC1DU|N8n1qoSM#<(Qc7FZ+%J z;;S!2+Gy^Yi7uT?=pXDvQ6aFs*Hl91@_sM%aVrgpQ^oWPybq0T^a|bJa#eWugF>5E z6MKcS^4hhW4l+N5M7e?&PW1U-R2QchXbPFrN%t-&D~;skb0n_55d#Z?%6Np7)ap58j{#J>m(vc$s3L`S2x89POJ^8kZY9 zrJA`we-3WW`8!SqrugZ`L~KJu@&SC6GJNWH-{&LZ>KAcccL>YSD0%2(V;(u>GQFhg zXxFmMo++{2ZRt!uUTurbDQa<_v+WQ+`#X-kQ$d_Bm?~%utIkNJLx1LK=>!vM8MoIf zxNR)gD}1>US9ELOYX70uxS)Ga+5-?RU$b}U7X=#0h|i3;P$Q%f!OyIhb7)?u0>A%K zjtK>?TI)zSy%Cnb%f3hdFRN0>KAqhH>gk!5*pP}QOCzJ*Ro5#DlkZnpe|Znb(tc3p z!K+6i>VNsyp34un%Xv+q=*%9)*{t>+GE~dMWhx9<)9(MU=kt~%qd2b{tNY9OB&Hi$ zw>WNU9K(k$C(FkYhT}@L2&mU~s0M|laoGW$1Dq%$XpP&~O6qXg1ODZ&$~zcz9*i*? zsqn3^enGD_;pxip767>h&wd7J$x3Xp@H|5yzIy6kmJTJhdAX|j0^Fs`4`_Jc^RMXQ z--^)i*VB1cg;upk1jKD0XyBa6)_p@>oxV1*GyNgljSGePD+et#qAvkIU1phr9NUKW zT!J6n8Ys>77a?g1&x5F>agwpc*{-l7!&gBSW!EG#Vc9)|YmY#el3qpZrX>Wmt{N?| z!d0i!?}Mfesk$#vuE1S9w&8m$Vg_rQvo*C-79!d!{pE8clWU&;DSLkNoQ zIjd4y+<2U9_ChF5Ezo;>dFkZ#e9VzMWl=Bz%P=&Ek_%Yj8!8$st8pc{J}T>2tdqSB zfsCqwu2ezJY;bUjW2E~CBcY@n2UIU=|H`wvB`5LxZzT+xM(K1qDKdcj0^`-WW$Ls=nF7%$55?~7vLgL`&g4%vKQU}SR>jgM_bTX`UM+`(oyCtz{1JHmJ|j~W zmjdHy2`cj)W6f`;(%~;tzoQ1Y#Kj-I0ghunTlZL9Q()nbr@DU&WzkaA7ypvH@fB@qJ!xRX!qt78!%hdcnOiVV;6Ivic&+sW#4mj5oXFnQoL%Jkl0R>{u(PB)gd zcHgRQfx#V~Lx-JA7O~Ce*0qYaI`bM(s^v#CibHGK4!$XhGAsmBrHqtG=WVh=djAb% zO{|7<4Qrjy$+G_w3FEun{ZE5>mSJ4LTImw`!8|Jgxh|I~_AMm_jm8N{<32L>5SJ?s^$w2@3A)TS2LdZw2R>H7S1USDR zxp7)5nO*N+3^9y~3)|DPck=bG#}!jHv{lxn{Lm8QEqQ)u zQXgbYtjx21dYsWq zH>iq)z6+}OOG$@rGzhVS)qhGM<{1Z&%H_PfVwK1AJ8#eOw6*x>hw1Jw$#9a_T-cSU z={F?T@9;H@@V^4vk6;qdsf4YCq0A{6bwtc2s!rp(^w+{3>rL7ZJtm>1OsFHFE)qM- zv9?+T+vcx5FQ<^>E9e5$^cWYwugpN{swEj4H_uu>vPpA*@zU z1fr%21fWl1Y}e{{Bp6DAiyJBzM<<c@3l&D`}c z{3+$WRjXsf%j?ru&P$5vtI&pIz*FZ0)9N>InEN%L|MODd=`pqQ^J$Wx2`84$n|Gna zOP#(f(xDC$6d%A+LRJ@5l_YhSsD$0y+E7`c_y{qzr8kwcHyPIb*iqyLjW%t2?p}^l zdf|?9e&~~2_GzZ`g^FCE4M~79*{Rtd>6zZC3?}-q0GDbHx23W@lIp*v^1bTnC?)DM z%^!zaW5Ovg{I1sEW8zR$<{OqD3;mAUPbPY;gauUQ-b5JJ-(U5mHEIyE(NMke*9V2} zk@{42dVNIp=giefP{H6!T@BknE;dee!l;t#vgiSwyGs`$E%b1qV&y5fpN1MTe4P4# zTpIUJa+49y-5)ON9yrFNQz0Df=#UtWAYaW>B4yi+Lol7Sc8~TS zznlt6;2;t+p9P5ay8Fjjcv{bWX`1?#LbpT{^7~rh=Add(EYmm_n92l9gD2!*OHu`8 zU1nOFu_vZHO$}gUIB==;!k1{oWmgR~Y>|4z>hPF%nnGFk}8P+@CyZ@0C zuUJcV>$^}CZ5^-%_^0Q34(N`Ds`lkh3DwtMUBB*Rb~6U|N6*|l*=@mw{v114N@yk* z5VU~}6y=v4qZ}5>gFWax^=@zTa&q!yj;^rt)Z89g5FC8}p(i`lKG+c7kp<-ziE%(+ ztWStEY)kF}?0k!L$rTQ|lShosE-9*6<4WvGH9bkMP1@Af1dCWM*5S1NB8;w$q*1=R z@*cL&%K4}b#eA41NqQ5n%Z_sq0y11C#dzlq~cL98{u%&EsVLtbC#3t2jW3@y#LgY1PyX~+6ofuZ^+=XJuUddw!(}-V?_B_G$$v9Na&)6K0?ZeAK#;+ZCl{G)MiJe1FY%MW!Mu`bs6| z8vXcsDa6^y$*!#+YVRz+rh^T?`Auk81{ENe;wPl|*_bMlTS+P_M3`$utFXHKEs{7R zp+o)$AJJeHy}8lw{X$Zci-wnHI$9F}ocg>U(AhFR&{b&jGw)-XeCYt0qvl-=>>yfO z_S94S^kUC4hEV9jntgOp%Nm$j!c0~jQa7$Q2K=}c*-}SS4hj+AU z$0q?!eh!(UCujo%+FPZ$>Gir~weThKDyGbb$6ZC4G<&!2HGb(gUEg!bW#`8W3kXYs zrD+0CC5gwl^a6$cTj6;d^{>a zR9Gn$@b_4t);u&eCCl`)fS$ycsI&N1v6j{bl^_s8V4pUKIc zOfdw!*4rui<>4X8uisza`ex@V3-!qve17^M)f*Jr0}#P(N}sD3H~URRx5D zEOBSb6nr!%r5&G--V)~!v0dnnQyo=owQYNCg>~3W0;eJCrLLd7`srEWUO=^bM~b=Q zrk(xh_!Iev`1oTm{%hY^^tuR4*cKSo-_IPXpRb(hRVLl}L8wOXDV+-n|zQ_W2vKQxzVq8TKOtr(mcdP=C=i zH_<|^YXPDsg*Q8KEnkA0-%_>O@U<4}`F8Gflb14lL6g{+sgY?PJf(_WGpVH&ev3JVUz7C379V|bsZTa1F<&G+elLO zZ6(_NK=7cw$xD4YyX%gFkxUS zF|BuGmyuud88}OAl4xfd9@rzhhL}I81y82lJ`A_fq`Jj$5#Z#Y7Tmxsxd84jf45s7 zrnPO{?I|k@pBndcS^7v&&B@G1N5Nx3M}zmBV5X;6v_f9sN-fLU_6_2~o-J~m?k&vo zj-Tefy8hp<=SR@i%$G0x!6s!`6*a)tyG=>#eDk~Hn}t`us^7m$Crmhaw?(tMF+c>f zx!~FWxg-iHTEQtANEnkuAv@|%7TDU{r^b>qNve_vy3d>%wv?~T>%%Vi?S@I~0PO5O zL_%M-gy@!)78!!b`Dku@z51&y;iF13?dQS|f7+{-r!SocJgg!#pBt!u@aPJlp^nKU zanzj>zSiP1sG3u7ckrpwP3j$KnIyXlK2ZGy=n4eDeB-_qyn3EJMOBN?SrlF4bGpWn z;U=&oR{1x^F5o;{0mrLRf=nY566IfTlB=tr`bErB_Ft2AjtkIS*8a9QKcBhAdDRGu zB*3EtWqmE|85OVv)#9PxxUmPd`-haQHxyiH{r-~v9sToQ{ z^m{A=sjp;=gaNfWYtub*Yjh{?(>p`2(@n2_yhs|pLURsy~P1!P$ra10RdN4(Vkl#pEz$;mBP5E?)b|r%HB2zOaTjp6cnUc0~V5?>x@ zl;Gx^T-wP6+!_hmDYRK5oql@)b9#sg&1EP!(JV$@D2xo+QRDSDHAIFJ;Ja5rQ4am7 zM*HDm6QX~+y<*-N?t3A8cd8=)cPmo7n| zVVryQLT&*G80*?apq~ic2rDwlWrC0)Ft%<)O?W=0p<$3u4i6u`-JkG7O!t#uo>fuS z3nYv_pe%C66*_9WAFA^k>0c-n#Q`bEoGecE&;oM$zDO3p> zRXs4{BAeV4g78ag!@&7Y5>lLiOEACupT)X07maAsbAJojhi{T=K_;kxu1lQ2`diHp z1ru!-GRT># z1fl17xdr-`BzCOWh!8~3hX8@SmHkH!-F^mM7@UyRC&Js%02NZnM;maW062jG@zEb2 zO7f~@j;c`~hx=DRWQ4F*t%!IzxF zHhEmsqckrx&4AE{_V*2yIe$PyQFf%hC9%=Y@^k5Ty?H7;FOGEq*erWd?8q@rFpdGm9F9*n@i!xYHNyV!4Ob#+dWe4MwdX$?uCmhzU)YU37nUL!0PpF( zjZGT(M5qS!yi9ZDVxD>eMp6=rZOqIvQOWR&$4uMin!TT)yRdLx*g*%FOr*{|Noi$y zT<%N{*$m>~kjJlAQ;6>S&>n~U4zd3E?>L;87fuf{Z%wE^c22jDdg*ONRfQ@CpblI< zL`9l-ROJ>gtZlM*;Gcmn@Z|Fr$9Osw@C~9K zp3h`tdT{sE7qafn^NTvDX+CHU5)89afQu;a)XQjHupf@+2kZ?wT0*TGVW?28RHZN& zI@T{-{o(=e9k-%k27PnsuuO@Ka*X&t=7X{(gmeT@m=a&0`aXWDk*kZ707V9c0ub90p|p^B@ASu=pjzT1OgY@ zGC-j&nM2hfg>Q@q`5}^|S0?dGfJw3wp><9m0-&IQ>$^TjQ8Qi)|HEVLNh?T=sXQ?# zPJu_F&u{a9Gj6aW!81Qf4dQR5Qqu(dcp3og5gJ39L8G48E)pkeoZZ2n=$}x{E5$kLic! zsA>ykr#ybQ2ay=0cK;ei${2F>$8<>+l`z}Ifi~DA%OlNqC;nzydWR&8J51L4T+rY; zyrAn}UJ%9x@g{UcufYVN)-tw|7brp&9fmqW`=`@AjVP!&fEVvjyXWWBH()ResC9xJ z44p|7`Xw!6-juH(Fg0!~~iz+>7Pyb@UO~Uv?p) zBgp@U2*nEnfKGB?qnt`!V-ig_q87f#IIXBA00D2DDQWQXkkm$t=5hcJulytN*8p7| z3ZkCI{Nf^D)pKCqI_3~yU`5PS&#fNxrU2@{m5{`Enl9jh5DXPrYev?juQ4HB2>chc zDc2#*)cWa;f@E2M0oSsAaK{*`He;p=L%tQwgjd=oKVQ zf>B{AWVB>aEg(V-9C^h)CV@f2^IwJQI{n9jvM^nTkVHMlTWO7YmQVFZ3NV5Rx5wMN zUGZeWmwE;UIUcg(j#;pq=-@fwm4S?t0vte{1Qq^TD+coi78%2$3BabVHyMAHQO2xK zi(V4^uK;PFxA@TBz*5IkG&F@okC7J0DaC~R)~9+L3OG2Vq@FL=~sI!=+_8L7v85-I5!(9 zxr(O(K3WLE3kI4|H9fMEIRJfhZCSjM zTP!JmOPZxHwxTSTI+#}Y!$r8S2}d7m0mN&u$**fjnaC-p?}y(km?k=RdDxpU$wgc) zfu|rp83#ZjCP?-h+-32%llqldk+e@Lh^%l^gHb4-H(pvdg9FYkvxJrAEEautO zxc`PqCP)>t`Qd0L;7Nc$Gcl`JE`?p?C%8Ibe1Y&fB(nKO5COIUhGHbK1x;rp_ReD@ z*A}gc)Wr^vf(TKEML%so*wXndFG&JOmWEyGDu4NmR`bHuaH8~Ewq;ts4E+#?VteTK zcvgcE9~G7IU`{+mir?c0A9vlo^65h0C%Tw74X<%v_L7eHe(Beezxaz!)y_n!5JOkd zrE`g7!96=nALDfPnW=zr^9ANC(GIsXSR+y$NV9)@e&uUg0vc`Ywtu#q4S3decnD@B zXYG8kWz)JXPma%5@tz38Ak1mdhKHF7bCuKv%yAfe3mz)(gs1->>cNAmTXT98$LlFT zsep&Lo;4TNMmkQ`JJG5%#_;n6_B~TE)~bsfz++>m^=0g*E{(3Y$XWJVBVO`q!hZS} zeHbiH)*m2wS+b@pas5%=EDqu%I}8 z2|$30k;n~xOp2#TBYl1W*Iw#_3@=l7z>=3C6ix8Om7?(D8);b1Q{&Pk)2kO~@yx?m zGp`>#G=w6@nFDi`qnPQ49&1pKx!AvMvhLfhRoM}c=6B|(grAFH+6~$|=iaWy1w8U| zT})<%3?I-d7rIG>eBye`W%pO*s@mqXgUjdW|D+ogi2kx!@`+Q{CqQ<%6FZ8ZK=QXR z;)vROB3SMB7zN8Krk{RV*3C8E4&jdaS1u4L0E^sGJl2m`o4OLBV@6mtmq9qq#uH}B z5!@+u+$-gmoGR4XrsPs5LPD1rVzajTU`Lpo1(k9c((&_I4pK_PNR>;0O433wq@0!D z4xb}mr7t~XsXmvY{R*QB0x%{}A8TGI2_6mc#yx#jqO{_&yKrwfOU}0#6QcCsXWUbS7d`fKbZaP?3BeeX%1k z_w2!)nz}mObb=>ykNrT9OCj_Z6?AnS^nGDr6yyG93r)H*BsuWPL1oOanqik&7Q8+N zyEwYa>we@vh~St_@qF~-i^mLkm={Kg671>?!L<(#t61!9Ok9lhC)l7HEXx&xu%fpq z)4WMH681sGD)$8*YrVCSU3liycM{MxI2^__#~1$qulST3Vf9YsO$5%UpGTF8S0Iec z;%fh7WNkTtE?0c{FbFHr6D9lUIX`*L$_d$_AKoi)MGW@=f*4Ws!W*c14AA|jJu;x_ zZnk40vzSzC4-7>gTg6%1oEv`nSLSQLnm>8o-JO=`L0zphMM3P%QLejm!+3K<=|YLt z6{&cxy^CMYX_!$b13LpJkNhwsbG;ZfsyBzH?x|a6336O_c{@)HP62C}EM91=dd=3MGv zTCZkpIxJ}ZFte~t`jVx7ZSLLcIzkz|)cOlBY|0wP`vH7Hkly6xGmjUEPXX92_~#G( z%;8%#@#u88dzr=Q$DQxptR4}`&s9jZ)W)spxIvHgNEag_G`t8OLRi(jlcr`8flM#f znZ6iu{?wZ=V`2-NB-uaP?0Gv#9P(zV<*K z4dmAEr`g#EY%$4`e4~%G8E|D*H|ga!xfI%0#;;1ZG2hhenz&OQ{JWZd|KOY51Z(Gc z&{U5?bN({X-W$AK8`hje=_H?$^?Nz;%$*vjVo(Qe+^?YR*#A|Bd)2HUaY>;==}oP6 z)GqQk=oM7*JPANhB5hTb=5LLuKxY%*TFs^Q!hCW8SmXPI>wcz9A`y}!VqwfdBi&i6 ze9KMz0}|CM2I6^?Cz+{wR=;|Pdhl1eAvLyuO8vsUwiX5BeNepsB&KIqpe{)P75Y;gZNEsEElkCt`c@!q0-apODg zjS&>dm?p-%wt<)UH#hK_rs=@r4w0vER!I|=~shrJk zQ45JOoA?|e><8}UdTrQ{e0VB8aH=ZZ@$r*;)8nZ~#FfviIo?AS0EJnsD-0#dr=$h>G}8HG^Osy0S!%04 z|EqlQ={cvqZ|hnzx0UEmTIdNqqs3Y=7&=78FDr?#*|ZtrM7k|g)^zLYtiA<{$w3~l zTgv@Nf4tkO-ZGzuuYrH7#S8bd(>a~B3Hs+1g(~Ea3sofqY)>p{w-|G^g^Yd`d(TcA z(MW}fAo65D(W>v5XFIoe{8X@MkI*`4PWKrKWbA0MTP;dK1lQPDulr|A`jn&B_T=Yf$Q-+x%om?W?{pldp5SfHsFxO@$r9&O}p${}$8R zc^=G9W(wiTNZspF#i^Fzw;Ei)}VthMwngL2#HKpBQ;$QusFC|qH z>7MPslX$3p6*A3rpNIOsgLb^UWT0p4)Gnk@`Jw%_D^-qyYso_e2YRnPIh3T~V>;8} z!l^3hIQN zVd&tRGt;=+|Gs<=af(y6ee}@lZgS50bvK|x2fY}B@9d=r>WujF1fy>{HglDy7PE6QaLJk%bx@07_65)V+dvs_bcy~u~J;zl10AOMBJwUS&|t_0lR%m%d4pXRfpf+w8w z5gJ0k_@>>zY{D$_j1mSzuEaSxOBILQb`f&xEnM zsfS@rA2;|8H)iQPa!vJhPlA87l9JvKgyFmGag@jp2TitVZ2L$^vncESUg#T5O#~VX zSGXzum*UWPP2;3u{=om<`ZcMagIXtPu1_9Wke-S)y{H7cu0_|wzU&V_eRm>fW^^h) z$`>Bq9eg@v|HJ^ie9NA_jx?%b^a0V*wilJV@sdd@!SW^d)U`Ic#6a7TJzc2?R#15U zh>Xlw(cZU(O)sI7w^5s2vn_ALc?=ZFU|1i{$N)*o$L?9LC6sdcCnQu_`&VCrkhh^* zE&f5O6|5k@E)@W>;X+xd<9a|F{0zDGs~!NZjpP3>fH_=?>GzTH%!d2oYJaGMNs1i&=lQhR$v}(qgkip>9g)n!RU+ zOA5M0BynH^`>HC+X_HvvL6vPC+kPla{&jZxo3l`lxrYuKP^4%HPHnyP7d0mHVEB>U zRAgJI2czGEN8kyBI@HH5R_a+{_TDFsulL4 zMb=LV`ij^=*azP|@2gmw0`h_5jN<>oEdb^ESmofo|_3HL?5`t{mv$x>h`n%r+ORQ`~ zE$H`R&QTU$_T4Y5qukS11U*#1ZL}bVizShhwz2m0y%DwT5v#^~M@6qyjr$B&i`?JAoQl+Bm5vE6 z`7XV8(dAAuH%H;TrSU>A3mqepZ*w zMu;2kXT>0ug4SHES<8oiB4V8Ze|OdH?cSCoQYVI3QDr}v3VXA3hhyaAVbR|5g2Mb4 zFnY|GP1-OL9f#_Ak%_o1ekbN0IkTG6*S7T{Il`UfrO?jmC7PJz{A<4%09q7EBy6@W z=SoiPslY(Y_cnX?O3Algo$AiKAwATX`sGv!jbm4@zk1DMi$T4rjN5%?tjoU@z``h- z5z@=!xYWgJAQCV>Jm@$M^<-5lqK}`Ms<4}_m(BKM_@*JE@0ta zXeOV#^1QO13j%kVd2mz|oEcZs`*)!3eXu_>{mx;8FNc~;zo@zeOWyXweN4>1j?yR zsSf;u#{Ek>?I-U%-kCbtE)XE^plY{Yk}~E|<`w?heIwf|$BtzNY%-&`!|H^K%NRPE zd~E#GzlBRa$JcIl#_$F{$Tam`=HMj*#s?+jW%J2ool{Mx*n?Ejxxx^KSvILK%RE;a zJKD?w_9orW1Q08v3GFK!f-va+wQ$v8Q9fOCSr*s@kuH%g0V(NTkOpZa1VO2#1Oy4G zT_mJS>24H|SOm#MY3VNM?vPsQTl{_dJp11}?|WzF&YhWi&pDZVfhStAIwY@;%fJ)k zng);i9}tf*r>VexCY%)iy1goFvc;R9aKkeqdE_XjW>H-pQ9WPxcE~pReXtk6Wr6_)@&6lIHLs6aP@t^EP`I?Q`g^Ee}DRdYSh0{R^nEn zTUGByj0a`TYho16mO3m12|5phdq+K*$qADJdvC9xgbH z%|l^H)wi*Xa3KbQRkZ#ERKjOIi9yVViOn_f#E&LrC;X$71+)Hg>h z=FxIyL!jxg0Rg(u>*Xm|;IE)pk9m9@*94+{LY@*I@FSL<#0?SK^Gn{U(%&CB%W-Au zZF=|EPi*|K@L5!89;AdXWs6qv;9C7m)5)vw;yf-<3EQ%YOzd;0m-XV$;bm(od_Tr) zAOcfuh^2Jo-v^5t;4}>;Kg3Gas%i3M?VRl!8V=n|GNG7=(==}9+#G;C)PYxKq|kq1pG zGCBq+s5NYPu=NgsP~(IaC`^^Be%?{Ug$l$|RsW;So6#ZFt-Ov2={`)u3HAUFo>6%|`};*vXH)FH^?#9FIGaY8V&iiLVoKN?E8eq~o5u@Ke6 z8EQf7f`GbC&Al9Qx&FQKK9yi^ahMP&e;MHMqo8^6qH_{QP*WsXi566ueLT0QqXbN^ z4#jodm0Yd<{f_2DjtpG)Ec=}5nGdgy?``#Uj>8)51ss zD5Pbtx1aE~{*RI@#bP-dz`-^tKojeRjpDF)TNP*y22DOlrP5yN-XJCivqCMlun9$} zANU}?SQL$d>b!G{iCeg``kb0rw@iQ*BHFBuQc$gvOxR=4D<-VDBwk3P(Ro|%{%`@a z;aZ6i^*s#)^e0G-Djah_-U{Fh;&;A&pf(BMD%P~{)D&7b_rP03Cvp3A&-W>A_XX}m zQ=A&GqAscfj~!rbu$J(n92yKTn*RiW?FZZc`$I?`-L9~m+h_>>n5Eypsbk|QR2~gT zZY?-OecRj=Y`_loQ-pwBnFJ#zl=zYbG={*8w4fz+{?+D=N_p7!9qX|-0*96*{dy=2 zWvT^u-p}itOUTpPhd2NrG$DihV=hs44iS{SXqzG&7h;eU)Z*y+WP<;VzRsgM!w|e_ z!C>MW^p=^lM}fBN-j{N7pjhdr=Gi2jai7!0GcR03KX`BzUNjiv6%PHmcmXFu0^@!k+wexO56pDm@m;@4Y%%RyPD}+m z#R?*i_Id}>N$=i6pD^YKOsDdz|BGe}S{r4X6CGJ~!eke@}grEUAk_IN@? zPBbQn^AUR`xw~*a^*xZ2t-X=UdtOE-J5fE~J9UH-kedFAlGI_2 z&xg3&g!VL4Ijh(E$5Rxj1A4_edea1=?53AkE#-v+$g=m1PQAn&eVwm`wIs$vxl^kp=D~aZ!lPnn5V|?x2rngX@M=S&!rX8!!)g0jK>bgy zUA%JGE4zSK)cbKRv!J}My}3id_$VtsaqoHeclZNH+VCAX(I>h(QkH7kfk4i^~GHoH|}# z64$Sk-FI(gYFE@V0Q{-Kl53;2<}6~%vp`TwCi9@mhtgZE2rH?ItuH_Dq$p?tU1Olz zXQQ}oELnIoWZ1f-%wA7BGJnsh%>?Wjb56-wZ(_2=4Q4SynWhHg12vLMJsALPvgnsa zCdD)zT<A`e1xV{whUnQ?_KA(Jc0!Jw{i%;R{WuAlM8wezZU!JWC+E{k`H7q_AQ^#q72BCrkZCcq{3VNq!xu}VV zE8gpfHN*Ul-f7UGMw<0uwE_l)m*cj|$l0#<+&;&Cen?oB<7mp%Qx8L4gD$#JSyhH0MO7b{NBy|4J1llaliq&vXnW7!U@^d=0+O2U zq6}7~)%*-@?REIxZ<&Zq6sBTgkXjyTL zioguP*j{j<6j*-o$4*5Zc7js(DKjk~m>23#5$NBRna^+e% zm{*Cnsi?DK<(ZM8UJ7PYTN$?&v5RsZ`b}2$6rj#H$cDH^+Ar|wupAB2CdqDhUH@p% z4EzcInLrZJ7JkCTK+A`!j9)>&sE~fYQv1zx2292TChz(Rm@f&GJ4c^9m${I-Fc(-d$g(X4WE?nLic; z+6&FT!))v~Yzg`pN-7C~y4w@NUybD1qX9-K59m#F*#}ixD5(TqNlA!Nmp;xi+uM4Z z`6=%(4(s@mMV*>z#5b4w(T?5UiEf!|d%&X7+RRj1Kx&cV<*SnH*=NtCIph6ojA=xb z$qKw5r2vxu!di9@k@MfZIiXXxjl;h9jI3-xA+Czgf-*_8OG8`SnengRYN=s)R~)rA z9PMV7yZYnQOem7S{%-q|+)-=1|Ahz+ZAbK>hQ=IzTXw^BR8hlqSt8G4Ycc?t)li`E z*2K6K1D>8=Bb{-yW1DgpAM*SmQP%uQ~gMHFLA34cc-t zO6!X*j`gO}4z!x#@T{g*37}mMio(UGYV#etaIrCz;Q|76>@@eL@V(ryZhjG8F^ebG zKpSAi$M4~0K@B8@y-#?tm#Gr(M}n6lMj+TE6Z`LdoDU?KwR{Tz` zMp`cduW?H4GM}~_4dmz~*?odNE8;^gjvGCn36X(V( z<4Ri}WdV{c2-XK01Knf{pLY%70F{XcHzzjjO}fzWuVA00)7-v%Ex$B8Cb2&FP6lMj zYOXWne<-F-?aRD-Pa-blq*J%@uVy&t7${Apyb74Uslermw7+Fk4`&MMONaI#?DUXMQ>ezQ%Qn+i3 z`IH`!NLmsQw+J8SL=I&QM9N3`DKyQdA#GrEf9_vCi`sN9tzVkT;N7n3lQwJErz1i0 z^PYq8{mGqLBvaQ;(>3o4I=XF9{bFAJlRgkI{WOBoq9j|-iMP$^v*+3Q2fe-cljRf; z3dHYmQ!4OR{grrATdYM@7I-uv8BnzQu^pH69LsF~2L49#SdOtr<{|m8jNU5-pxe1j zS#XvG*dG`*p5o(`u9in%G+6CjCXsb>P!R?A);H}s!qFO1U7f>Mf| z?pOQ0S8~Z3^As0Sp8JWYPtOLg>d40r2)Rb`8#Q-q2#ca=BhQL%V>n`Ej)_DoUC%zi zf*#;=3wq3o1oIXxJPDU}8iK*lGzOk){#G&G4rT9ovG@-MZvbv~;IY|N0wT{UJlPKU}ba&oZ^odp8f2DUhiTq*DM7>m3%j!ij!c=wGnPb71mvA3(sF zs#-s1=WIK%W|p$NW+bLkTOxF%BwqD^Nb5d!S<6}_ZWN7R3FLn9&^_&c&>KzCK|G;* z=NGCxerx6K!(To94$;{pb#ev$^Ns{s?3l6q%IAZ|<~7+rux@V0{#TIBWPRx z`cT#uXT)SHqSODc$pH`^AO*CK zyJ2f7H2?N+H+sq16x25I+3kHYOMj2Flq7I)ob>Y2KjNifO;hKcK-#~-^ZH#v*f}3Z zwfp1mNi@enIW6$TX8di`pHFe8$aQ6a#?jr)-ZDt|_<7{hd?yI_A1~<)^3h^>^LN$v zfHV)YQQ+S1I1@zcJ%O?#WofR0=QK~#`A7kRH2jj4jpg{NEe}wfcV9e^#wuTL zR8_~pd)q`M0^355931zEi_7kykyo~j0gwf4+QEEOtq@arVQtUjq1fAH>|jii>m@5A z@PuU>H$d`>t&@;vX4^6h`1MkuYF`{(VWR9S+*W;3;yF*t?*Qt9Shd>=la1bCis2(5 zg&36>il(TAJ;SF_!5`93c`I9*kSoq;H`)8UaYmM=!@d}(fWmj_i+=7-M{1c!H%C(P z2@X*3U3EzVDmX05K)zS`UeV;wlwTdV@PY=nT~qSqF?Zr@#%fr1eDe>`;Au`l(}OhZ zX+n3pg|hFKUYKmkU2&HUi)30P$A6ILMHI43ZO3)qnvpXzTh*=QOC9&+JZEZLVf`&B zKwe8!p+r<+#rOTi4v6!WFP zjZ&Puqd60DdjKhV`fMJOmOw5)3yeJw~s0E&^=?!%Uh$`&cS3rsKZ2PPo! zsaVT3bKTy$bG{~xGu>azly_57kpK{_n(vvqf(Fp<8dwxDKpd4pR3n(Uyi5T>8gaEI zB>|?%Ox-RL43Jz45)vV+h~3tOf9$L_Mn+_a;8yhQ_m4K^0g_4rt6D?IN@t(SV^P`; z&#wT9OQAPDM}G)AKiwa(^IQ1T-@iWUU3&PE=v(&bM|&=#V@{ z+Ev&zCUDw$_#J$GhVG~Oe~rYz8##ZLw{ZVF&npTj$MBL;AJ$5DAtRpqhl;k7)K@9` z%0dph5@}m1galtx^9>#@u?#H!Q{RDOzjBOm7-M7pMq4UFgtvM!IE4}&1K^MdL{}&t}%UEb8MP1<)YpyEY>K(d4_JteXSXT z^CYZFoq!b1+ZFALUV7P@h|-miu8Lq5!&DOgtELv(mJLk%nj;F&bY7P*Ivq=fdZ2w6 zl~gh%f^{%mF}0g* z;cBuyjq=~R>RbB>h8i+Vdvtd4Jf^5V z;pv&0Q4)Jn^5wB}R#Ws!bu@^vUxcQ?qJsu^Sv?li2D$eSKb1`eln2T7&E4dwRM-s; zYHCXQ!3cH_W#=4xt~v=wAkfDA;@J%x!4Nx*ulY&WWW^!5cg6yMbQlEJ&JQoA>1G-- zNqUdJf9OBZl6~FVy2#9X$@Vq`#Ms&aC5>g||3~FO@Pa|9TBdO|yXDNhfp%u(mD?`Z zWF3Ed8hn~(dm<=k)iViEu z5_!6B&wHu>bR93p#j#9ZXs}>)7Q*ny?pmSFKuDD=gQ*w$vccOaO{GC{Gnc-#OMB__ zMs|AdUSL2MuvQ&6+e~8cLAw>cB(4dHjQy_2tqBFm{+^R45+8iBLe8x+xc5}nNP=TM z>G|YQJRvK9vSzK$n~HM7WTOQmUU|RbBj+9e?mQLhHa!>dMbO?hGXo!|Wp_Ud%*_f2 z8{8^%+$Ic4tyzajs)|m0_sAnHx-$++2Sjwaa#xAY3#Nu4dld}wj$@wPgFqc+AXz-V zdx4MVVozBOr1%zlivLX}qYF9KoBWofrjt2G9#fZD9gYSm)P7-s|Cy)v0}dBHfTRpI z3uR)V^@EN)t4m$UojZ|9j9D?5k;3+1bZ zp828L43EuR#D%c8RX{qOjKx1_wyzSSV5**3c9ByVSeTr!u+LY5wzbguW=9fpkMQ3W z6j2SHp+V)}%h6p2g<}#^?0rz-q|C|6@*1}yG5kCKqi4@n^f9z`{;|(mZkr}sbzc^i zz~Db}cfoxq17DxS=s>ouS$0qTG9HC*-0wcLyX08+RZqp^Y2&d=f2kXKisJA~Ce?ElzT@EFMGTV5M|^LkiH3=J!tM7cUg z6z^g9SKB0K`=JpIex{eIjUx7U5g6m@EETHMjzEc94+T=+Fc71hMG0lVWskQ{8W6FW z9u|;qPjK?j^NK)Uu$8Q?rnxiU4-_4~AQj=oG71zBXFj(bh1;bmw z+C?Y2CN}x*?TQ2A7u;{R^Av#=(j!5SDS01fbP>u);)MqWapm&=H=U?o;y6BU$}tIm z6Z4I6QI{%R0qa0A$tAniaA6MN^PM*eb0DG}w zvw5DI)xgv}^kTx0AI6qRyAXHC8cYcknk?uqED^As24Vq>3qG4;cCr*+3p4hNLu3sy ze7DogWVOef5WGI1?&sJQU~;p3XG&sk6*1`g@>7_Bpv?_iy9U`YOksfTnyQrMT@f9p1raWRU;i={GoAT(+~@kT&txQX{=qBcG2Ke+H85IuqY^wh@HZhIsN zz~`dSs>%J8okaq#f1B9#qK!OfrGhoaN;WW3yU5)KlM%mu2B`^;Pyl8_u_^mntiCL) z^#xVvYa`YI6);D%eDHx2uAog>gX7S)s)2Gr5Jo;zg*ru&B7Du3Gqu~CKc@+3!b z-;L?yQ@BK~VM<|Nd8S$OFM6ViX&S6vtc}R_S-1+s-d zQn~BUJ6baI>D|PBj6vBVzs?&>j`7JDX@2_22H1uKW%(oMFJ+HCynv)*7~KGh(~YwgOb-uZN2VJw1_(66P6Xt>-U`{peEaq_3hU_xiyCI0&a?HIWN%eZ}ECBTXU z*>4I8p1Kt^rndb`UDR77KNxu1(&QEsmHTn|ym$75X_XD%fO(Qpr&7Ln!i0o@cwPyA zdQJzb(N-*XGbk%0Lq+fk9lT)UZM<*Q2-D6dNZt zSG>a@)kn%cF@L7^3)24V_4i*jXU*^JINRFVsxEi|b<00V2+Yvb?VOL9dT?Oj0`~r@ zPnSJt@VWupSTVS^KN`xgg94xpRf-Qx!wB(}VF+Fd;vTQvzc zjMGv4CTNMv3I#TEz#~*j^-9F&6<@bp(uZ^cFpnnx8h*7GG)5M<>(ifV8RUg~aiXQ1 z+4}*cg0cnWpBvZ-uFu#pRAt2}n+*U^dC}^jQ`RR2PqCkz#}ikH&|L@+kuqXRur5jm zK7{)xvQu8`OCOrG9ZOVj0f`c)?pBM-@)MXpL50@l=1wOPW5x|LW5JcK`UNrW--{vh zo?ZRl0F-7+X7iVN2?PJb4#~=80{P^nkpZ|*EC6Dp0cb+6>TNqD@l+Y*?dJ;s4l;f> zP)XSg_2ynRK8*p}(JS`++|zU8hQz{GK{u_PJ}-BC>h_Nv_WaK(R!#gkVzA)*rkSY= z;ppyPk@xx)?TZZHovQYe$>AVJCJUEgz7aL)$hDh{?;mrcZ^@S4sR#1|Hkey4x*=loltw0c!Od6$%tIRq z94nNVhl$80Lpqv^CbZ1HlRNo;ytt^&0AK@Vq|-J1z#w^b2YYQ%F$79+vp3Z{4ggt+ zL}{9W*C;9WeSG@bgfqme195CI4dA6Lwb{@{0pfZuOJN{lJ$nsEK&}IXoJYeyz5X`- z_6C8q%eP;5Qq9tg4Q<_-ZMV!`2g!e%Pb$LQX`m&<)>=M94Q8&lNvpy^Rm!NVSCOen z46y_gimyYf40+Yx(qerF)b+8@BW+EEe3h6>Y;$G~eu-tPNj*ZaC%{iYE^qcaZQrlc z{R|G!V?C`s3FJKZ1fm9{lai3-tD)>jVzF=1L&o8q6$8ckXOKC?rLRjo%ky(BBp4H)~B-#)LIMHVAWiQIKymC{(lPHt;nEUP^2M zh^6R#oF4%-7Lqbz>wC=E_pp?9ye88P(*b&t4b)5Zn!$mZQQrCCyoYrO&JfHxDcg4A zzJi6eA};L*wHD-zbW>h97)e09qEYvKE4KThS=Rm#dh*O~i*MUiaAQ2fC7AZgNujv8 zo|FejWI;X5uads#{6Jp|$iX&<5I`F~Vw#72F*{`o-InI$c%u@BnQL-60pm4@ezR;| z%YFLt_UhhCooVY7zBsm6I0twKb$RK`M%wM^Rl5%f@b18@uT0m^E%(Zdbj`Scui5v! z_JWV(ZgjP3SQ+ENL9&w5C49pNIQysG3RZ!bmuqNSXWFsw->sX###4T-Up_&fmg59s zrjSF{O1!Ltuk(#bH9VT<#jcm7d66;f#tQJyC5rt>Gd0EQl~?k(`|$o0Fj)Tkc7e=1 z@g#F(WH`LkoFUBVB+4GYPaG4T#67VHHy~K+B#5{Tp+|l9c4VtKyj>VbyO-!J5iax^U$@FSo7QyplQ+;yzlP@Xj#}dy z+HNFKz(HKdc`cl& zH$Bk`LsYbT@4rb)iwjbbc0I<7LdxpzSu+)jl`k*w(VYaHc)%^CzjSpZ?9uIQZmq%1 zZ%b7Bi{4GVGKtD5H7xt)be=hIY_`RjI>v+b{QaM$MI~9{VpCm%QiGe5jr4b9Tu0kF zC0W~ee@2s^AX;MI@?d^f1&d#hnzfBt9t{!U*rR*$Yf zHX{QO+dA{|GyfKw$OaUa;iKgGwTq$#DEVJRYkE z;(Eo0VwcJ?4W6Msuwco%^%cqh3l0Xcmu3<>I!ij#fx~snR@pWbM1W7!1;m}{4usq0 zE@wAC))MG(hm8G+>`zN`8FHq`$gQzcdg`!WZF`^9LKI_#$7Rx%sS4vmLOhdqxosa8 z9@+JROF)U%kVJJC`PatjoAT9euSnm_wbPqUdSb53PF@549B2ziQ8hE+^^A6fB696{CP3~PSr)*5ClGQ|M= z`Wv|i98QKAv28!)Y-z`rwc0XPEnBcB3p1=8U3gbA>YmEJj*M34TsERfQtEKk>3q2T yDUzdLn(rMmeOJ=qvE&EWxgQKH-87JuSX7I*aWvhnN@F7c^HWvSR4A9X2>c(8z|q$L literal 0 HcmV?d00001 diff --git a/demo-authorizationserver/src/main/resources/templates/consent.html b/demo-authorizationserver/src/main/resources/templates/consent.html new file mode 100644 index 0000000..5a4a5f6 --- /dev/null +++ b/demo-authorizationserver/src/main/resources/templates/consent.html @@ -0,0 +1,104 @@ + + + + + + Custom consent page - Consent required + + + + +

+
+

App permissions

+
+
+
+

+ The application + + wants to access your account + +

+
+
+
+
+

+ You have provided the code + . + Verify that this code matches what is shown on your device. +

+
+
+
+
+

+ The following permissions are requested by the above app.
+ Please review these and consent if you approve. +

+
+
+
+
+
+ + + + +
+ + +

+
+ +

+ You have already granted the following permissions to the above app: +

+
+ + +

+
+ +
+ +
+
+ +
+
+
+
+
+
+

+ + Your consent to provide access is required.
+ If you do not approve, click Cancel, in which case no information will be shared with the app. +
+

+
+
+
+ + diff --git a/demo-authorizationserver/src/main/resources/templates/device-activate.html b/demo-authorizationserver/src/main/resources/templates/device-activate.html new file mode 100644 index 0000000..bea7392 --- /dev/null +++ b/demo-authorizationserver/src/main/resources/templates/device-activate.html @@ -0,0 +1,33 @@ + + + + + + Spring Authorization Server sample + + + +
+
+
+

Device Activation

+

Enter the activation code to authorize the device.

+
+
+
+ + +
+
+ +
+
+
+
+
+ Devices +
+
+
+ + diff --git a/demo-authorizationserver/src/main/resources/templates/device-activated.html b/demo-authorizationserver/src/main/resources/templates/device-activated.html new file mode 100644 index 0000000..ef180ba --- /dev/null +++ b/demo-authorizationserver/src/main/resources/templates/device-activated.html @@ -0,0 +1,25 @@ + + + + + + Spring Authorization Server sample + + + +
+
+
+

Success!

+

+ You have successfully activated your device.
+ Please return to your device to continue. +

+
+
+ Devices +
+
+
+ + diff --git a/demo-authorizationserver/src/main/resources/templates/error.html b/demo-authorizationserver/src/main/resources/templates/error.html new file mode 100644 index 0000000..eceb5ce --- /dev/null +++ b/demo-authorizationserver/src/main/resources/templates/error.html @@ -0,0 +1,19 @@ + + + + + + Spring Authorization Server sample + + + +
+
+
+

+

+
+
+
+ + diff --git a/demo-authorizationserver/src/main/resources/templates/login.html b/demo-authorizationserver/src/main/resources/templates/login.html new file mode 100644 index 0000000..6bd7c48 --- /dev/null +++ b/demo-authorizationserver/src/main/resources/templates/login.html @@ -0,0 +1,42 @@ + + + + + + Spring Authorization Server sample + + + + +
+ +
+ + diff --git a/client-server/.gitignore b/demo-client/.gitignore similarity index 100% rename from client-server/.gitignore rename to demo-client/.gitignore diff --git a/client-server/mvnw b/demo-client/mvnw similarity index 100% rename from client-server/mvnw rename to demo-client/mvnw diff --git a/client-server/mvnw.cmd b/demo-client/mvnw.cmd similarity index 100% rename from client-server/mvnw.cmd rename to demo-client/mvnw.cmd diff --git a/demo-client/pom.xml b/demo-client/pom.xml new file mode 100644 index 0000000..d7b8a9b --- /dev/null +++ b/demo-client/pom.xml @@ -0,0 +1,89 @@ + + + + sample-demo + com.sample.demo + 1.0-SNAPSHOT + + + + 4.0.0 + + demo-client + + + + + org.springframework.boot + spring-boot-starter-security + + + + org.springframework.boot + spring-boot-starter-thymeleaf + + + + org.springframework.boot + spring-boot-starter-oauth2-client + + + + org.springframework + spring-webflux + + + + io.projectreactor.netty + reactor-netty + + + + org.webjars + webjars-locator-core + + + + org.webjars + bootstrap + 5.2.3 + + + + + org.webjars + popper.js + 2.9.3 + + + + + org.webjars + jquery + 3.6.4 + + + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-tomcat + + + + + + org.springframework.boot + spring-boot-starter-undertow + + + + + \ No newline at end of file diff --git a/demo-client/src/main/java/sample/DemoClientApplication.java b/demo-client/src/main/java/sample/DemoClientApplication.java new file mode 100644 index 0000000..ce73e4d --- /dev/null +++ b/demo-client/src/main/java/sample/DemoClientApplication.java @@ -0,0 +1,32 @@ +/* + * Copyright 2020-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package sample; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * @author Joe Grandja + * @since 0.0.1 + */ +@SpringBootApplication +public class DemoClientApplication { + + public static void main(String[] args) { + SpringApplication.run(DemoClientApplication.class, args); + } + +} diff --git a/demo-client/src/main/java/sample/authorization/DeviceCodeOAuth2AuthorizedClientProvider.java b/demo-client/src/main/java/sample/authorization/DeviceCodeOAuth2AuthorizedClientProvider.java new file mode 100644 index 0000000..ce0fe95 --- /dev/null +++ b/demo-client/src/main/java/sample/authorization/DeviceCodeOAuth2AuthorizedClientProvider.java @@ -0,0 +1,119 @@ +/* + * Copyright 2020-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package sample.authorization; + +import java.time.Clock; +import java.time.Duration; +import java.util.Collections; +import java.util.Map; +import java.util.function.Function; + +import jakarta.servlet.http.HttpServletRequest; + +import org.springframework.security.oauth2.client.ClientAuthorizationException; +import org.springframework.security.oauth2.client.OAuth2AuthorizationContext; +import org.springframework.security.oauth2.client.OAuth2AuthorizeRequest; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClientProvider; +import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.OAuth2AuthorizationException; +import org.springframework.security.oauth2.core.OAuth2Token; +import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse; +import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; +import org.springframework.util.Assert; + +/** + * @author Steve Riesenberg + * @since 1.1 + */ +public final class DeviceCodeOAuth2AuthorizedClientProvider implements OAuth2AuthorizedClientProvider { + + private OAuth2AccessTokenResponseClient accessTokenResponseClient = + new OAuth2DeviceAccessTokenResponseClient(); + + private Duration clockSkew = Duration.ofSeconds(60); + + private Clock clock = Clock.systemUTC(); + + public void setAccessTokenResponseClient(OAuth2AccessTokenResponseClient accessTokenResponseClient) { + this.accessTokenResponseClient = accessTokenResponseClient; + } + + public void setClockSkew(Duration clockSkew) { + this.clockSkew = clockSkew; + } + + public void setClock(Clock clock) { + this.clock = clock; + } + + @Override + public OAuth2AuthorizedClient authorize(OAuth2AuthorizationContext context) { + Assert.notNull(context, "context cannot be null"); + ClientRegistration clientRegistration = context.getClientRegistration(); + if (!AuthorizationGrantType.DEVICE_CODE.equals(clientRegistration.getAuthorizationGrantType())) { + return null; + } + OAuth2AuthorizedClient authorizedClient = context.getAuthorizedClient(); + if (authorizedClient != null && !hasTokenExpired(authorizedClient.getAccessToken())) { + // If client is already authorized but access token is NOT expired than no + // need for re-authorization + return null; + } + if (authorizedClient != null && authorizedClient.getRefreshToken() != null) { + // If client is already authorized but access token is expired and a + // refresh token is available, delegate to refresh_token. + return null; + } + // ***************************************************************** + // Get device_code set via DefaultOAuth2AuthorizedClientManager#setContextAttributesMapper() + // ***************************************************************** + String deviceCode = context.getAttribute(OAuth2ParameterNames.DEVICE_CODE); + // Attempt to authorize the client, which will repeatedly fail until the user grants authorization + OAuth2DeviceGrantRequest deviceGrantRequest = new OAuth2DeviceGrantRequest(clientRegistration, deviceCode); + OAuth2AccessTokenResponse tokenResponse = getTokenResponse(clientRegistration, deviceGrantRequest); + return new OAuth2AuthorizedClient(clientRegistration, context.getPrincipal().getName(), + tokenResponse.getAccessToken(), tokenResponse.getRefreshToken()); + } + + private OAuth2AccessTokenResponse getTokenResponse(ClientRegistration clientRegistration, + OAuth2DeviceGrantRequest deviceGrantRequest) { + try { + return this.accessTokenResponseClient.getTokenResponse(deviceGrantRequest); + } catch (OAuth2AuthorizationException ex) { + throw new ClientAuthorizationException(ex.getError(), clientRegistration.getRegistrationId(), ex); + } + } + + private boolean hasTokenExpired(OAuth2Token token) { + return this.clock.instant().isAfter(token.getExpiresAt().minus(this.clockSkew)); + } + + public static Function> deviceCodeContextAttributesMapper() { + return (authorizeRequest) -> { + HttpServletRequest request = authorizeRequest.getAttribute(HttpServletRequest.class.getName()); + Assert.notNull(request, "request cannot be null"); + + // Obtain device code from request + String deviceCode = request.getParameter(OAuth2ParameterNames.DEVICE_CODE); + return (deviceCode != null) ? Collections.singletonMap(OAuth2ParameterNames.DEVICE_CODE, deviceCode) : + Collections.emptyMap(); + }; + } + +} diff --git a/demo-client/src/main/java/sample/authorization/OAuth2DeviceAccessTokenResponseClient.java b/demo-client/src/main/java/sample/authorization/OAuth2DeviceAccessTokenResponseClient.java new file mode 100644 index 0000000..2c3486f --- /dev/null +++ b/demo-client/src/main/java/sample/authorization/OAuth2DeviceAccessTokenResponseClient.java @@ -0,0 +1,99 @@ +/* + * Copyright 2020-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package sample.authorization; + +import java.util.Arrays; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.RequestEntity; +import org.springframework.http.converter.FormHttpMessageConverter; +import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient; +import org.springframework.security.oauth2.client.http.OAuth2ErrorResponseErrorHandler; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.core.ClientAuthenticationMethod; +import org.springframework.security.oauth2.core.OAuth2AuthorizationException; +import org.springframework.security.oauth2.core.OAuth2Error; +import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse; +import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; +import org.springframework.security.oauth2.core.http.converter.OAuth2AccessTokenResponseHttpMessageConverter; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestClientException; +import org.springframework.web.client.RestOperations; +import org.springframework.web.client.RestTemplate; + +/** + * @author Steve Riesenberg + * @since 1.1 + */ +public final class OAuth2DeviceAccessTokenResponseClient implements OAuth2AccessTokenResponseClient { + + private RestOperations restOperations; + + public OAuth2DeviceAccessTokenResponseClient() { + RestTemplate restTemplate = new RestTemplate(Arrays.asList(new FormHttpMessageConverter(), + new OAuth2AccessTokenResponseHttpMessageConverter())); + restTemplate.setErrorHandler(new OAuth2ErrorResponseErrorHandler()); + this.restOperations = restTemplate; + } + + public void setRestOperations(RestOperations restOperations) { + this.restOperations = restOperations; + } + + @Override + public OAuth2AccessTokenResponse getTokenResponse(OAuth2DeviceGrantRequest deviceGrantRequest) { + ClientRegistration clientRegistration = deviceGrantRequest.getClientRegistration(); + + HttpHeaders headers = new HttpHeaders(); + /* + * This sample demonstrates the use of a public client that does not + * store credentials or authenticate with the authorization server. + * + * See DeviceClientAuthenticationProvider in the authorization server + * sample for an example customization that allows public clients. + * + * For a confidential client, change the client-authentication-method + * to client_secret_basic and set the client-secret to send the + * OAuth 2.0 Token Request with a clientId/clientSecret. + */ + if (!clientRegistration.getClientAuthenticationMethod().equals(ClientAuthenticationMethod.NONE)) { + headers.setBasicAuth(clientRegistration.getClientId(), clientRegistration.getClientSecret()); + } + + MultiValueMap requestParameters = new LinkedMultiValueMap<>(); + requestParameters.add(OAuth2ParameterNames.GRANT_TYPE, deviceGrantRequest.getGrantType().getValue()); + requestParameters.add(OAuth2ParameterNames.CLIENT_ID, clientRegistration.getClientId()); + requestParameters.add(OAuth2ParameterNames.DEVICE_CODE, deviceGrantRequest.getDeviceCode()); + + // @formatter:off + RequestEntity> requestEntity = + RequestEntity.post(deviceGrantRequest.getClientRegistration().getProviderDetails().getTokenUri()) + .headers(headers) + .body(requestParameters); + // @formatter:on + + try { + return this.restOperations.exchange(requestEntity, OAuth2AccessTokenResponse.class).getBody(); + } catch (RestClientException ex) { + OAuth2Error oauth2Error = new OAuth2Error("invalid_token_response", + "An error occurred while attempting to retrieve the OAuth 2.0 Access Token Response: " + + ex.getMessage(), null); + throw new OAuth2AuthorizationException(oauth2Error, ex); + } + } + +} diff --git a/demo-client/src/main/java/sample/authorization/OAuth2DeviceGrantRequest.java b/demo-client/src/main/java/sample/authorization/OAuth2DeviceGrantRequest.java new file mode 100644 index 0000000..5687e26 --- /dev/null +++ b/demo-client/src/main/java/sample/authorization/OAuth2DeviceGrantRequest.java @@ -0,0 +1,41 @@ +/* + * Copyright 2020-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package sample.authorization; + +import org.springframework.security.oauth2.client.endpoint.AbstractOAuth2AuthorizationGrantRequest; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.util.Assert; + +/** + * @author Steve Riesenberg + * @since 1.1 + */ +public final class OAuth2DeviceGrantRequest extends AbstractOAuth2AuthorizationGrantRequest { + + private final String deviceCode; + + public OAuth2DeviceGrantRequest(ClientRegistration clientRegistration, String deviceCode) { + super(AuthorizationGrantType.DEVICE_CODE, clientRegistration); + Assert.hasText(deviceCode, "deviceCode cannot be empty"); + this.deviceCode = deviceCode; + } + + public String getDeviceCode() { + return this.deviceCode; + } + +} diff --git a/demo-client/src/main/java/sample/config/SecurityConfig.java b/demo-client/src/main/java/sample/config/SecurityConfig.java new file mode 100644 index 0000000..1979b8a --- /dev/null +++ b/demo-client/src/main/java/sample/config/SecurityConfig.java @@ -0,0 +1,78 @@ +/* + * Copyright 2020-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package sample.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.oauth2.client.oidc.web.logout.OidcClientInitiatedLogoutSuccessHandler; +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.logout.LogoutSuccessHandler; + +import static org.springframework.security.config.Customizer.withDefaults; + +/** + * @author Joe Grandja + * @author Dmitriy Dubson + * @author Steve Riesenberg + * @since 0.0.1 + */ +@EnableWebSecurity +@Configuration(proxyBeanMethods = false) +public class SecurityConfig { + + @Bean + public WebSecurityCustomizer webSecurityCustomizer() { + return (web) -> web.ignoring().requestMatchers("/webjars/**", "/assets/**"); + } + + // @formatter:off + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http, + ClientRegistrationRepository clientRegistrationRepository) throws Exception { + http + .authorizeHttpRequests(authorize -> + authorize + .requestMatchers("/logged-out").permitAll() + .anyRequest().authenticated() + ) + .csrf(AbstractHttpConfigurer::disable) + .oauth2Login(oauth2Login -> + oauth2Login.loginPage("/oauth2/authorization/messaging-client-oidc")) + .oauth2Client(withDefaults()) + .logout(logout -> + logout.logoutSuccessHandler(oidcLogoutSuccessHandler(clientRegistrationRepository))); + return http.build(); + } + // @formatter:on + + private LogoutSuccessHandler oidcLogoutSuccessHandler( + ClientRegistrationRepository clientRegistrationRepository) { + OidcClientInitiatedLogoutSuccessHandler oidcLogoutSuccessHandler = + new OidcClientInitiatedLogoutSuccessHandler(clientRegistrationRepository); + + // Set the location that the End-User's User Agent will be redirected to + // after the logout has been performed at the Provider + oidcLogoutSuccessHandler.setPostLogoutRedirectUri("{baseUrl}/logged-out"); + + return oidcLogoutSuccessHandler; + } + +} diff --git a/demo-client/src/main/java/sample/config/WebClientConfig.java b/demo-client/src/main/java/sample/config/WebClientConfig.java new file mode 100644 index 0000000..274ddcc --- /dev/null +++ b/demo-client/src/main/java/sample/config/WebClientConfig.java @@ -0,0 +1,76 @@ +/* + * Copyright 2020-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package sample.config; + +import sample.authorization.DeviceCodeOAuth2AuthorizedClientProvider; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClientProvider; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClientProviderBuilder; +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; +import org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizedClientManager; +import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository; +import org.springframework.security.oauth2.client.web.reactive.function.client.ServletOAuth2AuthorizedClientExchangeFilterFunction; +import org.springframework.web.reactive.function.client.WebClient; + +/** + * @author Joe Grandja + * @author Steve Riesenberg + * @since 0.0.1 + */ +@Configuration +public class WebClientConfig { + + @Bean + public WebClient webClient(OAuth2AuthorizedClientManager authorizedClientManager) { + ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2Client = + new ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager); + // @formatter:off + return WebClient.builder() + .apply(oauth2Client.oauth2Configuration()) + .build(); + // @formatter:on + } + + @Bean + public OAuth2AuthorizedClientManager authorizedClientManager( + ClientRegistrationRepository clientRegistrationRepository, + OAuth2AuthorizedClientRepository authorizedClientRepository) { + + // @formatter:off + OAuth2AuthorizedClientProvider authorizedClientProvider = + OAuth2AuthorizedClientProviderBuilder.builder() + .authorizationCode() + .refreshToken() + .clientCredentials() + .provider(new DeviceCodeOAuth2AuthorizedClientProvider()) + .build(); + // @formatter:on + + DefaultOAuth2AuthorizedClientManager authorizedClientManager = new DefaultOAuth2AuthorizedClientManager( + clientRegistrationRepository, authorizedClientRepository); + authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider); + + // Set a contextAttributesMapper to obtain device_code from the request + authorizedClientManager.setContextAttributesMapper(DeviceCodeOAuth2AuthorizedClientProvider + .deviceCodeContextAttributesMapper()); + + return authorizedClientManager; + } + +} diff --git a/demo-client/src/main/java/sample/web/AuthorizationController.java b/demo-client/src/main/java/sample/web/AuthorizationController.java new file mode 100644 index 0000000..c92d789 --- /dev/null +++ b/demo-client/src/main/java/sample/web/AuthorizationController.java @@ -0,0 +1,110 @@ +/* + * Copyright 2020-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package sample.web; + +import jakarta.servlet.http.HttpServletRequest; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; +import org.springframework.security.oauth2.client.annotation.RegisteredOAuth2AuthorizedClient; +import org.springframework.security.oauth2.core.OAuth2Error; +import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.util.StringUtils; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.reactive.function.client.WebClient; +import org.springframework.web.reactive.function.client.WebClientResponseException; + +import static org.springframework.security.oauth2.client.web.reactive.function.client.ServletOAuth2AuthorizedClientExchangeFilterFunction.clientRegistrationId; +import static org.springframework.security.oauth2.client.web.reactive.function.client.ServletOAuth2AuthorizedClientExchangeFilterFunction.oauth2AuthorizedClient; + +/** + * @author Joe Grandja + * @since 0.0.1 + */ +@Controller +public class AuthorizationController { + private final WebClient webClient; + private final String messagesBaseUri; + + public AuthorizationController(WebClient webClient, + @Value("${messages.base-uri}") String messagesBaseUri) { + this.webClient = webClient; + this.messagesBaseUri = messagesBaseUri; + } + + @GetMapping(value = "/authorize", params = "grant_type=authorization_code") + public String authorizationCodeGrant(Model model, + @RegisteredOAuth2AuthorizedClient("messaging-client-authorization-code") + OAuth2AuthorizedClient authorizedClient) { + + String[] messages = this.webClient + .get() + .uri(this.messagesBaseUri) + .attributes(oauth2AuthorizedClient(authorizedClient)) + .retrieve() + .bodyToMono(String[].class) + .block(); + model.addAttribute("messages", messages); + + return "index"; + } + + // '/authorized' is the registered 'redirect_uri' for authorization_code + @GetMapping(value = "/authorized", params = OAuth2ParameterNames.ERROR) + public String authorizationFailed(Model model, HttpServletRequest request) { + String errorCode = request.getParameter(OAuth2ParameterNames.ERROR); + if (StringUtils.hasText(errorCode)) { + model.addAttribute("error", + new OAuth2Error( + errorCode, + request.getParameter(OAuth2ParameterNames.ERROR_DESCRIPTION), + request.getParameter(OAuth2ParameterNames.ERROR_URI)) + ); + } + + return "index"; + } + + @GetMapping(value = "/authorize", params = "grant_type=client_credentials") + public String clientCredentialsGrant(Model model) { + + String[] messages = this.webClient + .get() + .uri(this.messagesBaseUri) + .attributes(clientRegistrationId("messaging-client-client-credentials")) + .retrieve() + .bodyToMono(String[].class) + .block(); + model.addAttribute("messages", messages); + + return "index"; + } + + @GetMapping(value = "/authorize", params = "grant_type=device_code") + public String deviceCodeGrant() { + return "device-activate"; + } + + @ExceptionHandler(WebClientResponseException.class) + public String handleError(Model model, WebClientResponseException ex) { + model.addAttribute("error", ex.getMessage()); + return "index"; + } + +} diff --git a/demo-client/src/main/java/sample/web/DefaultController.java b/demo-client/src/main/java/sample/web/DefaultController.java new file mode 100644 index 0000000..88e242a --- /dev/null +++ b/demo-client/src/main/java/sample/web/DefaultController.java @@ -0,0 +1,44 @@ +/* + * Copyright 2020-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package sample.web; + +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; + +/** + * @author Joe Grandja + * @author Dmitriy Dubson + * @since 0.0.1 + */ +@Controller +public class DefaultController { + + @GetMapping("/") + public String root() { + return "redirect:/index"; + } + + @GetMapping("/index") + public String index() { + return "index"; + } + + @GetMapping("/logged-out") + public String loggedOut() { + return "logged-out"; + } + +} diff --git a/demo-client/src/main/java/sample/web/DeviceController.java b/demo-client/src/main/java/sample/web/DeviceController.java new file mode 100644 index 0000000..9f03d74 --- /dev/null +++ b/demo-client/src/main/java/sample/web/DeviceController.java @@ -0,0 +1,192 @@ +/* + * Copyright 2020-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package sample.web; + +import java.time.Instant; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; +import org.springframework.security.oauth2.client.annotation.RegisteredOAuth2AuthorizedClient; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; +import org.springframework.security.oauth2.core.ClientAuthenticationMethod; +import org.springframework.security.oauth2.core.OAuth2AuthorizationException; +import org.springframework.security.oauth2.core.OAuth2Error; +import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.util.StringUtils; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.reactive.function.BodyInserters; +import org.springframework.web.reactive.function.client.WebClient; + +import static org.springframework.security.oauth2.client.web.reactive.function.client.ServletOAuth2AuthorizedClientExchangeFilterFunction.oauth2AuthorizedClient; + +/** + * @author Steve Riesenberg + * @since 1.1 + */ +@Controller +public class DeviceController { + + private static final Set DEVICE_GRANT_ERRORS = new HashSet<>(Arrays.asList( + "authorization_pending", + "slow_down", + "access_denied", + "expired_token" + )); + + private static final ParameterizedTypeReference> TYPE_REFERENCE = + new ParameterizedTypeReference<>() {}; + + private final ClientRegistrationRepository clientRegistrationRepository; + + private final WebClient webClient; + + private final String messagesBaseUri; + + public DeviceController(ClientRegistrationRepository clientRegistrationRepository, WebClient webClient, + @Value("${messages.base-uri}") String messagesBaseUri) { + + this.clientRegistrationRepository = clientRegistrationRepository; + this.webClient = webClient; + this.messagesBaseUri = messagesBaseUri; + } + + @GetMapping("/device_authorize") + public String authorize(Model model) { + // @formatter:off + ClientRegistration clientRegistration = + this.clientRegistrationRepository.findByRegistrationId( + "messaging-client-device-code"); + // @formatter:on + + MultiValueMap requestParameters = new LinkedMultiValueMap<>(); + requestParameters.add(OAuth2ParameterNames.CLIENT_ID, clientRegistration.getClientId()); + requestParameters.add(OAuth2ParameterNames.SCOPE, StringUtils.collectionToDelimitedString( + clientRegistration.getScopes(), " ")); + + String deviceAuthorizationUri = (String) clientRegistration.getProviderDetails().getConfigurationMetadata().get("device_authorization_endpoint"); + + // @formatter:off + Map responseParameters = + this.webClient.post() + .uri(deviceAuthorizationUri) + .headers(headers -> { + /* + * This sample demonstrates the use of a public client that does not + * store credentials or authenticate with the authorization server. + * + * See DeviceClientAuthenticationProvider in the authorization server + * sample for an example customization that allows public clients. + * + * For a confidential client, change the client-authentication-method to + * client_secret_basic and set the client-secret to send the + * OAuth 2.0 Device Authorization Request with a clientId/clientSecret. + */ + if (!clientRegistration.getClientAuthenticationMethod().equals(ClientAuthenticationMethod.NONE)) { + headers.setBasicAuth(clientRegistration.getClientId(), clientRegistration.getClientSecret()); + } + }) + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .body(BodyInserters.fromFormData(requestParameters)) + .retrieve() + .bodyToMono(TYPE_REFERENCE) + .block(); + // @formatter:on + + Objects.requireNonNull(responseParameters, "Device Authorization Response cannot be null"); + Instant issuedAt = Instant.now(); + Integer expiresIn = (Integer) responseParameters.get(OAuth2ParameterNames.EXPIRES_IN); + Instant expiresAt = issuedAt.plusSeconds(expiresIn); + + model.addAttribute("deviceCode", responseParameters.get(OAuth2ParameterNames.DEVICE_CODE)); + model.addAttribute("expiresAt", expiresAt); + model.addAttribute("userCode", responseParameters.get(OAuth2ParameterNames.USER_CODE)); + model.addAttribute("verificationUri", responseParameters.get(OAuth2ParameterNames.VERIFICATION_URI)); + // Note: You could use a QR-code to display this URL + model.addAttribute("verificationUriComplete", responseParameters.get( + OAuth2ParameterNames.VERIFICATION_URI_COMPLETE)); + + return "device-authorize"; + } + + /** + * @see #handleError(OAuth2AuthorizationException) + */ + @PostMapping("/device_authorize") + public ResponseEntity poll(@RequestParam(OAuth2ParameterNames.DEVICE_CODE) String deviceCode, + @RegisteredOAuth2AuthorizedClient("messaging-client-device-code") + OAuth2AuthorizedClient authorizedClient) { + + /* + * The client will repeatedly poll until authorization is granted. + * + * The OAuth2AuthorizedClientManager uses the device_code parameter + * to make a token request, which returns authorization_pending until + * the user has granted authorization. + * + * If the user has denied authorization, access_denied is returned and + * polling should stop. + * + * If the device code expires, expired_token is returned and polling + * should stop. + * + * This endpoint simply returns 200 OK when the client is authorized. + */ + return ResponseEntity.status(HttpStatus.OK).build(); + } + + @ExceptionHandler(OAuth2AuthorizationException.class) + public ResponseEntity handleError(OAuth2AuthorizationException ex) { + String errorCode = ex.getError().getErrorCode(); + if (DEVICE_GRANT_ERRORS.contains(errorCode)) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(ex.getError()); + } + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(ex.getError()); + } + + @GetMapping("/device_authorized") + public String authorized(Model model, + @RegisteredOAuth2AuthorizedClient("messaging-client-device-code") + OAuth2AuthorizedClient authorizedClient) { + + String[] messages = this.webClient.get() + .uri(this.messagesBaseUri) + .attributes(oauth2AuthorizedClient(authorizedClient)) + .retrieve() + .bodyToMono(String[].class) + .block(); + model.addAttribute("messages", messages); + + return "index"; + } + +} diff --git a/demo-client/src/main/resources/application.yml b/demo-client/src/main/resources/application.yml new file mode 100644 index 0000000..ac20c1b --- /dev/null +++ b/demo-client/src/main/resources/application.yml @@ -0,0 +1,54 @@ +server: + port: 8080 + +logging: + level: + root: INFO + org.springframework.web: trace + org.springframework.security: trace + org.springframework.security.oauth2: trace + org.springframework.security.oauth2.client: trace + +spring: + thymeleaf: + cache: false + security: + oauth2: + client: + registration: + messaging-client-oidc: + provider: spring + client-id: messaging-client + client-secret: secret + authorization-grant-type: authorization_code + redirect-uri: "http://127.0.0.1:8080/login/oauth2/code/{registrationId}" + scope: openid, profile + client-name: messaging-client-oidc + messaging-client-authorization-code: + provider: spring + client-id: messaging-client + client-secret: secret + authorization-grant-type: authorization_code + redirect-uri: "http://127.0.0.1:8080/authorized" + scope: message.read,message.write + client-name: messaging-client-authorization-code + messaging-client-client-credentials: + provider: spring + client-id: messaging-client + client-secret: secret + authorization-grant-type: client_credentials + scope: message.read,message.write + client-name: messaging-client-client-credentials + messaging-client-device-code: + provider: spring + client-id: device-messaging-client + client-authentication-method: none + authorization-grant-type: urn:ietf:params:oauth:grant-type:device_code + scope: message.read,message.write + client-name: messaging-client-device-code + provider: + spring: + issuer-uri: http://192.168.2.16:9000 + +messages: + base-uri: http://127.0.0.1:8090/messages diff --git a/demo-client/src/main/resources/static/assets/img/devices.png b/demo-client/src/main/resources/static/assets/img/devices.png new file mode 100644 index 0000000000000000000000000000000000000000..fda6b12e312ff705176efdb8db7f8400ba9fb1d6 GIT binary patch literal 19071 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 a1q~^)FZa(e1RnpylG+{3+ht0YA^!_vu?F@4 literal 0 HcmV?d00001 diff --git a/demo-client/src/main/resources/static/assets/img/spring-security.svg b/demo-client/src/main/resources/static/assets/img/spring-security.svg new file mode 100644 index 0000000..897f986 --- /dev/null +++ b/demo-client/src/main/resources/static/assets/img/spring-security.svg @@ -0,0 +1 @@ +logo-security \ No newline at end of file diff --git a/demo-client/src/main/resources/templates/device-activate.html b/demo-client/src/main/resources/templates/device-activate.html new file mode 100644 index 0000000..c704193 --- /dev/null +++ b/demo-client/src/main/resources/templates/device-activate.html @@ -0,0 +1,27 @@ + + + + + + Spring Authorization Server sample + + + +
+
+
+
+

Activation Required

+

You must activate this device.

+ Activate +
+
+ Devices +
+
+
+ + + + + diff --git a/demo-client/src/main/resources/templates/device-authorize.html b/demo-client/src/main/resources/templates/device-authorize.html new file mode 100644 index 0000000..41ef563 --- /dev/null +++ b/demo-client/src/main/resources/templates/device-authorize.html @@ -0,0 +1,87 @@ + + + + + + Spring Authorization Server sample + + + +
+
+
+
+

Device Activation

+

Please visit on another device to continue.

+

Activation Code

+
+ +
+ +
+
+
+
+ Devices +
+
+
+ + + + + + diff --git a/demo-client/src/main/resources/templates/index.html b/demo-client/src/main/resources/templates/index.html new file mode 100644 index 0000000..c0ef10b --- /dev/null +++ b/demo-client/src/main/resources/templates/index.html @@ -0,0 +1,19 @@ + + + + + + Spring Authorization Server sample + + + +
+
+
+
+
+ + + + + diff --git a/demo-client/src/main/resources/templates/logged-out.html b/demo-client/src/main/resources/templates/logged-out.html new file mode 100644 index 0000000..67a38ac --- /dev/null +++ b/demo-client/src/main/resources/templates/logged-out.html @@ -0,0 +1,22 @@ + + + + + + Spring Authorization Server sample + + + +
+
+
+
+

You are now logged out.

+
+
+
+ + + + + diff --git a/demo-client/src/main/resources/templates/page-templates.html b/demo-client/src/main/resources/templates/page-templates.html new file mode 100644 index 0000000..6d9e17b --- /dev/null +++ b/demo-client/src/main/resources/templates/page-templates.html @@ -0,0 +1,69 @@ + + + + + + Spring Authorization Server sample + + + + +
+
+ +
+
+
+ + + + + + + + + + + + + + +
Messages
#Message
+
+
+
+ + + + + diff --git a/resource-server/.gitignore b/messages-resource/.gitignore similarity index 100% rename from resource-server/.gitignore rename to messages-resource/.gitignore diff --git a/resource-server/mvnw b/messages-resource/mvnw similarity index 100% rename from resource-server/mvnw rename to messages-resource/mvnw diff --git a/resource-server/mvnw.cmd b/messages-resource/mvnw.cmd similarity index 100% rename from resource-server/mvnw.cmd rename to messages-resource/mvnw.cmd diff --git a/messages-resource/pom.xml b/messages-resource/pom.xml new file mode 100644 index 0000000..a379d58 --- /dev/null +++ b/messages-resource/pom.xml @@ -0,0 +1,59 @@ + + + + sample-demo + com.sample.demo + 1.0-SNAPSHOT + + 4.0.0 + + messages-resource + + + + + org.springframework.boot + spring-boot-starter-oauth2-resource-server + + + + + org.springframework.boot + spring-boot-starter-security + + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-tomcat + + + + + + + org.springframework.boot + spring-boot-starter-undertow + + + + + org.springframework.boot + spring-boot-starter-test + + + + org.projectlombok + lombok + + + + + \ No newline at end of file diff --git a/messages-resource/src/main/java/sample/MessagesResourceApplication.java b/messages-resource/src/main/java/sample/MessagesResourceApplication.java new file mode 100644 index 0000000..8fc9bb9 --- /dev/null +++ b/messages-resource/src/main/java/sample/MessagesResourceApplication.java @@ -0,0 +1,32 @@ +/* + * Copyright 2020-2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package sample; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * @author Joe Grandja + * @since 0.0.1 + */ +@SpringBootApplication +public class MessagesResourceApplication { + + public static void main(String[] args) { + SpringApplication.run(MessagesResourceApplication.class, args); + } + +} diff --git a/messages-resource/src/main/java/sample/config/ResourceServerConfig.java b/messages-resource/src/main/java/sample/config/ResourceServerConfig.java new file mode 100644 index 0000000..c0a849d --- /dev/null +++ b/messages-resource/src/main/java/sample/config/ResourceServerConfig.java @@ -0,0 +1,63 @@ +/* + * Copyright 2020-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package sample.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.Customizer; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import org.springframework.web.filter.CorsFilter; + +/** + * @author Joe Grandja + * @since 0.0.1 + */ +@EnableWebSecurity +@Configuration(proxyBeanMethods = false) +public class ResourceServerConfig { + + // @formatter:off + @Bean + SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http + .authorizeHttpRequests(authorizeHttpRequests->authorizeHttpRequests.anyRequest().authenticated()) + + .oauth2ResourceServer(oauth2ResourceServer->oauth2ResourceServer.jwt(Customizer.withDefaults())) + ; + return http.build(); + } + // @formatter:on + + /** + * 跨域过滤器配置 + * @return + */ + @Bean + public CorsFilter corsFilter() { + CorsConfiguration configuration = new CorsConfiguration(); + configuration.addAllowedOrigin("*"); + configuration.setAllowCredentials(true); + configuration.addAllowedMethod("*"); + configuration.addAllowedHeader("*"); + UrlBasedCorsConfigurationSource configurationSource = new UrlBasedCorsConfigurationSource(); + configurationSource.registerCorsConfiguration("/**", configuration); + return new CorsFilter(configurationSource); + } +} diff --git a/messages-resource/src/main/java/sample/web/MessagesController.java b/messages-resource/src/main/java/sample/web/MessagesController.java new file mode 100644 index 0000000..501fb2d --- /dev/null +++ b/messages-resource/src/main/java/sample/web/MessagesController.java @@ -0,0 +1,32 @@ +/* + * Copyright 2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package sample.web; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * @author Joe Grandja + * @since 0.0.1 + */ +@RestController +public class MessagesController { + + @GetMapping("/messages") + public String[] getMessages() { + return new String[] {"Message 1", "Message 2", "Message 3"}; + } +} diff --git a/messages-resource/src/main/resources/application.yml b/messages-resource/src/main/resources/application.yml new file mode 100644 index 0000000..a063d98 --- /dev/null +++ b/messages-resource/src/main/resources/application.yml @@ -0,0 +1,17 @@ +server: + port: 8090 + +logging: + level: + root: INFO + org.springframework.web: debug + org.springframework.security: debug + org.springframework.security.oauth2: debug +# org.springframework.boot.autoconfigure: DEBUG + +spring: + security: + oauth2: + resourceserver: + jwt: + issuer-uri: http://192.168.2.16:9000 diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..16c4cef --- /dev/null +++ b/pom.xml @@ -0,0 +1,44 @@ + + + + org.springframework.boot + spring-boot-starter-parent + 3.1.0 + + + 4.0.0 + + com.sample.demo + sample-demo + spring authorization server 基于maven构建的官方原始demo + 1.0-SNAPSHOT + pom + + + + demo-authorizationserver + messages-resource + demo-client + + + + 17 + 17 + 3.1.0 + 1.1.1 + + + + + + + org.springframework.security + spring-security-oauth2-authorization-server + ${spring-security-oauth2.version} + + + + + \ No newline at end of file diff --git a/resource-server/pom.xml b/resource-server/pom.xml deleted file mode 100644 index 304f624..0000000 --- a/resource-server/pom.xml +++ /dev/null @@ -1,60 +0,0 @@ - - - 4.0.0 - - org.springframework.boot - spring-boot-starter-parent - 3.1.3 - - - com.leonardozw - resource-server - 0.0.1-SNAPSHOT - resource-server - Demo project for Spring Boot - - 17 - - - - org.springframework.boot - spring-boot-starter-oauth2-resource-server - - - org.springframework.boot - spring-boot-starter-webflux - - - org.springframework.security - spring-security-oauth2-jose - - - - org.springframework.boot - spring-boot-devtools - runtime - true - - - org.springframework.boot - spring-boot-starter-test - test - - - io.projectreactor - reactor-test - test - - - - - - - org.springframework.boot - spring-boot-maven-plugin - - - - - diff --git a/resource-server/src/main/java/com/leonardozw/resourceserver/ResourceServerApplication.java b/resource-server/src/main/java/com/leonardozw/resourceserver/ResourceServerApplication.java deleted file mode 100644 index 73a6243..0000000 --- a/resource-server/src/main/java/com/leonardozw/resourceserver/ResourceServerApplication.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.leonardozw.resourceserver; - -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; - -@SpringBootApplication -public class ResourceServerApplication { - - public static void main(String[] args) { - SpringApplication.run(ResourceServerApplication.class, args); - } - -} diff --git a/resource-server/src/main/java/com/leonardozw/resourceserver/TasksController.java b/resource-server/src/main/java/com/leonardozw/resourceserver/TasksController.java deleted file mode 100644 index 22ba5d9..0000000 --- a/resource-server/src/main/java/com/leonardozw/resourceserver/TasksController.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.leonardozw.resourceserver; - -import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.security.oauth2.jwt.Jwt; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -@RestController -@RequestMapping("tasks") -public class TasksController { - - @GetMapping - public String getTasks( - @AuthenticationPrincipal Jwt jwt - ){ - return """ -

Top secret task for %s

-

Do not share with anyone!

-
    -
  1. Buy milk
  2. -
  3. Buy eggs
  4. -
  5. Buy bread
  6. -
- """.formatted(jwt.getSubject()); - } -} diff --git a/resource-server/src/main/resources/application.yml b/resource-server/src/main/resources/application.yml deleted file mode 100644 index 39831b7..0000000 --- a/resource-server/src/main/resources/application.yml +++ /dev/null @@ -1,8 +0,0 @@ -server: - port: 9090 -spring: - security: - oauth2: - resourceserver: - jwt: - issuer-uri: http://localhost:9000 diff --git a/resource-server/src/test/java/com/leonardozw/resourceserver/ResourceServerApplicationTests.java b/resource-server/src/test/java/com/leonardozw/resourceserver/ResourceServerApplicationTests.java deleted file mode 100644 index 1677ae3..0000000 --- a/resource-server/src/test/java/com/leonardozw/resourceserver/ResourceServerApplicationTests.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.leonardozw.resourceserver; - -import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; - -@SpringBootTest -class ResourceServerApplicationTests { - - @Test - void contextLoads() { - } - -}