/ Spring Cloud  

Spring Cloud 13: Security

Security is a fundamental feature that cannot be bypassed in almost any application development. As we move applications to microservices architecture, security will become more complex. David Borsos presented the following four options at the London Microservices Conference in 2016:

  1. Single sign-on (SSO): Every microservice needs to interact with the authentication service, but this will generate a lot of very trivial network traffic and repeated work. The disadvantages of the scheme are very obvious;

  2. Distributed session: This solution stores user authentication information in shared storage (such as: Redis), and uses the user session ID as a key to implement a simple distributed hash mapping. When a user accesses the microservice, the user authentication information can be obtained from the shared storage through the session ID. This solution is very good most of the time, but its main disadvantage is that shared storage requires a certain protection mechanism, and the corresponding implementation will be relatively complicated at this time;

  3. Client token: The token is generated on the client and signed by the authentication server. The token contains enough information so that microservices can use it. A token is attached to each request to provide user authentication for the microservice. The security of this solution is relatively good, but because the token is generated and saved by the client, it is very troublesome to log out. A compromise solution is to verify the validity of the token through short-term tokens and frequent checking of authentication services. JSON Web Tokens (JWT) is a very good choice for client tokens;

  4. Client token combined with API gateway: Using this scheme means that all requests pass through the gateway, effectively hiding the microservice. When requested, the gateway converts the original user token into an internal session. In this way, the gateway can deregister the token, thereby solving the problems in the previous solution.

In this article we will focus on token-based solutions, and the best option for token-based solutions is OAuth2.0.

OAuth2.0

OAuth2.0 is described in Wikipedia as follows:

Open Authorization (OAuth) is an open standard that allows users to allow third-party applications to access private resources (such as photos, videos, contact lists) that the user has stored on a website without providing a username and password to the third party application.

OAuth allows users to provide a token instead of a username and password to access their data stored in a particular service provider. Each token authorizes a specific website (for example, a video editing website) to access a specific resource (for example, just a video in a certain album) within a specific period (for example, within the next 2 hours). In this way, OAuth allows users to authorize third-party websites to access certain information, but not all content, that they have stored in another service provider.

OAuth 2.0 is the next version of the OAuth protocol, but is not backward compatible with OAuth 1.0. OAuth 2.0 focuses on the simplicity of client developers, while providing specialized authentication processes for web applications, desktop applications and mobile phones, and living room devices.

For let us first understand a few key terms in OAuth2.0:

  • Resource Owner: The resource owner, in other words: User;
  • User Agent: User agent can be directly understood as a browser for Web applications;
  • Authorization server: Authentication server, that is, a server that provides user authentication and authorization. It can be a stand-alone server;
  • Resource server: the microservices that need to be protected.

Then, let’s take a look at the authentication flow chart of OAuth2.0 (taken from RFC6749):



The authentication process are as follows:

  1. After the user opens the client, the client asks the user for authorization;
  2. the user agrees to authorize the client;
  3. The client uses the authorization obtained in the previous step to apply for a token from the authentication server;
  4. After the authentication server authenticates the client, it confirms that it is correct and agrees to issue the token;
  5. The client uses the token to apply to the resource server for resources;
  6. The resource server confirms that the token is correct and agrees to open the resource to the client.

From the process we can see that the client can obtain the token from the authentication service only after the user authorize it. OAuth2.0 provides the following four authorization methods for client authorization:

  • Authorization code mode: This mode is the most complete and strictest authorization mode;
  • Simplified mode (implicit): This mode does not require the server of a third-party application, skips the “authorization code” step, and directly applies a token to the authentication server in the browser.
  • Password mode: The user provides his or her username and password to the client, and the client obtains authorization from the authentication server directly through this information;
  • Client credentials: The client obtains authentication from the authentication server in its own name, not in the name of the user. In this way, the authentication server treats the client as a user.

Example Project

1. Authentication server

First, we will build an authentication server. The server is also a standard Spring Boot application.

