Authorization Server Sample

A simple Authorization Server sample based off of the Legacy
Authorization Server project, spring-security-oauth2.

This project is mostly useful as a utility for other samples whose
usage would be clearer if an Authorization Server were introduced.

Note that this server is a barebones OAuth 2.0 Authorization Server
configuration, but is still useful for understanding how to set up an
Authorization Server using spring-security-oauth2.

Fixes: gh-5833
This commit is contained in:
Josh Cummings 2018-09-12 18:08:12 -06:00
parent c21b2f31c6
commit f2140dd5f5
No known key found for this signature in database
GPG Key ID: 49EF60DD7FF83443
6 changed files with 364 additions and 0 deletions

View File

@ -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
```

View File

@ -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'
}

View File

@ -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
* <a
* target="_blank"
* href="https://docs.spring.io/spring-security-oauth2-boot/docs/current-SNAPSHOT/reference/htmlsingle/">
* Spring Security OAuth Autoconfig's documentation</a> 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
* <a href target="_blank" href="https://tools.ietf.org/html/rfc7517#section-5">JWK Set</a> 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<String, Object> 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
* <a target="_blank" href="https://tools.ietf.org/html/rfc7519">JWT Specification</a>.
*/
class SubjectAttributeUserTokenConverter extends DefaultUserAuthenticationConverter {
@Override
public Map<String, ?> convertUserAuthentication(Authentication authentication) {
Map<String, Object> response = new LinkedHashMap<String, Object>();
response.put("sub", authentication.getName());
if (authentication.getAuthorities() != null && !authentication.getAuthorities().isEmpty()) {
response.put(AUTHORITIES, AuthorityUtils.authorityListToSet(authentication.getAuthorities()));
}
return response;
}
}

View File

@ -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);
}
}

View File

@ -0,0 +1 @@
server.port: 8081

View File

@ -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());
}
}