I have found this article at the Digital Ocean site does a great job of describing the OAuth2 Authorization code flow, so instead of rehashing what is involved in this flow I will directly jump into implementing this flow using Spring Boot/Spring Security.
The following diagram inspired by the one here shows a high level flow in an Authorization Code grant type:
I will have two applications - a resource server exposing some resources of a user, and a client application that wants to access those resources on behalf of a user. The Authorization server itself can be brought up as described in the previous blog post.
The rest of the post can be more easily followed along with the code available in my github repo here
Authorization Server
The Cloud Foundry UAA server can be easily brought up using the steps described in my previous blog post. Once it is up the following uaac commands can be used for populating the different credentials required to run the sample.These scripts will create a client credential for the client app and add a user called "user1" with a scope of "resource.read" and "resource.write".
# Login as a canned client uaac token client get admin -s adminsecret # Add a client credential with client_id of client1 and client_secret of client1 uaac client add client1 \ --name client1 \ --scope resource.read,resource.write \ -s client1 \ --authorized_grant_types authorization_code,refresh_token,client_credentials \ --authorities uaa.resource # Another client credential resource1/resource1 uaac client add resource1 \ --name resource1 \ -s resource1 \ --authorized_grant_types client_credentials \ --authorities uaa.resource # Add a user called user1/user1 uaac user add user1 -p user1 --emails user1@user1.com # Add two scopes resource.read, resource.write uaac group add resource.read uaac group add resource.write # Assign user1 both resource.read, resource.write scopes.. uaac member add resource.read user1 uaac member add resource.write user1
Resource Server
The resource server exposes a few endpoints, expressed using Spring MVC and secured using Spring Security, the following way:@RestController public class GreetingsController { @PreAuthorize("#oauth2.hasScope('resource.read')") @RequestMapping(method = RequestMethod.GET, value = "/secured/read") @ResponseBody public String read(Authentication authentication) { return String.format("Read Called: Hello %s", authentication.getCredentials()); } @PreAuthorize("#oauth2.hasScope('resource.write')") @RequestMapping(method = RequestMethod.GET, value = "/secured/write") @ResponseBody public String write(Authentication authentication) { return String.format("Write Called: Hello %s", authentication.getCredentials()); } }
There are two endpoint uri's being exposed - "/secured/read" authorized for scope "resource.read" and "/secured/write" authorized for scope "resource.write"
The configuration which secures these endpoints and marks the application as a resource server is the following:
@Configuration @EnableResourceServer @EnableWebSecurity @EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true) public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter { @Override public void configure(ResourceServerSecurityConfigurer resources) throws Exception { resources.resourceId("resource"); } @Override public void configure(HttpSecurity http) throws Exception { http .antMatcher("/secured/**") .authorizeRequests() .anyRequest().authenticated(); } }
This configuration along with properties describing how the token is to be validated is all that is required to get the resource server running.
Client
The client configuration for OAuth2 using Spring Security OAuth2 is also fairly simple, @EnableAuth2SSO annotation pulls in all the required configuration to wire up the spring security filters for OAuth2 flows:@EnableOAuth2Sso @Configuration public class OAuth2SecurityConfig extends WebSecurityConfigurerAdapter { @Override public void configure(WebSecurity web) throws Exception { super.configure(web); } @Override protected void configure(HttpSecurity http) throws Exception { http.csrf().disable(); //@formatter:off http.authorizeRequests() .antMatchers("/secured/**") .authenticated() .antMatchers("/") .permitAll() .anyRequest() .authenticated(); //@formatter:on } }
To call a downstream system, the client has to pass on the OAuth token as a header in the downstream calls, this is done by hooking a specialized RestTemplate called the OAuth2RestTemplate that can grab the access token from the context and pass it downstream, once it is hooked up a secure downstream call looks like this:
public class DownstreamServiceHandler { private final OAuth2RestTemplate oAuth2RestTemplate; private final String resourceUrl; public DownstreamServiceHandler(OAuth2RestTemplate oAuth2RestTemplate, String resourceUrl) { this.oAuth2RestTemplate = oAuth2RestTemplate; this.resourceUrl = resourceUrl; } public String callRead() { return callDownstream(String.format("%s/secured/read", resourceUrl)); } public String callWrite() { return callDownstream(String.format("%s/secured/write", resourceUrl)); } public String callInvalidScope() { return callDownstream(String.format("%s/secured/invalid", resourceUrl)); } private String callDownstream(String uri) { try { ResponseEntity<String> responseEntity = this.oAuth2RestTemplate.getForEntity(uri, String.class); return responseEntity.getBody(); } catch(HttpStatusCodeException statusCodeException) { return statusCodeException.getResponseBodyAsString(); } } }
Demonstration
The Client and the resource server can be brought up using the instructions here. Once all the systems are up, accessing the client will present the user with a page which looks like this:Accessing the secure page, will result in a login page being presented by the authorization server:
The client is requesting a "resource.read" and "resource.write" scope from the user, user is prompted to authorize these scopes:
Assuming that the user has authorized "resource.read" but not "resource.write", the token will be presented to the user:
At this point if the downstream resource is requested which requires a scope of "resource.read", it should get retrieved:
And if a downstream resource is requested with a scope that the user has not authorized - "resource.write" in this instance: