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 0000000..fda6b12 Binary files /dev/null and b/demo-authorizationserver/src/main/resources/static/assets/img/devices.png differ 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 0000000..ee329fe Binary files /dev/null and b/demo-authorizationserver/src/main/resources/static/assets/img/gitee.png differ diff --git a/demo-authorizationserver/src/main/resources/static/assets/img/github.png b/demo-authorizationserver/src/main/resources/static/assets/img/github.png new file mode 100644 index 0000000..e28a837 Binary files /dev/null and b/demo-authorizationserver/src/main/resources/static/assets/img/github.png differ 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 0000000..795dea3 Binary files /dev/null and b/demo-authorizationserver/src/main/resources/static/assets/img/google.png differ 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 0000000..fda6b12 Binary files /dev/null and b/demo-client/src/main/resources/static/assets/img/devices.png differ 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() { - } - -}