Customize the OAuth2 Authorization Consent Page
Tags: java, security, oauth2, springauthorizationserver
Published: 2023-02-16 03:08:41

[In the previous article](https://dev.to/relive27/using-jwt-with-spring-security-oauth2-41jp), we have briefly introduced how to build an authorization server. Next, we will continue to introduce how to customize the OAuth2 authorization consent page.

If you can no longer tolerate the ugly default authorization consent page of [Spring Authorization Server](https://spring.io/projects/spring-authorization-server#learn), then you can continue reading this article and gradually create an authorization consent page that satisfies you.

> 💡 Note: If you don’t want to read till the end, you can view the [source code](https://github.com/ReLive27/spring-security-oauth2-sample/tree/main/oauth2-custom-consent-authorizationserver) here.Don’t forget to give a star to the project if you like it!

### OAuth2 authorization server implementation

Start by creating an authorization server.

#### Maven dependencies

```xml
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-security</artifactId>
  <version>2.6.7</version>
</dependency>

<dependency>
  <groupId>org.springframework.security</groupId>
  <artifactId>spring-security-oauth2-authorization-server</artifactId>
  <version>0.3.1</version>
</dependency>

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-web</artifactId>
  <version>2.6.7</version>
</dependency>

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-thymeleaf</artifactId>
  <version>2.6.7</version>
</dependency>

```

#### Configuration

First we configure port 8080 for the authorization server:

```yaml
server:
  port: 8080
```

Then we create an `AuthorizationServerConfig` configuration class, in this class we will create the specific beans required by the OAuth2 authorization server. First specify that we authorize the consent page */oauth2/consent* URI to replace the original default implementation.

```java
@Configuration(proxyBeanMethods = false)
public class AuthorizationServerConfig {
    private static final String CUSTOM_CONSENT_PAGE_URI = "/oauth2/consent";

    @Bean
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
        OAuth2AuthorizationServerConfigurer<HttpSecurity> authorizationServerConfigurer = new OAuth2AuthorizationServerConfigurer<>();
        //define authorization consent page
        authorizationServerConfigurer.authorizationEndpoint(authorizationEndpoint ->
                authorizationEndpoint.consentPage(CUSTOM_CONSENT_PAGE_URI));

        RequestMatcher endpointsMatcher = authorizationServerConfigurer
                .getEndpointsMatcher();

        http.requestMatcher(endpointsMatcher)
                .authorizeRequests(authorizeRequests ->
                        authorizeRequests.anyRequest().authenticated()
                )
                .csrf(csrf -> csrf.ignoringRequestMatchers(endpointsMatcher))
                .apply(authorizationServerConfigurer);
        return http.exceptionHandling(exceptions -> exceptions.
                authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/login"))).build();
    }

  //...
}
```

Next we create an OAuth2 client using the *RegisteredClient* builder type and store it in the cache.

```java
 @Bean
    public RegisteredClientRepository registeredClientRepository() {
        RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString())
                .clientId("relive-client")
                .clientSecret("{noop}relive-client")
                .clientName("ReLive27")
                .clientAuthenticationMethods(s -> {
                    s.add(ClientAuthenticationMethod.CLIENT_SECRET_POST);
                    s.add(ClientAuthenticationMethod.CLIENT_SECRET_BASIC);
                })
                .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
                .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
                .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
                .authorizationGrantType(AuthorizationGrantType.PASSWORD)
                .redirectUri("http://127.0.0.1:8070/login/oauth2/code/messaging-client-authorization-code")
                .scope(OidcScopes.PROFILE)
                .scope("message.read")
                .scope("message.write")
                .clientSettings(ClientSettings.builder()
                        .requireAuthorizationConsent(true)
                        .requireProofKey(false)
                        .build())
                .tokenSettings(TokenSettings.builder()
                        .accessTokenFormat(OAuth2TokenFormat.SELF_CONTAINED)
                        .idTokenSignatureAlgorithm(SignatureAlgorithm.RS256)
                        .accessTokenTimeToLive(Duration.ofSeconds(30 * 60))
                        .refreshTokenTimeToLive(Duration.ofSeconds(60 * 60))
                        .reuseRefreshTokens(true)
                        .build())
                .build();

        return new InMemoryRegisteredClientRepository(registeredClient);
    }
```

The rest of the configuration will not be described in detail, you can refer to the previous [Using JWT with Spring Security OAuth2] (https://dev.to/relive27/using-jwt-with-spring-security-oauth2-41jp) article.

<br />

Next we will create an authorization page controller and pass the required parameters to the *Model*:

```java
@Controller
@RequiredArgsConstructor
public class AuthorizationConsentController {
    private final RegisteredClientRepository registeredClientRepository;

    @GetMapping(value = "/oauth2/consent")
    public String consent(Principal principal, Model model,
                          @RequestParam(OAuth2ParameterNames.CLIENT_ID) String clientId,
                          @RequestParam(OAuth2ParameterNames.SCOPE) String scope,
                          @RequestParam(OAuth2ParameterNames.STATE) String state) {

        Set<String> scopesToApprove = new LinkedHashSet<>();
        RegisteredClient registeredClient = this.registeredClientRepository.findByClientId(clientId);
        Set<String> scopes = registeredClient.getScopes();
        for (String requestedScope : StringUtils.delimitedListToStringArray(scope, " ")) {
            if (scopes.contains(requestedScope)) {
                scopesToApprove.add(requestedScope);
            }
        }

        model.addAttribute("clientId", clientId);
        model.addAttribute("clientName", registeredClient.getClientName());
        model.addAttribute("state", state);
        model.addAttribute("scopes", withDescription(scopesToApprove));
        model.addAttribute("principalName", principal.getName());
        model.addAttribute("redirectUri", registeredClient.getRedirectUris().iterator().next());

        return "consent";
    }

    private static Set<ScopeWithDescription> withDescription(Set<String> scopes) {
        Set<ScopeWithDescription> scopeWithDescriptions = new LinkedHashSet<>();
        for (String scope : scopes) {
            scopeWithDescriptions.add(new ScopeWithDescription(scope));

        }
        return scopeWithDescriptions;
    }

    public static class ScopeWithDescription {
        private static final String DEFAULT_DESCRIPTION = "我们无法提供有关此权限的信息";
        private static final Map<String, String> scopeDescriptions = new HashMap<>();

        static {
            scopeDescriptions.put(
                    "profile",
                    "验证您的身份"
            );
            scopeDescriptions.put(
                    "message.read",
                    "了解您可以访问哪些权限"
            );
            scopeDescriptions.put(
                    "message.write",
                    "代表您行事"
            );
        }

        public final String scope;
        public final String description;

        ScopeWithDescription(String scope) {
            this.scope = scope;
            this.description = scopeDescriptions.getOrDefault(scope, DEFAULT_DESCRIPTION);
        }
    }
}

```

Then let's define the html page, here we use the [thymeleaf](https://www.thymeleaf.org/) template engine:

```html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css"
          integrity="sha384-JcKb8q3iqJ61gNV9KGb8thSsNjpSL0n8PARn9HuZOnIxN0hoP+VmmDGMN5t9UJ0Z" crossorigin="anonymous">
    <title>Custom consent page - Consent required</title>
    <style>
        body {
            background-color: #f6f8fa;
        }

        #submit-consent {
            width: 45%;
            float: right;
            height: 40px;
            font-size: 18px;
            border-color: #cccccc;
            margin-right: 3%;
        }

        #cancel-consent {
            width: 45%;
            height: 40px;
            font-size: 18px;
            color: black;
            background-color: #cccccc;
            border-color: #cccccc;
            float: left;
            margin-left: 3%;
        }
    </style>
    <script>
        function cancelConsent() {
            document.consent_form.reset();
            document.consent_form.submit();
        }
    </script>
</head>
<body>
<div style="width: 500px;height: 600px;margin: 100px auto">
    <h5 style="text-align: center"><b th:text="${clientName}"></b>希望获得以下许可：</h5>
    <div style="width: 100%;height: 500px;border: #cccccc 1px solid;border-radius: 10px">
        <form name="consent_form" method="post" action="/oauth2/authorize">
            <input type="hidden" name="client_id" th:value="${clientId}">
            <input type="hidden" name="state" th:value="${state}">

            <div th:each="scope: ${scopes}" class="form-group form-check py-1" style="margin-left: 5%">
                <input class="form-check-input"
                       type="checkbox"
                       name="scope"
                       th:value="${scope.scope}"
                       th:id="${scope.scope}"
                       checked>
                <label class="form-check-label font-weight-bold" th:for="${scope.scope}"
                       th:text="${scope.scope}=='profile'?(${scope.description}+'('+${principalName}+')'):${scope.description}"></label>
            </div>

            <hr style="width: 90%">
            <p style="margin-left: 5%"><b th:text="${clientName}"></b>尚未安装在您有权访问的任何账户上。</p>
            <hr style="width: 90%">
            <div class="form-group pt-3" style="width: 100%;height: 80px;">
                <button class="btn btn-primary btn-lg" type="submit" id="submit-consent">
                    授权同意
                </button>
                <button class="btn btn-primary btn-lg" type="button" id="cancel-consent" onclick="cancelConsent();">
                    取消
                </button>
            </div>
            <div style="margin-top: 5px;width: 100%;height: 50px">
                <p style="text-align: center;font-size: 14px">授权将重定向到</p>
                <p style="text-align: center;font-size: 14px"><b th:text="${redirectUri}"></b></p>
            </div>
        </form>
    </div>
</div>
</body>
</html>

```

#### Access authorization page

After starting the service, we will initiate an authorization request, [http://localhost:8080/oauth2/authorize?response_type=code&client_id=relive-client&scope=message.write%20message.read%20profile&state=some-state&redirect_uri=http:// 127.0.0.1:8070/login/oauth2/code/messaging-client-authorization-code](http://localhost:8080/oauth2/authorize?response_type=code&client_id=relive-client&scope=message.write%20message.read%20profile&state =some-state&redirect_uri=http://127.0.0.1:8070/login/oauth2/code/messaging-client-authorization-code), after successful authentication, we can see the following authorized consent page we defined:


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3iwt6xti0g4krea38x28.png)


### Conclusion

As always, the source code used in this article is available [on GitHub](https://github.com/ReLive27/spring-security-oauth2-sample/tree/main/oauth2-custom-consent-authorizationserver).


You might want to read on to the next one:
- [Spring Security Persistent OAuth2 Client](https://dev.to/relive27/spring-security-persistent-oauth2-client-1gcc)
- [Spring Security OAuth2 Client Credentials Grant](https://dev.to/relive27/spring-security-oauth2-client-credentials-grant-fj5)
- [Authorization Code Flow with Proof Key for Code Exchange (PKCE)](https://dev.to/relive27/authorization-code-flow-with-proof-key-for-code-exchange-pkce-4h22)
- [Spring Security OAuth2 Login](https://dev.to/relive27/spring-security-oauth2-login-51lj)

Thanks for reading!