OAuth 2.0 is a way of securing APIs. Spring Security provides Spring Cloud Security and Spring Cloud OAuth2 components for implementing the rant flows we discussed above.
We'll create one more service, security-service, which will control authentication and authorization.
Create a new Maven project and follow these steps:
pom.xml
:<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-oauth2</artifactId> </dependency>
@EnableResourceServer
annotation in your application class. This will allow this application to work as a resource server. @EnableAuthorizationServer
is another annotation we will use to enable the authorization server as per OAuth 2.0 specifications:@SpringBootApplication @RestController @EnableResourceServer public class SecurityApp { @RequestMapping("/user") public Principal user(Principal user) { return user; } public static void main(String[] args) { SpringApplication.run(SecurityApp.class, args); } @Configuration @EnableAuthorizationServer protected static class OAuth2Config extends AuthorizationServerConfigurerAdapter { @Autowired private AuthenticationManager authenticationManager; @Override public void configure(AuthorizationServerEndpointsConfigurer endpointsConfigurer) throws Exception { endpointsConfigurer.authenticationManager(authenticationManager); } @Override public void configure(ClientDetailsServiceConfigurer clientDetailsServiceConfigurer) throws Exception { // Using hardcoded inmemory mechanism because it is just an example clientDetailsServiceConfigurer.inMemory() .withClient("acme") .secret("acmesecret") .authorizedGrantTypes("authorization_code", "refresh_token", "implicit", "password", "client_credentials") .scopes("webshop"); } } }
application.yml
, as shown in the following code:server.contextPath
: It denotes the context path.security.user.password
: We'll use the hardcoded password for this demonstration. You can re-configure it for real use:application.yml info: component: Security Server server: port: 9001 ssl: key-store: classpath:keystore.jks key-store-password: password key-password: password contextPath: /auth security: user: password: password logging: level: org.springframework.security: DEBUG
Now we have our security server in place, we'll expose our APIs using the new microservice api-service
, which will be used for communicating with external applications and UIs.
Create a new Maven project and follow these steps:
pom.xml
:<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-undertow</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <dependency> <groupId>com.packtpub.mmj</groupId> <artifactId>online-table-reservation-common</artifactId> <version>PACKT-SNAPSHOT</version> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-oauth2</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-eureka</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-hystrix</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-bus-amqp</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-stream-rabbit</artifactId> </dependency> <dependency> <groupId>org.apache.httpcomponents</groupId> <artifactId>httpclient</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <!-- Testing starter --> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> </dependency>
@EnableResourceServer
annotation in your application class. This will allow this application to work as a resource server:@SpringBootApplication @EnableDiscoveryClient @EnableCircuitBreaker @EnableResourceServer @ComponentScan({"com.packtpub.mmj.api.service", "com.packtpub.mmj.common"}) public class ApiApp { private static final Logger LOG = LoggerFactory.getLogger(ApiApp.class); static { // for localhost testing only LOG.warn("Will now disable hostname check in SSL, only to be used during development"); HttpsURLConnection.setDefaultHostnameVerifier((hostname, sslSession) -> true); } @Value("${app.rabbitmq.host:localhost}") String rabbitMqHost; @Bean public ConnectionFactory connectionFactory() { LOG.info("Create RabbitMqCF for host: {}", rabbitMqHost); CachingConnectionFactory connectionFactory = new CachingConnectionFactory(rabbitMqHost); return connectionFactory; } public static void main(String[] args) { LOG.info("Register MDCHystrixConcurrencyStrategy"); HystrixPlugins.getInstance().registerConcurrencyStrategy(new MDCHystrixConcurrencyStrategy()); SpringApplication.run(ApiApp.class, args); } }
api-service
configuration in application.yml
, as shown in the following code:security.oauth2.resource.userInfoUri
: It denotes the security service user URI.application.yml info: component: API Service spring: application: name: api-service aop: proxyTargetClass: true server: port: 7771 security: oauth2: resource: userInfoUri: https://localhost:9001/auth/user management: security: enabled: false ## Other properties like Eureka, Logging and so on
Now we have our security server in place, we'll expose our APIs using the new microservice api-service
, which will be used for communicating with external applications and UIs.
Now let's test and explore how it works for different OAuth 2.0 grant types.
We will enter the following URL in our browser. A request for authorization code is as follows:
https://localhost:9001/auth/oauth/authorize?response_type=code&client_id=client&redirect_uri=http://localhost:7771/1&scope=apiAccess&state=1234
Here, we provide the client ID (hardcoded client is by default we have registered in our security service), redirect URI, scope (hardcoded value apiAccess
in security service) and state. You must be wondering about the state
parameter. It contains the random number that we re-validate in response to prevent cross site request forgery.
If the resource owner (user) is not already authenticated, it will ask for the user name and password. Provide user as the username
and password as the password
; we have hardcoded these values in security service.
Once the login is successful, it will ask to provide your (resource owner) approval:
Select Approve and click on Authorize. This action will redirect the application to http://localhost:7771/1?code=o8t4fi&state=1234
.
As you can see, it has returned the authorization code and state.
Now, we'll use this code to retrieve the access code. We'll use the postman Chrome extension. First we'll add the authorization header using Username as client
and Password as clientsecret
, as shown in the following screenshot:
This will add the Authorization header to the request with the value Basic Y2xpZW50OmNsaWVudHNlY3JldA==
.
Now, we'll add a few other parameters to the request, as shown in the following screenshot, and then submit the request:
This returns the following response, as per the OAuth 2.0 specification:
{ "access_token": "6a233475-a5db-476d-8e31-d0aeb2d003e9", "token_type": "bearer", "refresh_token": "8d91b9be-7f2b-44d5-b14b-dbbdccd848b8", "expires_in": 43199, "scope": "apiAccess" }
Now we can use this information to access the resources owned by the resource owner. For example, if https://localhost:8765/api/restaurant/1
represents the restaurant with the ID of 1
, then it should return the respective restaurant details.
Without the access token, if we enter the URL, it returns the error Unauthorized
, with the message Full authentication is required to access this resource
.
Now, let's access this URL with the access token, as shown in the following screenshot:
As you can see, we have added the Authorization header with the access token.
Now, we will explore implicit grant implementation.
Implicit grants are very similar to authorization code grants, except for the code grant step. If you remove the first step—the code grant step (where the client application receives the authorization token from the authorization server)—from the authorization code grant, the rest of the steps are the same. Let's check it out.
Enter the following URL and parameters in the browser and press Enter. Also, make sure to add basic authentication, with client as the username
and password as the password
if asked:
https://localhost:9001/auth/oauth/authorize?response_type=token&redirect_uri=https://localhost:8765&scope=apiAccess&state=553344&client_id=client
Here, we are calling the authorization endpoint with the following request parameters: Response type, client ID, redirect URI, scope, and state.
When the request is successful, the browser will be redirected to the following URL with new request parameters and values:
https://localhost:8765/#access_token=6a233475-a5db-476d-8e31-d0aeb2d003e9&token_type=bearer&state=553344&expires_in=19592
Here, we receive the access_token
, token_type
, state, and expiry duration for the token. Now, we can make use of this access token to access the APIs, as used in the authorization code grant.
In this grant, we provide the username
and password
as parameters when requesting the access token, along with the grant_type
, client
, and scope
parameters. We also need to use the client ID and secret to authenticate the request. These grant flows use client applications in place of browsers, and are normally used in mobile and desktop apps.
In the following postman tool screenshot, the authorization header has already been added using basic authentication with client_id
and password
:
Once the access token is received by the client, it can be used in a similar way to how it is used in the authorization code grant.
In this flow, the client provides their own credentials and retrieves the access token. It does not use the resource owner's credentials and permissions.
As you can see in the following screenshot, we directly enter the token endpoint with only two parameters: grant_type
and scope
. The authorization header is added using client_id
and client secret
:
You can use the access token similarly as it is explained for the authorization code grant.