Initial understanding of the resource server in OAuth 2.0

Resource Server is exactly what and how to use few tutorials to talk specifically about this stuff, today we will talk about the concept first, to lay a foundation for subsequent use.

The shortcomings of the traditional security approach

The traditional way to protect an application is to get the credentials (JWT is one of them) given by the server through a username and password (also can be an authentication code), and then carry the credentials to request the interface to get the corresponding resources (Resource). Most of the standalone applications use this model is very convenient and simple.

But once your project gets bigger and needs to be transformed into a microservice, using this approach becomes a bit unwieldy, as each service’s resources need to be authenticated and authorized, so a paradigm is needed to simplify the process.

Authentication and authorization can be loosely coupled

Authentication of users and access control of resources can actually be separated. Let’s say you buy a plane ticket, now you can not only get your ticket from the airline ticketing department, but also from a third party ticketing center offline or online, eventually each flight will verify your ticket to determine if you are eligible for registration and will not care about the channel you purchased your ticket from. This is a real-life example.

If in microservices, each of our services only needs to verify that the request has the permission to access the resource, we can abstract the logic of resource access verification to a common model and implement it with code, very much in line with the idea of decentralization of microservices. This is the fundamental meaning of resource server.

Resource Server

The full name of the resource server is OAuth2 Resource Server, which is actually part of the OAuth 2.0 protocol and is usually implemented with the help of Json Web Token (there is actually another one called Opaque Tokens that can also be used). OAuth 2.0 Authorization Server sends the client a Json Web Token , which is used to verify that the client has permission to access a resource.

When a single application is transformed into a microservice, the authorization service is actually better centralized, unified management of user authentication authorization and issued to the client Token. each specific service is also a resource server, we only need to abstract the access control interface can be. Here follow a principle: common encapsulation into a class library (jar), personalized abstraction for configuration. The general flow chart is as follows.

image

This way the authorization server only cares about issuing Token function, and the resource server is only responsible for verifying Token. Whenever a new service is accessed we only need to add the supporting resource service dependency and configuration, which is very simple to transform.

There is also a way to centralize the resource server on the Internet, which is to centralize the authentication process at the gateway. Personally, I don’t think it is easy to accept unless you have had similar experience, and there are some security context cross-service issues to deal with. The above model is highly recommended for beginners.

I have actually done a preliminary implementation and transformation of the above model, and will then explain how to implement a resource server in microservices using Spring Security, and some of the key points related to transforming microservices in monolithic applications.

Resource server transformation

This DEMO was originally a standalone application, where authentication and authorization were used in one application. After the transformation into a standalone service, the original authentication will be stripped out (more on how to implement this later) and the service will retain only the user credential (JWT) based access control functionality. We will then implement the capability step by step.

Required dependencies

On top of Spring Security, we need to add new dependencies to support OAuth2 Resource Server and JWT. we need to add the following dependency libraries.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- 资源服务器 -->
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-oauth2-resource-server</artifactId>
</dependency>
<!-- jose -->
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-oauth2-jose</artifactId>
</dependency>

Spring Security 5.x removes the OAuth2.0 authorization server and retains the OAuth2.0 resource server.

JWT decoding

To validate JWT you must implement decoding functionality for JWT, in Spring Security OAuth2 Resource Server module, a decoder is provided by default, this decoder needs to be called based on.

1
spring.security.oauth2.resourceserver

The metadata under the configuration to generate the decoding configuration, where most of the configuration is to call the well-known endpoint opened by the authorization server, containing a series of parameters to parse and verify the JWT.

  • jwkSetUri is generally the well-known endpoint provided by the authorization server to obtain the JWK configuration to verify the JWT Token.
  • jwsAlgorithm Specifies the algorithm used by jwt, default RSA-256 .
  • issuerUri Endpoint for getting OAuth2.0 authorization server metadata.
  • publicKeyLocation The path to the public key used for decoding, as the resource server will only hold the public key and should not hold the private key.

In order to achieve a smooth transition, the default configuration will definitely not work, and a JWT decoder needs to be customized. Let’s implement it step by step next.

Separate public and private keys

The resource server can only hold the public key, so it needs to export a public key from the previous jks file.

1
keytool -export -alias felordcn -keystore <jks证书全路径>  -file <导出cer的全路径>

For example.

1
keytool -export -alias felordcn -keystore D:\keystores\felordcn.jks  -file d:\keystores\publickey.cer

Put the separated cer public key file under the path of the original jks file, and the resource server no longer keeps jks.

Custom jwt decoder

spring-security-oauth2-jose is the jose specification dependency of Spring Security. I will implement a custom JWT decoder based on this class library.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
/**
    * 基于Nimbus的jwt解码器,并增加了一些自定义校验策略
    *
    * @param validator the validator
    * @return the jwt decoder
    */
@SneakyThrows
@Bean
public JwtDecoder jwtDecoder(@Qualifier("delegatingTokenValidator") DelegatingOAuth2TokenValidator<Jwt> validator) {
    CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
    // 从classpath路径读取cer公钥证书来配置解码器
    ClassPathResource resource = new ClassPathResource(this.jwtProperties.getCertInfo().getPublicKeyLocation());
    Certificate certificate = certificateFactory.generateCertificate(resource.getInputStream());
    PublicKey publicKey = certificate.getPublicKey();
    NimbusJwtDecoder nimbusJwtDecoder = NimbusJwtDecoder.withPublicKey((RSAPublicKey) publicKey).build();
    nimbusJwtDecoder.setJwtValidator(validator);
    return nimbusJwtDecoder;
}

