diff --git a/samples/boot/oauth2authorizationserver/README.adoc b/samples/boot/oauth2authorizationserver/README.adoc new file mode 100644 index 0000000000..34e21b744f --- /dev/null +++ b/samples/boot/oauth2authorizationserver/README.adoc @@ -0,0 +1,39 @@ += OAuth 2.0 Authorization Server Sample + +This sample demonstrates an Authorization Server that supports a simple, static JWK Set. + +It's useful for working with the other samples in the library that want to point to an Authorization Server. + +== 1. Running the server + +To run the server, do: + +```bash +./gradlew bootRun +``` + +Or import the project into your IDE and run `OAuth2AuthorizationServerApplication` from there. + +Once it is up, this request asks for a token with the "message:read" scope: + +```bash +curl reader:secret@localhost:8081/oauth/token -d grant_type=password -d username=user -d password=password +``` + +Which will respond with something like: + +```json +{ + "access_token":"eyJhbGciOiJSUzI1NiIsI...Fhq4RIVyA4ZAkC7T1aZbKAQ", + "token_type":"bearer", + "expires_in":599999999, + "scope":"message:read", + "jti":"8a425df7-f4c9-4ca4-be12-0136c3015da0" +} +``` + +You can also so the same with the `writer` client: + +```bash +curl writer:secret@localhost:8081/oauth/token -d grant_type=password -d username=user -d password=$PASSWORD +``` diff --git a/samples/boot/oauth2authorizationserver/spring-security-samples-boot-oauth2authorizationserver.gradle b/samples/boot/oauth2authorizationserver/spring-security-samples-boot-oauth2authorizationserver.gradle new file mode 100644 index 0000000000..d1c378962f --- /dev/null +++ b/samples/boot/oauth2authorizationserver/spring-security-samples-boot-oauth2authorizationserver.gradle @@ -0,0 +1,10 @@ +apply plugin: 'io.spring.convention.spring-sample-boot' + +dependencies { + compile 'org.springframework.boot:spring-boot-starter-web' + compile 'org.springframework.boot:spring-boot-starter-security' + compile "org.springframework.security.oauth.boot:spring-security-oauth2-autoconfigure:${springBootVersion}" + compile 'com.nimbusds:nimbus-jose-jwt' + + testCompile 'org.springframework.boot:spring-boot-starter-test' +} diff --git a/samples/boot/oauth2authorizationserver/src/main/java/sample/JwkSetConfiguration.java b/samples/boot/oauth2authorizationserver/src/main/java/sample/JwkSetConfiguration.java new file mode 100644 index 0000000000..73e59f5619 --- /dev/null +++ b/samples/boot/oauth2authorizationserver/src/main/java/sample/JwkSetConfiguration.java @@ -0,0 +1,220 @@ +/* + * Copyright 2002-2018 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 + * + * http://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 java.math.BigInteger; +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.Principal; +import java.security.interfaces.RSAPublicKey; +import java.security.spec.RSAPrivateKeySpec; +import java.security.spec.RSAPublicKeySpec; +import java.util.LinkedHashMap; +import java.util.Map; + +import com.nimbusds.jose.jwk.JWKSet; +import com.nimbusds.jose.jwk.RSAKey; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer; +import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter; +import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer; +import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer; +import org.springframework.security.oauth2.provider.endpoint.FrameworkEndpoint; +import org.springframework.security.oauth2.provider.token.DefaultAccessTokenConverter; +import org.springframework.security.oauth2.provider.token.DefaultUserAuthenticationConverter; +import org.springframework.security.oauth2.provider.token.TokenStore; +import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter; +import org.springframework.security.oauth2.provider.token.store.JwtTokenStore; +import org.springframework.security.provisioning.InMemoryUserDetailsManager; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ResponseBody; + +/** + * An instance of Legacy Authorization Server (spring-security-oauth2) that uses a single, + * not-rotating key and exposes a JWK endpoint. + * + * See + * + * Spring Security OAuth Autoconfig's documentation for additional detail + * + * @author Josh Cummings + * @since 5.1 + */ +@EnableAuthorizationServer +@Configuration +public class JwkSetConfiguration extends AuthorizationServerConfigurerAdapter { + + AuthenticationManager authenticationManager; + KeyPair keyPair; + + public JwkSetConfiguration( + AuthenticationConfiguration authenticationConfiguration, + KeyPair keyPair) throws Exception { + + this.authenticationManager = authenticationConfiguration.getAuthenticationManager(); + this.keyPair = keyPair; + } + + @Override + public void configure(ClientDetailsServiceConfigurer clients) + throws Exception { + // @formatter:off + clients.inMemory() + .withClient("reader") + .authorizedGrantTypes("password") + .secret("{noop}secret") + .scopes("message:read") + .accessTokenValiditySeconds(600_000_000) + .and() + .withClient("writer") + .authorizedGrantTypes("password") + .secret("{noop}secret") + .scopes("message:write") + .accessTokenValiditySeconds(600_000_000); + // @formatter:on + } + + @Override + public void configure(AuthorizationServerEndpointsConfigurer endpoints) { + // @formatter:off + endpoints + .authenticationManager(this.authenticationManager) + .accessTokenConverter(accessTokenConverter()) + .tokenStore(tokenStore()); + // @formatter:on + } + + @Bean + public TokenStore tokenStore() { + return new JwtTokenStore(accessTokenConverter()); + } + + @Bean + public JwtAccessTokenConverter accessTokenConverter() { + JwtAccessTokenConverter converter = new JwtAccessTokenConverter(); + converter.setKeyPair(this.keyPair); + + DefaultAccessTokenConverter accessTokenConverter = new DefaultAccessTokenConverter(); + accessTokenConverter.setUserTokenConverter(new SubjectAttributeUserTokenConverter()); + converter.setAccessTokenConverter(accessTokenConverter); + + return converter; + } +} + +/** + * For configuring the end users recognized by this Authorization Server + */ +@Configuration +class UserConfig extends WebSecurityConfigurerAdapter { + + @Override + protected void configure(HttpSecurity http) throws Exception { + http + .authorizeRequests() + .mvcMatchers("/.well-known/jwks.json").permitAll() + .anyRequest().authenticated(); + } + + @Bean + @Override + public UserDetailsService userDetailsService() { + return new InMemoryUserDetailsManager( + User.withDefaultPasswordEncoder() + .username("user") + .password("password") + .roles("USER") + .build()); + } +} + +/** + * Legacy Authorization Server (spring-security-oauth2) does not support any + * JWK Set endpoint. + * + * This class adds ad-hoc support in order to better support the other samples in the repo. + */ +@FrameworkEndpoint +class JwkSetEndpoint { + KeyPair keyPair; + + public JwkSetEndpoint(KeyPair keyPair) { + this.keyPair = keyPair; + } + + @GetMapping("/.well-known/jwks.json") + @ResponseBody + public Map getKey(Principal principal) { + RSAPublicKey publicKey = (RSAPublicKey) this.keyPair.getPublic(); + RSAKey key = new RSAKey.Builder(publicKey).build(); + return new JWKSet(key).toJSONObject(); + } +} + +/** + * An Authorization Server will more typically have a key rotation strategy, and the keys will not + * be hard-coded into the application code. + * + * For simplicity, though, this sample doesn't demonstrate key rotation. + */ +@Configuration +class KeyConfig { + @Bean + KeyPair keyPair() { + try { + String privateExponent = "3851612021791312596791631935569878540203393691253311342052463788814433805390794604753109719790052408607029530149004451377846406736413270923596916756321977922303381344613407820854322190592787335193581632323728135479679928871596911841005827348430783250026013354350760878678723915119966019947072651782000702927096735228356171563532131162414366310012554312756036441054404004920678199077822575051043273088621405687950081861819700809912238863867947415641838115425624808671834312114785499017269379478439158796130804789241476050832773822038351367878951389438751088021113551495469440016698505614123035099067172660197922333993"; + String modulus = "18044398961479537755088511127417480155072543594514852056908450877656126120801808993616738273349107491806340290040410660515399239279742407357192875363433659810851147557504389760192273458065587503508596714389889971758652047927503525007076910925306186421971180013159326306810174367375596043267660331677530921991343349336096643043840224352451615452251387611820750171352353189973315443889352557807329336576421211370350554195530374360110583327093711721857129170040527236951522127488980970085401773781530555922385755722534685479501240842392531455355164896023070459024737908929308707435474197069199421373363801477026083786683"; + String exponent = "65537"; + + RSAPublicKeySpec publicSpec = new RSAPublicKeySpec(new BigInteger(modulus), new BigInteger(exponent)); + RSAPrivateKeySpec privateSpec = new RSAPrivateKeySpec(new BigInteger(modulus), new BigInteger(privateExponent)); + KeyFactory factory = KeyFactory.getInstance("RSA"); + return new KeyPair(factory.generatePublic(publicSpec), factory.generatePrivate(privateSpec)); + } catch ( Exception e ) { + throw new IllegalArgumentException(e); + } + } +} + +/** + * Legacy Authorization Server does not support a custom name for the user parameter, so we'll need + * to extend the default. By default, it uses the attribute {@code user_name}, though it would be + * better to adhere to the {@code sub} property defined in the + * JWT Specification. + */ +class SubjectAttributeUserTokenConverter extends DefaultUserAuthenticationConverter { + @Override + public Map convertUserAuthentication(Authentication authentication) { + Map response = new LinkedHashMap(); + response.put("sub", authentication.getName()); + if (authentication.getAuthorities() != null && !authentication.getAuthorities().isEmpty()) { + response.put(AUTHORITIES, AuthorityUtils.authorityListToSet(authentication.getAuthorities())); + } + return response; + } +} diff --git a/samples/boot/oauth2authorizationserver/src/main/java/sample/OAuth2AuthorizationServerApplication.java b/samples/boot/oauth2authorizationserver/src/main/java/sample/OAuth2AuthorizationServerApplication.java new file mode 100644 index 0000000000..24caab6def --- /dev/null +++ b/samples/boot/oauth2authorizationserver/src/main/java/sample/OAuth2AuthorizationServerApplication.java @@ -0,0 +1,30 @@ +/* + * Copyright 2002-2018 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 + * + * http://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 Josh Cummings + */ +@SpringBootApplication +public class OAuth2AuthorizationServerApplication { + + public static void main(String[] args) { + SpringApplication.run(OAuth2AuthorizationServerApplication.class, args); + } +} diff --git a/samples/boot/oauth2authorizationserver/src/main/resources/application.yml b/samples/boot/oauth2authorizationserver/src/main/resources/application.yml new file mode 100644 index 0000000000..e5ba667fd3 --- /dev/null +++ b/samples/boot/oauth2authorizationserver/src/main/resources/application.yml @@ -0,0 +1 @@ +server.port: 8081 diff --git a/samples/boot/oauth2authorizationserver/src/test/java/sample/OAuth2AuthorizationServerApplicationTests.java b/samples/boot/oauth2authorizationserver/src/test/java/sample/OAuth2AuthorizationServerApplicationTests.java new file mode 100644 index 0000000000..6a663423b9 --- /dev/null +++ b/samples/boot/oauth2authorizationserver/src/test/java/sample/OAuth2AuthorizationServerApplicationTests.java @@ -0,0 +1,64 @@ +/* + * Copyright 2002-2018 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 + * + * http://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.junit.Test; +import org.junit.runner.RunWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.MockMvc; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * Tests for {@link OAuth2AuthorizationServerApplication} + * + * @author Josh Cummings + */ +@RunWith(SpringRunner.class) +@SpringBootTest +@AutoConfigureMockMvc +public class OAuth2AuthorizationServerApplicationTests { + + @Autowired + MockMvc mvc; + + @Test + public void requestTokenWhenUsingPasswordGrantTypeThenOk() + throws Exception { + + this.mvc.perform(post("/oauth/token") + .param("grant_type", "password") + .param("username", "user") + .param("password", "password") + .header("Authorization", "Basic cmVhZGVyOnNlY3JldA==")) + .andExpect(status().isOk()); + } + + @Test + public void requestJwkSetWhenUsingDefaultsThenOk() + throws Exception { + + this.mvc.perform(get("/.well-known/jwks.json")) + .andExpect(status().isOk()); + } + +}