background
The distributed feature of microservice architecture can bring many benefits, but a single microservice cannot provide services independently. A microservice group needs to provide complete service experience as a whole, and how to realize the universal function supporting the whole needs to be carefully considered.
In terms of our company’s requirements, the general functions we need to implement include Routing, Authorization, Authentication, and back-end API Composition. We plan to deploy this set of capabilities in one place, the API Gateway, rather than repeatedly deploying them in each microservice.
ℹ️ introduces five typical BFF application scenarios in [Front-end Development in the Microservices /API Era] : 5 Practical BFF Use Cases. API Gateway is one of them.
OIDC is an authentication mode suitable for micro services. Since our services are mainly built on AWS, we plan to adopt AWS Cognito as the ID Provider (or Authorization Server).
About OAuth and OIDC
What is the difference between OAuth and OIDC? To put it simply, OAuth is a Framework and Open ID Connect is a protocol. In the OAuth2.0 framework, various roles in the authentication process and five authentication processes are defined.
Roles include:
-
Resource Owner, an entity that can grant access to a protected Resource. When the owner of a resource is a person, it is called an end user.
-
Resource Servers, servers that host protected resources, can respond to requests for protected resources by accessing tokens.
-
Client, an application that represents and is authorized by the resource owner to make requests to protected resources. The term “client” does not imply any specific implementation characteristics (for example, whether the application is executed on a server, desktop, or other device). It is also often referred to as an OAuth client for easy differentiation.
-
Authorization Server, which issues access tokens to clients after successfully authenticating resource owners and obtaining Authorization.
-
The User Agent, the resource owner’s User Agent, is the intermediary between the client and the resource owner (usually a Web browser).
The five certification processes are:
- Authorization Code Grant
- Implicit Grant
- Resource Owner Password Credentials Grant
- The Client Credentials Grant mode
- Extension Grants
Take the most common authorization code mode as an example. The authentication process is as follows:
+----------+ | Resource | | Owner | | | +----------+ ^ | (B) +----|-----+ Client Identifier +---------------+ | -+----(A)-- & Redirection URI ---->| | | User- | | Authorization | | Agent -+----(B)-- User authenticates --->| Server | | | | | | -+----(C)-- Authorization Code ---<| | +-|----|---+ +---------------+ | | ^ v (A) (C) | | | | | | ^ v | | +---------+ | | | |>---(D)-- Authorization Code ---------' | | Client | & Redirection URI | | | | | |<---(E)----- Access Token -------------------' +---------+ (w/ Optional Refresh Token)Copy the code
OAuth 2.0 is designed for authentication only, to grant access to data and functionality from one application to another. OpenID Connect (OIDC) is on top of OAuth 2.0, which adds user Login and Profile information. When an authentication server supports OIDC, it is sometimes referred to as an ID Provider because it provides the client with information about the owner of the resource.
OpenID Connect supports scenarios where a single login can be used across multiple applications, also known as single sign-on (SSO). For example, applications can support SSO through social networking services, such as Facebook or Twitter, so that users can choose to use their existing login information.
The OpenID Connect process looks the same as OAuth. The only differences are that in the initial request, a specific SCOPE is used: OpenID, and in the final exchange, the client receives both an Access Token and an ID Token.
About Cognito
AWS Cognito can add user registration, login, and access control capabilities to Web and mobile applications. Users can scale to millions and log in with social identity providers (such as Apple, Facebook, Google, and Amazon) as well as enterprise identity providers through SAML 2.0 and OpenID Connect. Simply put, Cognito is an AWS ID Provider that can easily integrate with ALB, AWS API Gateway, and CloudFront.
The SUPPORT for the OAuth authorization code pattern is shown in the following figure in the AWS Cognito documentation:
plan
As mentioned earlier, we used AWS Cognito as the authentication server, and we used a combination of Spring Cloud Gateway and Spring Security for the implementation of the OAuth client (API Gateway).
The certification process
The microservice in the figure above acts as a resource server to show how to secure the service using Spring Security 5.2+. Any user (machine) that invokes it is responsible for providing a valid Access Token, in our case an anonymous Token in JWT format. In addition to the typical access token, JWT also allows the transmission of AuthN/ Authz-related declarations, such as user names or roles/permissions. This eliminates the need for microservices to continually request the original identity provider for such information.
API Gateway will use session-based login to display OAuth 2’s authorization code pattern. In addition, it shows how to control HTTP access requests using the appropriate OAuth token as required by the resource server. The key is that access/refresh tokens acquired from Cognito are never exposed to the browser. Spring Cloud Gateway supports the WebFlux mode and the traditional MVC mode. Since all back-end requests need to pass through the API Gateway, it has high requirements for high throughput and low latency, so we choose the non-blocking WebFlux mode.
The relevant sequence diagram is as follows:
authentication
We plan to manage users, the roles they belong to, and the URL permissions for those roles in an account service on the back end. Spring Security supports authentication as shown below, but it can only be hard-coded and cannot meet our requirements.
http.authorizeExchange()
.pathMatchers(HttpMethod.GET, "/api/account/**")
.hasRole("account.access");
Copy the code
We need to consider how the API gateway can get real-time permission configuration from the account service for dynamic authentication. We will implement a custom ReactiveAuthorizationManager, as authentication filter, and added to the configuration in the following way. In the custom filter, the latest permission configuration is periodically fetched from the back-end account service and refreshed to the API gateway.
http.authorizeExchange()
.pathMatchers("/api/**")
.access(authorizationManager);
Copy the code
For user roles, we plan to use Cognito’s Group feature for user rights control. The reason for this is that Cognito makes it easy to maintain a user’s relationship with a group, and it’s also easy to get information about the group a user belongs to in a token. The key is that we need to manually convert Cognito Group information to roles/permissions that Spring Security can recognize. We need to customize a ReactiveOAuth2UserService Bean, implement group to role transformation. You can then use GrantedAuthority to verify the user’s permissions.
API composite
We hope to realize the function of API combination in THE API gateway, and combine the apis of multiple micro-services at the back end into a relatively complete API in terms of functions to provide services externally. For example, “When displaying the page, get the list information of the diary from the diary list API, and get multiple comments from the comment list API at the same time.” In this case, multiple back-end apis need to be requested simultaneously. In this article’s code example, I’ll combine the order AND inventory apis and return them to the front end.
Unfortunately, Spring Cloud Gateway does not officially provide this functionality, although there is a lot of chatter in the community (see Have Routes Support Multiple URIs? . However, community member spencergibb suggested using the ProxyExchange object provided by the Spring Cloud Gateway for this functionality. The same implementation is used in our example.
@PostMapping("/proxy")
public Mono<ResponseEntity<Foo>> proxy(ProxyExchange<Foo> proxy) throws Exception {
return proxy.uri("http://localhost:9000/foos/") //
.post(response -> ResponseEntity.status(response.getStatusCode()) //
.headers(response.getHeaders()) //
.header("X-Custom"."MyCustomHeader") //
.body(response.getBody()) //
);
}
Copy the code
implementation
AWS Cognito
- Creating an AWS instance
First, construct an AWS Cognito instance using Terraform.
cd ./aws-cognito
terraform init
terraform apply
Copy the code
The Client ID and user pool ID are displayed in the window for future use.
- Access to the client secret
For security reasons, Terrafrom does not allow client secret to be printed in a window, so you need to obtain it using the following command.
aws cognito-idp describe-user-pool-client --user-pool-id <your-user-pool-id> \
--client-id <your-client-id>
Copy the code
- Register the user and confirm
Registered user [email protected].
aws cognito-idp sign-up --region <your-aws-region> \
--client-id <your-client-id> --username [email protected] \
--password password123
aws cognito-idp admin-confirm-sign-up --region <your-aws-region> \
--user-pool-id <your-user-pool-id> \
--username [email protected]
Copy the code
- Join the group
Add the test user to the account.access group.
aws cognito-idp admin-add-user-to-group --user-pool-id <your-user-pool-id> \ --username [email protected] \ --group-name account.accessCopy the code
API gateway
- Rely on
Add the following dependencies to build.gradle:
implementation "org.springframework.boot:spring-boot-starter-webflux"
implementation "org.springframework.boot:spring-boot-starter-oauth2-client"
implementation "org.springframework.security:spring-security-oauth2-jose"
implementation "org.springframework.security:spring-security-config"
implementation "org.springframework.cloud:spring-cloud-starter-gateway"
implementation "org.springframework.cloud:spring-cloud-gateway-webflux"
Copy the code
- configuration
Add the OAuth2 configuration in application.yaml. You need to configure Cognito’s client-ID, client-secret, and other information obtained in the previous step into this file.
spring:
security:
oauth2:
client:
provider:
cognito:
issuerUri: https://cognito-idp.<region-id>.amazonaws.com/<region-id>_<user-pool-id>
user-name-attribute: username
registration:
cognito:
client-id: <client-id>
client-secret: <client-secret>
client-name: scg-cognito-sample-user-pool
provider: cognito
scope: openid
redirect-uri: '{baseUrl}/login/oauth2/code/{registrationId}'
authorization-grant-type: authorization_code
Copy the code
In addition, you need to add routing-related configurations, in this case TokenRelayFilter, which is used to pass tokens to back-end microservices via Http headers.
spring:
cloud:
gateway:
default-filters:
- TokenRelay
routes:
- id: account_service_route
uri: http://localhost:8082
predicates:
- Path=/api/account/**
- id: order_service_route
uri: http://localhost:8083
predicates:
- Path=/api/order/**
- id: storage_service_route
uri: http://localhost:8084
predicates:
- Path=/api/storage/**
Copy the code
- code
Set a custom authentication filter MyAuthorizationManager in SecurityWebFilterChain.
@Bean
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http, ReactiveClientRegistrationRepository clientRegistrationRepository, MyAuthorizationManager authorizationManager) {
// Authenticate through configured OpenID Provider
http.oauth2Login(withDefaults());
// Also logout at the OpenID Connect provider
http.logout(logout -> logout.logoutSuccessHandler(new OidcClientInitiatedServerLogoutSuccessHandler(
clientRegistrationRepository)));
// add authorization filters
http.authorizeExchange()
.pathMatchers("/api/**")
.access(authorizationManager);
// Require authentication for all requests
http.authorizeExchange().anyExchange().authenticated();
// Disable CSRF in the gateway to prevent conflicts with proxied service CSRF
http.csrf().disable();
return http.build();
}
Copy the code
Use ReactiveOAuth2UserService, converts Cognito group information to Spring Security is to identify the role of/authority.
@Bean
public ReactiveOAuth2UserService<OidcUserRequest, OidcUser> oidcUserService(a) {
final OidcReactiveOAuth2UserService delegate = new OidcReactiveOAuth2UserService();
return (userRequest) -> {
// Delegate to the default implementation for loading a user
return delegate.loadUser(userRequest)
.map(user -> {
Set<GrantedAuthority> mappedAuthorities = new HashSet<>();
user.getAuthorities().forEach(authority -> {
if (authority instanceof OidcUserAuthority) {
OidcUserAuthority oidcUserAuthority = (OidcUserAuthority) authority;
// get cognito groups from token
JSONArray groups = oidcUserAuthority.getIdToken().getClaim("cognito:groups");
if (Objects.nonNull(groups)) {
groups.stream()
// map group to role
.map(roleName -> "ROLE_" + roleName)
.map(SimpleGrantedAuthority::new) .forEach(mappedAuthorities::add); }}});return new DefaultOidcUser(mappedAuthorities, user.getIdToken(), user.getUserInfo());
});
};
}
Copy the code
Custom authentication filter MyAuthorizationManager code is as follows:
@Component
public class MyAuthorizationManager implements ReactiveAuthorizationManager<AuthorizationContext> {
// ...
@Override
public Mono<AuthorizationDecision> check(Mono
authenticationMono, AuthorizationContext authorizationContext)
{
ServerHttpRequest request = authorizationContext.getExchange().getRequest();
// pass OPTIONS request of CORS
if (request.getMethod() == HttpMethod.OPTIONS) {
return Mono.just(new AuthorizationDecision(true));
}
// authenticate
String url = request.getURI().getPath();
return authenticationMono
.map(auth -> new AuthorizationDecision(urlAuthorityChecker.check(auth.getAuthorities(), url)))
.defaultIfEmpty(new AuthorizationDecision(false)); }}Copy the code
The specific permission verification logic is implemented in UrlAuthorityChecker.
@Component
@EnableScheduling
public class UrlAuthorityChecker {
// ...
/** * map for permission and url */
private Map<String, String> permissionUrlMap;
/** * check granted authorities of user */
public boolean check(Collection<? extends GrantedAuthority> authorities, String requestedUrl) {
// loop all the authorities of user to find out if the url is authenticated
for (GrantedAuthority authority : authorities) {
String authorizationUrl = permissionUrlMap.get(authority.getAuthority());
if(authorizationUrl ! =null && antPathMatcher.match(authorizationUrl, requestedUrl)) {
return true; }}return false;
}
/** * create newPermissionUrlMap and replace the old one. * add scheduled task to refresh the map every certain ms. */
@Scheduled(initialDelay = 0, fixedDelay = REFRESH_DELAY)
private void updatePermissionNameAuthorizationUrlMap(a) {
Map<String, String> permissions = permissionManager.getPermissions();
Map<String, String> newPermissionUrlMap = new ConcurrentHashMap<>();
permissions.forEach((k,v) -> newPermissionUrlMap.put("ROLE_"+ k, v)); permissionUrlMap = newPermissionUrlMap; }}Copy the code
In terms of API composition, we implemented the integration of the order API and inventory API using XyExchange.
@GetMapping("/composition/{id}")
publicMono<? extends ResponseEntity<? >> proxy(@PathVariableInteger id, ProxyExchange<? > proxy)throws Exception {
return proxy.uri("http://localhost:8083/api/order/get/" + id)
.get(resp -> ResponseEntity.status(resp.getStatusCode())
.body(resp.getBody()))
.flatMap(re1 -> proxy.uri("http://localhost:8084/api/storage/get/" + id)
.get(resp -> ResponseEntity.status(resp.getStatusCode())
.body(Map.of("order",re1.getBody(),"storage",resp.getBody()))));
}
Copy the code
Asset Management Server (Microservices)
- Rely on
Add the following dependencies to build.gradle:
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server'
Copy the code
- configuration
In application.yaml, add the resource server configuration for OAuth2.
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: https://cognito-idp.<region-id>.amazonaws.com/<region-id>_<user-pool-id>
Copy the code
- code
Configure the resource server in SecurityConfig, enable JWT token validation, and take the same steps as the API gateway to replace the Cognito Group information in the token with roles/permissions.
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
// Validate tokens through configured OpenID Provider
http.oauth2ResourceServer().jwt().jwtAuthenticationConverter(jwtAuthenticationConverter());
// Permit request for permissions
http.authorizeRequests().antMatchers(HttpMethod.GET, "/api/account/permissions").permitAll();
// Require authentication for the other requests
http.authorizeRequests().anyRequest().authenticated();
}
private JwtAuthenticationConverter jwtAuthenticationConverter(a) {
JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
// Convert realm_access.roles claims to granted authorities, for use in access decisions
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(
jwt -> {
Object groups = jwt.getClaims().get("cognito:groups");
if (Objects.nonNull(groups)) {
return((List<? >) groups).stream() .map(roleName ->"ROLE_" + roleName)
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
} else {
return newArrayList<>(); }});returnjwtAuthenticationConverter; }}Copy the code
The full sample code can be downloaded from my Github.
Other options
In addition to the authorization code mode of OAuth, you can also choose the simple mode, in which the login and token acquisition are handed over to the front end, the API Gateway is only used as the resource server, and the back-end micro services are only accessible to the API Gateway. This is desirable when the front end is a Native Application. Local application refers to the client software installed on the device, which is different from the browser-based SPA. The browser-based application code is open and the token is easy to leak, so the simple mode is not applicable.
Related articles
- BFF: Why Netflix, Twitter, Recruit chose BFF
- Getting started with BFF –5 practical BFF use cases
- [Front-end development in the Microservices /API era] BFF Progression — three common anti-patterns in practice
Refer to the link
- OAuth2.0
- Spring Cloud Gateway with OpenID Connect and Token Relay
- Understanding Amazon Cognito user pool OAuth 2.0 grants
- Have Routes Support Multiple URIs?
- Spring cloud samples: sample-gateway-oauth2login
- JavaDoc: org.springframework.cloud.gateway.webflux.ProxyExchange