DEV Community

ReLive27
ReLive27

Posted on • Edited on

Customize the OAuth2 Authorization Consent Page

In the previous article, 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, 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 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

<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>

Enter fullscreen mode Exit fullscreen mode

Configuration

First we configure port 8080 for the authorization server:

server:
  port: 8080
Enter fullscreen mode Exit fullscreen mode

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.

@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();
    }

  //...
}
Enter fullscreen mode Exit fullscreen mode

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

 @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);
    }
Enter fullscreen mode Exit fullscreen mode

The rest of the configuration will not be described in detail, you can refer to the previous Using JWT with Spring Security OAuth2 article.

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

@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);
        }
    }
}

Enter fullscreen mode Exit fullscreen mode

Then let's define the html page, here we use the thymeleaf template engine:

<!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>

Enter fullscreen mode Exit fullscreen mode

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, after successful authentication, we can see the following authorized consent page we defined:

Image description

Conclusion

As always, the source code used in this article is available on GitHub.

You might want to read on to the next one:

Thanks for reading!

Top comments (0)