1.1 Dependency

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
</dependency>
<dependency>
<groupId>jakarta.xml.bind</groupId>
<artifactId>jakarta.xml.bind-api</artifactId>
</dependency>
<dependency>
<groupId>org.glassfish.jaxb</groupId>
<artifactId>jaxb-runtime</artifactId>
</dependency>

Here we introduced spring-cloud-security and spring-security-oauth2 dependencies. jakarta.xml.bind-api and jaxb-runtime are needed if using Java11.

1.2 Client management

For OAuth2.0 applications, it is necessary to implement a client authentication management. Here we directly inherit AuthorizationServerConfigurerAdapter and add a client application through in-memory management.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
@Configuration
public class OAuthConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
AuthenticationManager authenticationManager;
@Qualifier("userDetailsServiceBean")
@Autowired
UserDetailsService userDetailsService;

@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("demo")
.secret("{noop}pgDBd99tOX8d")
.authorities("test")
.authorizedGrantTypes("authorization_code", "refresh_token", "implicit", "password", "client_credentials")
.scopes("spring-cloud-demo")
.redirectUris("http://localhost:8761");
}

@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
//if used permitAll() this will make oauth/check_token endpoint to be unsecure
//security.checkTokenAccess("permitAll()");
security.checkTokenAccess("hasAuthority('test')");
}

@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints
.authenticationManager(authenticationManager)
.userDetailsService(userDetailsService);
}

@Bean
public TokenStore tokenStore() {
return new InMemoryTokenStore();
}
}

The client ID is set to demo.

he secret is set to: pgDBd99tOX8d. {noop} is the Password Storage Format used for Spring Security 5 default password encoder DelegatingPasswordEncoder. For details refer to: Password Storage Format.

We also authorized the client with authorization_code, refresh_token, implicit, password, client_credentials authentication mode. And in the following examples we will use authorization code mode and password mode for testing.

1.3 User authentication and authorization management

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class OAuthWebSecurityConfigurer extends WebSecurityConfigurerAdapter {
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}

@Override
@Bean
public UserDetailsService userDetailsServiceBean() throws Exception {
return super.userDetailsServiceBean();
}

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("user001")
.password("{noop}pwd001")
.roles("USER")
.and()
.withUser("admin")
.password("{noop}pwdAdmin")
.roles("USER", "ADMIN");
}

@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable();
http.authorizeRequests()
.anyRequest()
.authenticated()
.and()
.httpBasic();
}
}

In the above code, we still manage using in-memory and create two users:

  • user001: is an ordinary user, only USER role;
  • admin: is an administrator user with USER and ADMIN roles.

We also specified that all access needs to be secured, and enabled httpBasic authentication. Through this, when an unauthenticated user accesses, an authentication dialog box can pop up through the browser, allowing the user to enter a username and password for authentication.

1.4 Main application

1
2
3
4
5
6
7
@SpringBootApplication
@EnableAuthorizationServer
public class AuthServerApplication {
public static void main(String[] args) {
SpringApplication.run(AuthServerApplication.class, args);
}
}

Adding @EnableAuthorizationServer annotation to main class. With this annotation, the applicatoin will start with Spring Cloud Security and provide us with a series of endpoints to achieve OAuth2.0 authentication. These endpoints are:

  • /oauth/authorize: endpoint for authorization;
  • /oauth/token: endpoint for getting the access token;
  • /oauth/confirm_access: endpoint for submit user authorization confirmation;
  • /oauth/error: endpoint for acquire authentication server error information;
  • /oauth/check_token: token resolution endpoint for resource server access;
  • /oauth/token_key: If using a JWT token, expose the public key used for token verification.

1.5 User information loading endpoint

For the authentication service, we also need to provide a user information loading endpoint, so that other microservices can use the token to obtain information about the authenticated user from the authentication server, so that user authentication and authentication processing can be achieved.

1
2
3
4
5
6
7
8
9
10
11
12
@RestController
public class AuthEndpoint {
protected Logger logger = LoggerFactory.getLogger(AuthEndpoint.class);

@RequestMapping(value = { "/auth/user" }, produces = "application/json")
public Map<String, Object> user(OAuth2Authentication user) {
Map<String, Object> userInfo = new HashMap<>();
userInfo.put("user", user.getUserAuthentication().getPrincipal());
userInfo.put("authorities", AuthorityUtils.authorityListToSet( user.getUserAuthentication().getAuthorities()));
return userInfo;
}
}

