Hello world spring-boot project (Part 4)
After having simple username and password protection, we want to extend the system to allow more apps accessing our resources. In Part 4, we try to implement simple OAuth 2 in our system.
When a client send a request to a resource endpoint (Resource Server), the client need to send together a HTTP Authorization header:
Authorization: Bearer $TOKEN
The $TOKEN is obtained from an Authorization Server.
Therefore, we first build the Authorization Server with spring framework first.
We first add some dependency to our build.gradle file:-
compile("org.springframework.security.oauth:spring-security-oauth2:2.0.7.RELEASE")
Then, we add a Java class to create the Authorization Server.
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
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;
@Configuration
@EnableAuthorizationServer
class OAuth2Config extends AuthorizationServerConfigurerAdapter {
public static final String RESOURCE_ID = "hello_resource";
@Autowired
private AuthenticationManager authenticationManager;
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.authenticationManager(authenticationManager);
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
// @formatter:off
clients.inMemory()
.withClient("client-with-registered-redirect")
.authorizedGrantTypes("authorization_code")
.authorities("ROLE_CLIENT")
.scopes("read", "trust")
.resourceIds(RESOURCE_ID)
.redirectUris("http://anywhere?key=value")
.secret("secret123")
.and()
.withClient("my-client-with-secret")
.authorizedGrantTypes("client_credentials", "password")
.authorities("ROLE_CLIENT")
.scopes("read")
.resourceIds(RESOURCE_ID)
.secret("secret");
// @formatter:on
}
}
The Authorization Server has the following default endpoint.
"{[/oauth/authorize]}": {
"bean": "oauth2EndpointHandlerMapping",
"method": "public org.springframework.web.servlet.ModelAndView org.springframework.security.oauth2.provider.endpoint.AuthorizationEndpoint.authorize(java.util.Map<java.lang.String, java.lang.Object>,java.util.Map<java.lang.String, java.lang.String>,org.springframework.web.bind.support.SessionStatus,java.security.Principal)"
},
"{[/oauth/authorize],methods=[POST],params=[user_oauth_approval]}": {
"bean": "oauth2EndpointHandlerMapping",
"method": "public org.springframework.web.servlet.View org.springframework.security.oauth2.provider.endpoint.AuthorizationEndpoint.approveOrDeny(java.util.Map<java.lang.String, java.lang.String>,java.util.Map<java.lang.String, ?>,org.springframework.web.bind.support.SessionStatus,java.security.Principal)"
},
"{[/oauth/token],methods=[GET]}": {
"bean": "oauth2EndpointHandlerMapping",
"method": "public org.springframework.http.ResponseEntity<org.springframework.security.oauth2.common.OAuth2AccessToken> org.springframework.security.oauth2.provider.endpoint.TokenEndpoint.getAccessToken(java.security.Principal,java.util.Map<java.lang.String, java.lang.String>) throws org.springframework.web.HttpRequestMethodNotSupportedException"
},
"{[/oauth/token],methods=[POST]}": {
"bean": "oauth2EndpointHandlerMapping",
"method": "public org.springframework.http.ResponseEntity<org.springframework.security.oauth2.common.OAuth2AccessToken> org.springframework.security.oauth2.provider.endpoint.TokenEndpoint.postAccessToken(java.security.Principal,java.util.Map<java.lang.String, java.lang.String>) throws org.springframework.web.HttpRequestMethodNotSupportedException"
},
"{[/oauth/check_token]}": {
"bean": "oauth2EndpointHandlerMapping",
"method": "public java.util.Map<java.lang.String, ?> org.springframework.security.oauth2.provider.endpoint.CheckTokenEndpoint.checkToken(java.lang.String)"
},
"{[/oauth/confirm_access]}": {
"bean": "oauth2EndpointHandlerMapping",
"method": "public org.springframework.web.servlet.ModelAndView org.springframework.security.oauth2.provider.endpoint.WhitelabelApprovalEndpoint.getAccessConfirmation(java.util.Map<java.lang.String, java.lang.Object>,javax.servlet.http.HttpServletRequest) throws java.lang.Exception"
},
"{[/oauth/error]}": {
"bean": "oauth2EndpointHandlerMapping",
"method": "public org.springframework.web.servlet.ModelAndView org.springframework.security.oauth2.provider.endpoint.WhitelabelErrorEndpoint.handleError(javax.servlet.http.HttpServletRequest)"
},
Authroization Code Grant Summary in our implementation
1. Authorization Server authenticates the User
The user need to login to the authorization server using the login name and password. In this case, we use HTTP Basic, so the spring default username is user and the password is generated in the boot log.
2. Client starts the authorization flow and obtain User’s approval
The owner need to authorize the 3rd party to use its resource by
GET http://localhost:8080/oauth/authorize?response_type=code&client_id=client-with-registered-redirect&redirect_url=http://client_host?key=value&scope=read
3. Authorization Server issues an authorization code (opaque one-time token)
The authorization end point will redirect the user to the registered redirect uri with a code (authorization code) for the 3rd party to access the resource.
http://anywhere/?key=value&code=H3aJ6s
4. Client exchanges the authorization code for an access token
Then, the 3rd party can obtain an access token from the Authorization Server using the code.
We try to do this using Postman to illustrate the result.
The 3rd party POST the authorization code (i.e. H3aJ6s) to the Authorization Server with a grant type authorization_code. The HTTP request need to be HTTP-Basic authenticated using the client_id (i.e. client-with-registered-redirect) as username and secret (i.e. secret123) as the password to the default OAuth2 Token end point.
POST http://localhost:8080/oauth/token
Finally, the server returns an access_token ($TOKEN) to the 3rd party for accessing the Resource Server!
O! Wait, how about the Resource Server? How to implement it using Spring?
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
@Configuration
@EnableResourceServer
class ResourceServer extends ResourceServerConfigurerAdapter {
public static final String RESOURCE_ID = "hello_resource";
@Override
public void configure(HttpSecurity http) throws Exception {
// @formatter:off
http
.requestMatchers().antMatchers("/people").and()
.authorizeRequests()
.anyRequest().access("#oauth2.hasScope('read')");
// @formatter:on
}
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.resourceId(RESOURCE_ID);
}
}
By substituting the $TOKEN in the introduction when accessing the /people resource, we can obtain the JSON response we get in previous parts. Cool!
http://www.slideshare.net/SpringCentral/syer-microservicesecurity
https://github.com/spring-projects/spring-security-oauth/tree/master/tests/annotation
https://raymondhlee.wordpress.com/2014/12/21/implementing-oauth2-with-spring-security/
https://github.com/spring-projects/spring-security-javaconfig/tree/master/samples