├── src ├── main │ ├── resources │ │ └── application.properties │ └── java │ │ └── de │ │ └── synyx │ │ └── jwt │ │ ├── FoobarController.java │ │ ├── Application.java │ │ ├── ResourceServer.java │ │ ├── AuthenticationProvider.java │ │ └── AuthorizationServer.java └── test │ ├── resources │ └── application.properties │ └── java │ └── de │ └── synyx │ └── jwt │ └── SpringOauthJwtIntegrationTest.java ├── .gitignore └── README.md /src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | access-token-validity=30 -------------------------------------------------------------------------------- /src/test/resources/application.properties: -------------------------------------------------------------------------------- 1 | access-token-validity=2 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | .gradle/ 3 | .idea/ 4 | 5 | *.iml 6 | 7 | .DS_Store 8 | -------------------------------------------------------------------------------- /src/main/java/de/synyx/jwt/FoobarController.java: -------------------------------------------------------------------------------- 1 | package de.synyx.jwt; 2 | 3 | import org.springframework.web.bind.annotation.RequestMapping; 4 | import org.springframework.web.bind.annotation.RestController; 5 | 6 | /** 7 | * Provides very simple controller method mapped to "/foobar" 8 | */ 9 | @RestController 10 | public class FoobarController { 11 | 12 | @RequestMapping("/foobar") 13 | public String foobar() { 14 | return "hello OAuth2!"; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/de/synyx/jwt/Application.java: -------------------------------------------------------------------------------- 1 | package de.synyx.jwt; 2 | 3 | import org.springframework.boot.autoconfigure.EnableAutoConfiguration; 4 | import org.springframework.boot.SpringApplication; 5 | import org.springframework.context.annotation.ComponentScan; 6 | import org.springframework.context.annotation.Configuration; 7 | 8 | @EnableAutoConfiguration 9 | @Configuration 10 | @ComponentScan 11 | public class Application { 12 | 13 | public static void main(String[] args) throws Throwable { 14 | SpringApplication.run(Application.class, args); 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Simple playground application using Spring Security with OAuth2 and JWT to 2 | * secure a resource with OAuth2 3 | * provide an endpoint to supply JWT access tokens to the resource 4 | 5 | Usage: 6 | 7 | * run with 8 | ``` 9 | $ gradle bootRun 10 | ``` 11 | * try to access resource w/o authorization, which will fail: 12 | ``` 13 | $ curl localhost:8080/foobar' 14 | ``` 15 | * request OAuth2 access token by supplying user credentials: 16 | ``` 17 | $ curl -X 'POST' 18 | -u my_client_username:my_client_password 19 | --data "username=hdampf 20 | &password=wert123$ 21 | &client_id=my_client_username 22 | &grant_type=password 23 | &scope=foobar_scope" 24 | localhost:8080/oauth/token 25 | 26 | ``` 27 | * the endpoint replies with an access token, which is a Json Web Token (JWT) provided as a base64-encoded string 28 | * go to http://jwt.io/ and paste the token there to see its contents. the _secret_ used is "foobar" (as set in AuthorizationServer) 29 | * access resource with access token: 30 | ``` 31 | $ curl -H "Authorization: Bearer $TOKEN" localhost:8080/foobar 32 | ``` 33 | 34 | -------------------------------------------------------------------------------- /src/main/java/de/synyx/jwt/ResourceServer.java: -------------------------------------------------------------------------------- 1 | package de.synyx.jwt; 2 | 3 | import org.springframework.context.annotation.Configuration; 4 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; 5 | import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer; 6 | import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter; 7 | import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer; 8 | 9 | /** 10 | * Secures a resource, in this case "/foobar", with OAuth2 11 | * 12 | */ 13 | @Configuration 14 | @EnableResourceServer 15 | public class ResourceServer extends ResourceServerConfigurerAdapter { 16 | @Override 17 | public void configure(HttpSecurity http) throws Exception { 18 | http 19 | .requestMatchers().antMatchers("/foobar").and() 20 | .authorizeRequests() 21 | .anyRequest().access("#oauth2.hasScope('foobar_scope')"); 22 | } 23 | 24 | @Override 25 | public void configure(ResourceServerSecurityConfigurer resources) throws Exception { 26 | resources.resourceId("my_resource_id"); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/de/synyx/jwt/AuthenticationProvider.java: -------------------------------------------------------------------------------- 1 | package de.synyx.jwt; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.security.authentication.AuthenticationManager; 6 | import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; 7 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; 8 | import org.springframework.security.config.annotation.web.builders.WebSecurity; 9 | import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; 10 | import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; 11 | 12 | /** 13 | * Provides AuthenticationManager with some simple in-memory users, used to 14 | * authenticate users for {@link AuthorizationServer} 15 | * 16 | * In a real application, this should be replaced by an {@link AbstractLdapAuthenticationProvider} or something similar 17 | * 18 | */ 19 | @EnableWebSecurity 20 | @Configuration 21 | public class AuthenticationProvider extends WebSecurityConfigurerAdapter { 22 | 23 | @Override 24 | protected void configure(AuthenticationManagerBuilder auth) throws Exception { 25 | auth 26 | .inMemoryAuthentication() 27 | .withUser("hdampf").password("wert123$").roles("USER").and() 28 | .withUser("fschmidt").password("wert123$").roles("USER", "ADMIN"); 29 | } 30 | 31 | @Bean 32 | @Override 33 | public AuthenticationManager authenticationManagerBean() throws Exception { 34 | return super.authenticationManagerBean(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/de/synyx/jwt/AuthorizationServer.java: -------------------------------------------------------------------------------- 1 | package de.synyx.jwt; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.beans.factory.annotation.Value; 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.context.annotation.Configuration; 7 | import org.springframework.security.authentication.AuthenticationManager; 8 | import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer; 9 | import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter; 10 | import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer; 11 | import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer; 12 | import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter; 13 | 14 | /** 15 | * Provides "/oauth/token" endpoint, which supplies JWT-encoded OAuth2 authentication tokens 16 | * 17 | * To acquire a token, POST to "/oauth/token", providing credentials for a registered client app as basic auth 18 | * and an OAuth2-compliant parameter set 19 | * 20 | * Example: 21 | * 22 | * curl -X 'POST' 23 | * -u my_client_username:my_client_password (1) 24 | * --data "username=hdampf (2) 25 | * &password=wert123$ (3) 26 | * &client_id=my_client_username (4) 27 | * &grant_type=password (5) 28 | * &scope=foobar_scope" (6) 29 | * localhost:8080/oauth/token (7) 30 | * 31 | * (1) username and password for this request are transmitted via HTTP basic auth, 32 | * they are static for a client application (see configure(ClientDetailsServiceConfigurer clients)) 33 | * (2) in this case, the payload is an OAuth2 request for grant type 'password' 34 | * (see http://aaronparecki.com/articles/2012/07/29/1/oauth2-simplified#others) 35 | * (2/3) the user credentials are the ones that are authenticated against the AuthenticationManager instance 36 | * e.g., the actual credentials of the human user using the client application 37 | * (4) the ID of the client that the user wants to a token for. must be the same as the basic auth username in (1) 38 | * (5) grant type 'password' means that actual user credentials are supplied 39 | * (6) the scope in which the token will be valid. this is an arbitrary string that needs to be configured along with 40 | * the client (see configure(ClientDetailsServiceConfigurer clients)) 41 | * (7) the url of the endpoint 42 | * 43 | */ 44 | @EnableAuthorizationServer 45 | @Configuration 46 | public class AuthorizationServer extends AuthorizationServerConfigurerAdapter { 47 | 48 | @Value("${access-token-validity}") 49 | private int accessTokenValidity; 50 | 51 | /** 52 | * An AuthenticationManager instance is required to enable OAuth2 grant type 'password' 53 | */ 54 | @Autowired 55 | private AuthenticationManager authenticationManager; 56 | 57 | /** 58 | * Supplies an AccessTokenConverter implementation to be used by this endpoint 59 | * 60 | * Also sets the 'secret' used to sign the JWT, in this case to 'foobar' 61 | * 62 | * @return A JwtAccessTokenConverter instance 63 | */ 64 | @Bean 65 | public JwtAccessTokenConverter accessTokenConverter() { 66 | JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter(); 67 | jwtAccessTokenConverter.setSigningKey("foobar"); 68 | return jwtAccessTokenConverter; 69 | } 70 | 71 | /** 72 | * Sets up this authorization endpoint 73 | * 74 | * @param endpoints 75 | * @throws Exception 76 | */ 77 | @Override 78 | public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { 79 | endpoints.authenticationManager(authenticationManager) 80 | .accessTokenConverter(accessTokenConverter()); 81 | } 82 | 83 | /** 84 | * Configures a static client application that can request access tokens 85 | * 86 | * @param clients 87 | * @throws Exception 88 | */ 89 | @Override 90 | public void configure(ClientDetailsServiceConfigurer clients) throws Exception { 91 | clients.inMemory() 92 | .withClient("my_client_username") 93 | .authorities("ROLE_ADMIN") 94 | .resourceIds("my_resource_id") 95 | .scopes("foobar_scope") 96 | .authorizedGrantTypes("password") 97 | .secret("my_client_password") 98 | .accessTokenValiditySeconds(accessTokenValidity); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/test/java/de/synyx/jwt/SpringOauthJwtIntegrationTest.java: -------------------------------------------------------------------------------- 1 | package de.synyx.jwt; 2 | 3 | import com.jayway.restassured.RestAssured; 4 | import com.jayway.restassured.response.Header; 5 | import com.jayway.restassured.response.Response; 6 | import org.hamcrest.CoreMatchers; 7 | import org.junit.Assert; 8 | import org.junit.Before; 9 | import org.junit.Test; 10 | import org.junit.runner.RunWith; 11 | import org.springframework.beans.factory.annotation.Value; 12 | import org.springframework.boot.test.IntegrationTest; 13 | import org.springframework.boot.test.SpringApplicationConfiguration; 14 | import org.springframework.http.HttpStatus; 15 | import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; 16 | import org.springframework.test.context.web.WebAppConfiguration; 17 | 18 | import java.util.Base64; 19 | 20 | import static com.jayway.restassured.RestAssured.*; 21 | 22 | @RunWith(SpringJUnit4ClassRunner.class) 23 | @SpringApplicationConfiguration(classes = Application.class) 24 | @WebAppConfiguration 25 | @IntegrationTest("server.port:0") 26 | public class SpringOauthJwtIntegrationTest { 27 | 28 | @Value("${local.server.port}") 29 | private int port; 30 | 31 | private String clientBasicAuthCredentials; 32 | 33 | @Before 34 | public void setUp() { 35 | RestAssured.port = this.port; 36 | this.clientBasicAuthCredentials = 37 | Base64.getEncoder().encodeToString("my_client_username:my_client_password".getBytes()); 38 | } 39 | 40 | @Test 41 | public void foobarRequiresAuthorization() { 42 | when(). 43 | get("/foobar"). 44 | then(). 45 | statusCode(HttpStatus.UNAUTHORIZED.value()); 46 | } 47 | 48 | @Test 49 | public void accessTokenRequiresClientCredentialsParameters() { 50 | when(). 51 | get("/oauth/token"). 52 | then(). 53 | statusCode(HttpStatus.UNAUTHORIZED.value()); 54 | } 55 | 56 | @Test 57 | public void accessTokenRequiresOAuthParameters() { 58 | given(). 59 | header(new Header("Authorization", "Basic " + this.clientBasicAuthCredentials)). 60 | when(). 61 | get("/oauth/token"). 62 | then(). 63 | statusCode(HttpStatus.BAD_REQUEST.value()); 64 | } 65 | 66 | @Test 67 | public void grantsAccessToken() { 68 | Response response = 69 | given(). 70 | header(new Header("Authorization", "Basic " + this.clientBasicAuthCredentials)). 71 | queryParam("username", "hdampf"). 72 | queryParam("password", "wert123$"). 73 | queryParam("client_id", "my_client_username"). 74 | queryParam("grant_type", "password"). 75 | queryParam("scope", "foobar_scope"). 76 | when(). 77 | post("/oauth/token"). 78 | then(). 79 | statusCode(HttpStatus.OK.value()). 80 | extract().response(); 81 | 82 | Assert.assertEquals("bearer", response.getBody().jsonPath().getString("token_type")); 83 | Assert.assertEquals("foobar_scope", response.getBody().jsonPath().getString("scope")); 84 | Assert.assertEquals("eyJhbGciOiJIUzI1NiJ9", 85 | response.getBody().jsonPath().getString("access_token").split("[.]")[0]); 86 | } 87 | 88 | @Test 89 | public void foobarIsAccessibleWithAccessToken() { 90 | Response tokenResponse = 91 | given(). 92 | header(new Header("Authorization", "Basic " + this.clientBasicAuthCredentials)). 93 | queryParam("username", "hdampf"). 94 | queryParam("password", "wert123$"). 95 | queryParam("client_id", "my_client_username"). 96 | queryParam("grant_type", "password"). 97 | queryParam("scope", "foobar_scope"). 98 | when(). 99 | post("/oauth/token"). 100 | then(). 101 | statusCode(HttpStatus.OK.value()). 102 | extract().response(); 103 | 104 | String token = tokenResponse.getBody().jsonPath().getString("access_token"); 105 | 106 | Response foobarResponse = 107 | given(). 108 | header(new Header("Authorization", "Bearer " + token)). 109 | when(). 110 | get("/foobar"). 111 | then(). 112 | statusCode(HttpStatus.OK.value()). 113 | extract().response(); 114 | 115 | Assert.assertEquals("hello OAuth2!", foobarResponse.getBody().print()); 116 | } 117 | 118 | @Test 119 | public void accessTokenAreInvalidatedAfterTimeout() throws InterruptedException { 120 | Response tokenResponse = 121 | given(). 122 | header(new Header("Authorization", "Basic " + this.clientBasicAuthCredentials)). 123 | queryParam("username", "hdampf"). 124 | queryParam("password", "wert123$"). 125 | queryParam("client_id", "my_client_username"). 126 | queryParam("grant_type", "password"). 127 | queryParam("scope", "foobar_scope"). 128 | when(). 129 | post("/oauth/token"). 130 | then(). 131 | statusCode(HttpStatus.OK.value()). 132 | extract().response(); 133 | 134 | String token = tokenResponse.getBody().jsonPath().getString("access_token"); 135 | 136 | Thread.sleep(2000); 137 | 138 | Response foobarResponse = 139 | given(). 140 | header(new Header("Authorization", "Bearer " + token)). 141 | when(). 142 | get("/foobar"). 143 | then(). 144 | statusCode(HttpStatus.UNAUTHORIZED.value()). 145 | extract().response(); 146 | 147 | Assert.assertEquals("invalid_token", foobarResponse.getBody().jsonPath().getString("error")); 148 | Assert.assertThat(foobarResponse.getBody().jsonPath().getString("error_description"), 149 | CoreMatchers.startsWith("Access token expired:")); 150 | 151 | } 152 | } 153 | --------------------------------------------------------------------------------