This piece of code will obtain the current user information from Spring Security and convert it into a Map object to return.

1.6 Configuration file

1
2
3
4
5
6
7
# PORT
server.port=8290

spring.application.name=authserver

logging.level.org.springframework=INFO
logging.level.springclouddemo.authserver=DEBUG

2. User service

2.1 Dependency

The user service is a Resource server. When certain resources are accessed, user authentication and authentication are required. Therefore, a dependency on Spring Cloud Security needs to be introduced

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
</dependency>
<dependency>
<groupId>jakarta.xml.bind</groupId>
<artifactId>jakarta.xml.bind-api</artifactId>
</dependency>
<dependency>
<groupId>org.glassfish.jaxb</groupId>
<artifactId>jaxb-runtime</artifactId>
</dependency>

Similar to authentication server, jakarta.xml.bind-api and jaxb-runtime are needed if using Java11.

2.2 Main application

In Spring Cloud Security, we only need to add the @EnableResourceServer annotation to applications that need security management to enable security management and control.

1
2
3
4
5
6
7
8
@EnableDiscoveryClient
@SpringBootApplication
@EnableResourceServer
public class UserServiceApplication {
public static void main(String[] args) {
SpringApplication.run(UserServiceApplication.class, args);
}
}

When spring-cloud-security is introduced and the @EnableResourceServer annotation is added, the application will start the default security management process. If you want to add more detailed security management to your application, you need to inherit WebSecurityConfigurerAdapter And implement security-related configuration, we will not go into details here.

2.3 Configuration file

1
2
3
4
5
6
7
8
server.port=2200

spring.application.name=USER-SERVICE

eureka.client.service-url.defaultZone=http://localhost:8761/eureka

# OAuth
security.oauth2.resource.user-info-uri=http://localhost:8290/auth/user

We need to add an OAuth2 endpoint in the application configuration file to obtain authenticated user information

2.4 Endpoint of the current user information

In order to test, we need to add an endpoint in the user service to get the information of the currently logged in user。

1
2
3
4
5
6
@RequestMapping(value = "/my", method = RequestMethod.GET)
public User myDetail() {
String userName = SecurityContextHolder.getContext().getAuthentication().getPrincipal().toString();

return new User(userName, userName, "/avatar/default.png", "");
}

The code is very simple, directly obtain the information of the currently logged-in user from the SecurityContextHolder, construct a User object, and then return.

2.5 ResourceServerConfig

As our authenticatoin-server and user-service are seperated service, they need to share the same tokenStore to share and recognize a token. In user-service, we need to tell the service to use the RemoteTokenStore located at authenticatoin-server.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Configuration
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
@Bean
public ResourceServerTokenServices tokenService() {
RemoteTokenServices tokenServices = new RemoteTokenServices();
tokenServices.setClientId("demo");
tokenServices.setClientSecret("pgDBd99tOX8d");
tokenServices.setCheckTokenEndpointUrl("http://localhost:8290/oauth/check_token");
return tokenServices;
}

@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest()
.authenticated()
.and()
.requestMatchers()
.antMatchers("/users/**");
}
}

Ok, now user-service is secured. If we now start the user-service and access: http://localhost:2200/users/my, we will see the following return:



That is, permissions are now required to access the endpoint. So how do we provide permissions?

Earlier when we talked about OAuth2.0, we mentioned that there are four client authorization modes, so let’s take a look at how to use these authorization modes to implement specific user authentication processing.

Access through authorization code mode

First, let’s take a look at how to implement user authentication through the most comprehensive authorization process authorization code model provided by OAuth2.0.

Start the service in following order:

  1. Eureak Server
  2. Auth Server
  3. User service

Firstly, we need tp construct a URL to obtain an access token:

