Advanced Microservices Security with Spring and OAuth2
The main purpose of this article is to show a sample security architecture for microservices and an authorization server behind API gateways.
Join the DZone community and get the full member experience.
Join For FreePreface
A basic sample and theoretical introduction on how Spring Cloud Security and OAuth2 works are available in my previous articles. Below you can see a picture illustrating the architecture of our solution. We have two microservices, OAuth2 authentication server, and Eureka discovery service behind the Zuul gateway.
Gateway
Let's start from an API gateway as an entry point into our system. Following Spring Cloud Security Documentation we add @EnableOAuth2Sso
into module main class. Thus, the gateway is forwarding OAuth2 access tokens downstream to the services it is proxying. It also sets form-based protection of our resources. All requests are redirecting to a login page if the user is not authenticated. Here's our login page.
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<head>
<title>Login page</title>
<meta charset="utf-8" />
</head>
<body>
<h1>Login page</h1>
<p>Example user: user / password</p>
<p th:if="${loginError}" class="error">Wrong user or password</p>
<form th:action="@{/login}" method="post">
<label for="username">Username</label>:
<input type="text" id="username" name="username" autofocus="autofocus" /> <br />
<label for="password">Password</label>:
<input type="password" id="password" name="password" /> <br />
<input type="submit" value="Log in" />
</form>
<p><a href="/index" th:href="@{/index}">Back to home page</a></p>
</body>
</html>
We also have to initialize MVC view resolver for login and index views.
@Configuration
public class WebMvcConfig extends WebMvcConfigurerAdapter {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/login").setViewName("login");
registry.addViewController("/").setViewName("index");
}
}
We are also using Thymeleaf for our view template engine and Thymeleaf - Spring Security integration modules in order to utilize the sec:authentication and sec:authorize attributes. That library has some other useful features. It will automatically add the CSRF token to our login form and helps us resolve templates from /src/main/resource/templates directory. Surprisingly, that is not all - we also have to provide security configuration to allow the login page to display and disable basic security. In addition, we need to change default in-memory based authentication to JDBC-based. In this sample, we use a MySQL database:
@Configuration
@EnableWebSecurity
@Order(-10)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private DataSource dataSource;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.permitAll()
.and().httpBasic().disable();
}
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.jdbcAuthentication().dataSource(dataSource);
}
}
Authorization
We initialize our authorization server using @EnableAuthorizationServer
. Now it has basic security by default. But we have already configured form-based security on the gateway. The user is logged in there, so we would like to force him to login to the authorization server as well. The problem is that without authentication on our authorization server we will not be able to get an OAuth2 token. Even if we force him to login into the authorization server it won't authenticate the session started on the gateway. We can try it calling this URL in our web browser:
http://localhost:8765/uaa/oauth/authorize?response_type=token&client_id=acme&redirect_uri=http://example.com&scope=openid&state=48532
Review our in-depth guide covering OAuth2 for Spring Boot APIs
Generally, I spend some hours working on a solution to this problem. I checked out some samples - here's a smart solution with JWT tokens: UAA Behind Zuul. But all of them, in my opinion, are some kind of workaround. So, I decided to use Spring Session project. It provides an API and implementations for managing a user’s session information. With this framework HTTP session started on Zuul gateway is stored in a database. We only have to annotate our gateway and authorization server main classes with @EnableJdbcHttpSession
and provide Datasource connection properties with @Bean
. Other steps are managed by Spring Boot out of the box. In this sample, MySQL was used. Spring Session manages the storing and getting of HTTP sessions using the two tables below:
CREATE TABLE SPRING_SESSION (
SESSION_ID CHAR(36),
CREATION_TIME BIGINT NOT NULL,
LAST_ACCESS_TIME BIGINT NOT NULL,
MAX_INACTIVE_INTERVAL INT NOT NULL,
PRINCIPAL_NAME VARCHAR(100),
CONSTRAINT SPRING_SESSION_PK PRIMARY KEY (SESSION_ID)
) ENGINE=InnoDB;
CREATE INDEX SPRING_SESSION_IX1 ON SPRING_SESSION (LAST_ACCESS_TIME);
CREATE TABLE SPRING_SESSION_ATTRIBUTES (
SESSION_ID CHAR(36),
ATTRIBUTE_NAME VARCHAR(200),
ATTRIBUTE_BYTES BLOB,
CONSTRAINT SPRING_SESSION_ATTRIBUTES_PK PRIMARY KEY (SESSION_ID, ATTRIBUTE_NAME),
CONSTRAINT SPRING_SESSION_ATTRIBUTES_FK FOREIGN KEY (SESSION_ID) REFERENCES SPRING_SESSION(SESSION_ID) ON DELETE CASCADE
) ENGINE=InnoDB;
CREATE INDEX SPRING_SESSION_ATTRIBUTES_IX1 ON SPRING_SESSION_ATTRIBUTES (SESSION_ID);
Security configuration on the authorization server is really simple. All requests need to be authenticated.
@Configuration
@EnableWebSecurity
@Order(-10)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().anyRequest().authenticated();
}
}
We need to add a dependency to Spring Session both on the gateway and authorization modules. After login has been performed on Zuul gateway, an HTTP session is stored in the MySQL database. After that, when we are calling OAuth2 with gateway's JSESSIONID authorization server, find it in the database and successfully authenticate it. A generated token is returned to the caller. Here's a complete list of dependencies from pom.xml.
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity4</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session</artifactId>
</dependency>
</dependencies>
Services
There are two microservices defined in account-service and customer-service modules. Sample application source code is available on GitHub (branch advanced). Here's a fragment of the application.yml configuration on gateway. Zuul has to route to Account, Customer services, and Authorization server. DZone’s previously covered how to implement Oauth2 security in Microservices.
zuul:
routes:
uaa:
path: /uaa/**
sensitiveHeaders:
serviceId: auth-server
account:
path: /account/**
sensitiveHeaders:
serviceId: account-service
customer:
path: /customer/**
sensitiveHeaders:
serviceId: customer-service
eureka:
client:
registerWithEureka: false
serviceUrl:
defaultZone: http://localhost:8761/eureka/
security:
user:
name: root
password: password
oauth2:
client:
accessTokenUri: http://localhost:8765/uua/oauth/token
userAuthorizationUri: http://localhost:8765/uua/oauth/authorize
clientAuthenticationScheme: form
resource:
userInfoUri: http://localhost:8765/uaa/user
preferTokenInfo: false
sessions: ALWAYS
After calling http://localhost:8765 in the web browser you will see a Login page. Enter your username, and the password you inserted in the database users table.
You should now be logged in. Now you can send a token request to the authorization server by calling the address below. Then you should see an OAuth Approval page and after approval, the authorization server sends you a response with the requested OAuth2 token.
http://localhost:8765/uaa/oauth/authorize?response_type=token&client_id=acme&redirect_uri=http://example.com&scope=openid&state=48532
The final response is visible below:
http://example.com/#access_token=60519c8b-42fa-4995-8192-866e266f9cec&token_type=bearer&state=48532&expires_in=43199
And finally, you can call your endpoint in your service with HTTP header Authorization:"Bearer 60519c8b-42fa-4995-8192-866e266f9cec".
Opinions expressed by DZone contributors are their own.
Comments