The above decoder is based on our public key certificate, and I also customized some checksum policies. I have to say that Nimbus’ jwt class library is much better than jjwt.

Custom resource server configuration

Next, configure the resource server.

Core processes and concepts

The resource server is actually configured with a filter BearerTokenAuthenticationFilter to intercept and authenticate Bearer Token, allowing the request if the authentication passes and the permission meets the requirements, and denying the request if it does not.

The difference is that after successful authentication, the credentials are no longer UsernamePasswordAuthenticationToken but JwtAuthenticationToken.

 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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
@Transient
public class JwtAuthenticationToken extends AbstractOAuth2TokenAuthenticationToken<Jwt> {

    private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;

    private final String name;

    /**
     * Constructs a {@code JwtAuthenticationToken} using the provided parameters.
     * @param jwt the JWT
     */
    public JwtAuthenticationToken(Jwt jwt) {
        super(jwt);
        this.name = jwt.getSubject();
    }

    /**
     * Constructs a {@code JwtAuthenticationToken} using the provided parameters.
     * @param jwt the JWT
     * @param authorities the authorities assigned to the JWT
     */
    public JwtAuthenticationToken(Jwt jwt, Collection<? extends GrantedAuthority> authorities) {
        super(jwt, authorities);
        this.setAuthenticated(true);
        this.name = jwt.getSubject();
    }

    /**
     * Constructs a {@code JwtAuthenticationToken} using the provided parameters.
     * @param jwt the JWT
     * @param authorities the authorities assigned to the JWT
     * @param name the principal name
     */
    public JwtAuthenticationToken(Jwt jwt, Collection<? extends GrantedAuthority> authorities, String name) {
        super(jwt, authorities);
        this.setAuthenticated(true);
        this.name = name;
    }

    @Override
    public Map<String, Object> getTokenAttributes() {
        return this.getToken().getClaims();
    }

    /**
     * jwt 中的sub 值  用户名比较合适
     */
    @Override
    public String getName() {
        return this.name;
    }

}

Special care should be taken when making changes, especially when user credential information is obtained from SecurityContext.

Resource manager configuration

Starting from a certain version of Spring Security 5 there is no need to integrate adapter classes anymore, you can configure Spring Security just like this, and the same for the resource manager.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
@Bean
SecurityFilterChain jwtSecurityFilterChain(HttpSecurity http) throws Exception {
    return http.authorizeRequests(request -> request.anyRequest()
                    .access("@checker.check(authentication,request)"))
            .exceptionHandling()
            .accessDeniedHandler(new SimpleAccessDeniedHandler())
            .authenticationEntryPoint(new SimpleAuthenticationEntryPoint())
            .and()
            .oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt)
            .build();
}

Here you just need to declare the resource server that uses JWT checksum, and configure the defined 401 endpoint and 403 processor. Here I added SpEL-based dynamic permission control, which has been covered in the past, so I won’t go over it here.

JWT personalization parsing

Parsing data from JWT Token and generating JwtAuthenticationToken is done by JwtAuthenticationConverter. You can customize this converter to implement some personalized features. For example, if by default the parsed permissions are prefixed with SCOPE_ and the project uses ROLE_, you can use this class to be compatible with the old project.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
     @Bean
    JwtAuthenticationConverter jwtAuthenticationConverter() {
        JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
        JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
//        如果不按照规范  解析权限集合Authorities 就需要自定义key
//        jwtGrantedAuthoritiesConverter.setAuthoritiesClaimName("scopes");
//        OAuth2 默认前缀是 SCOPE_     Spring Security 是 ROLE_
        jwtGrantedAuthoritiesConverter.setAuthorityPrefix("");
        jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(jwtGrantedAuthoritiesConverter);
        // 设置jwt中用户名的key  默认就是sub  你可以自定义
        jwtAuthenticationConverter.setPrincipalClaimName(JwtClaimNames.SUB);
        return jwtAuthenticationConverter;
    }

Here the transformation is basically complete. Your protected resource API will be protected by Bearer Token.

In practice, it is recommended to wrap the resource server as a dependency and integrate it into the service that needs to protect the resource.

Additional Notes

To test the resource server, assume we have an authorization server that issues tokens. Here we simply simulate a token issuing method to obtain a token:

 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
/**
    * 资源服务器不应该生成JWT 但是为了测试 假设这是个认证服务器
    */
@SneakyThrows
@Test
public void imitateAuthServer() {

    JwtEncoder jwsEncoder = new NimbusJwsEncoder(jwkSource());

    JwtTokenGenerator jwtTokenGenerator = new JwtTokenGenerator(jwsEncoder);
    OAuth2AccessTokenResponse oAuth2AccessTokenResponse = jwtTokenGenerator.tokenResponse();

    System.out.println("oAuth2AccessTokenResponse = " + oAuth2AccessTokenResponse.getAccessToken().getTokenValue());
}

    @SneakyThrows
private JWKSource<SecurityContext> jwkSource() {
    ClassPathResource resource = new ClassPathResource("felordcn.jks");
    KeyStore jks = KeyStore.getInstance("jks");
    String pass = "123456";
    char[] pem = pass.toCharArray();
    jks.load(resource.getInputStream(), pem);

    RSAKey rsaKey = RSAKey.load(jks, "felordcn", pem);

    JWKSet jwkSet = new JWKSet(rsaKey);
    return new ImmutableJWKSet<>(jwkSet);
}

Reference