1
http://localhost:8290/oauth/authorize?response_type=code&client_id=demo&redirect_uri=http://localhost:8761&scope=spring-cloud-demo&state=63879

For this URL:

  • /oauth/authorize: This is the endpoint to get the authorization code;
  • client_id: ID of the client. It must be included in the requested URL. Please note that the value we give here is: demo, this is what we configured the client list in the authentication server;
  • response_type: indicates the authorization type, which is also required. For the authorization code mode, it is fixed as: code;
  • scope: indicates the scope of the requested authority, which can be omitted. This refers to whether the authorization is reused when there are different clients;
  • redirect_uri: the URI to redirect to after successful authorization;
  • state: indicates the current state of the client. Any value can be specified. This value will be returned regardless of whether the authorization is successful or not.

Visiting the URL, the page as follows:



This is the browser’s own user login window. Because we haven’t logged in, the authentication server turns on the browser login mode through httpBasic, so that when the user has not been authenticated, the above login window will pop up.

Enter user001 and pwd001 in the login window, which is one of the user configured in the previous authentication server, and then it will jump to the following page:



Here is the interface for whether the user is authorized. In this interface we can see that the ID and authorization scope of the incoming client. Here we click Approve, and the browser will jump to the address specified by redirect_uri. At this time, carefully observe the returned address, as follows:

1
http://localhost:8761/?code=w3ZOlC&state=63879

We can see that the address contains two parameters:

  • code: The authorization code issued for the authentication service. The validity period of the code is short. The default is 10 minutes, and the client can only use the code once, otherwise it will be rejected by the authentication server. The code has a one-to-one correspondence with the client ID and the redirect URI;
  • state: This is the parameter passed in when we requested above. The authentication server will return unaltered.

Now that we have obtained the authorization code in the first step. Then the second step is to obtain the access token based on this authorization code.



We need the following parameters to construct the http post request:

  • /oauth/token: This is the endpoint that gets the access token;
  • grant_type: indicates the authorization mode to be used, which must be filled in.
  • code: the authorization code we obtained in the previous step;
  • redirect_uri: The URI to redirect to after successfully. Similarly, we need to set it to the address given previously;
  • client_id: Client ID, must be filled in, and it must be the same as before.

In addition, in the screenshot above, we also need to fill in the authorization information. This is because our authentication server has enabled authorization verification. Here you can fill in the client ID and secret. At this point, the server treats the client as a user and is able to access the /oauth/token endpoint.

Here we mainly simplify the test method, so that we can pop up the authentication dialog box during the previous visit. However, this should not be done in actual production use. You need to define your own user login page and authorization page.

For the above request, we can get the following return



The returned content is as follows:

  • access_token: this is the access token obtained;
  • token_type: Token type. The default value returned by Spring Cloud OAuth is bearer;
  • expires_in: indicates the token expiration time, in seconds, the default is 12 hours;
  • refresh_token: update token, which can be used to obtain the next access token after expiration;
  • scope: The scope of the permission, which is generally consistent with the scope applied by the client.

With the access token next step we can access the user service:



The response:



When accessing, we need to specify Authorization in the header, and set the value to the access token obtained in the previous step, so that we can get the correct data returned, as shown in the figure above.

We have completed the test of user authentication through the authorization code mode. To summarize the steps:

  1. Visit auth-server and obtain authentication code.
  2. Use authentication code to get a access code.
  3. Use access code to access resource server.

Access through password mode

If the authentication server is third party, using the above process is not a big problem. If the authentication server is built by us, such as this example, the authorization above is a bit complicated. Let’s take a look at how to use password mode to simplify.

We directly request the authentication server to obtain the access token:



We need to set the client ID, secret, username and password of the user, and set grant_type to password, so that you can directly obtain the access token, as shown in the following figure:



And using the access token to access user service:



This shows that user authentication is also successful.

It can be seen that the password authentication mode can greatly simplify the user authentication process, but it is provided only when the user trusts the client. Therefore, this method is suitable for the case where the client and the authentication server are the same application.

For other client authorization modes, we will not discuss here.

Check out the source code here: Security demo