Реализация авторизации OAuth 2 на Spring Boot

Я разбираюсь с авторизацией OAuth2. Реализовать надо flow clientCredentials. Написал простое приложение, конфигурация которого выглядит так:

application.yml:

server:
  port: 9000

logging:
  level:
    org.springframework.security: trace

spring:
  main:
    allow-bean-definition-overriding: true

Конфигурация сервера авторизации:

package org.aoizora.config;

import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.server.authorization.client.InMemoryRegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;

import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.util.UUID;

@Configuration
@Import(OAuth2AuthorizationServerConfiguration.class)
public class AuthenticationServerConfiguration {
    @Bean
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
        OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);

        return http.formLogin(Customizer.withDefaults()).build();
    }

    @Bean
    public JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) {
        return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
    }

    @Bean
    public JWKSource<SecurityContext> jwkSource() throws NoSuchAlgorithmException {
        RSAKey rsaKey = generateRsa();
        JWKSet jwkSet = new JWKSet(rsaKey);

        return (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);
    }

    private static RSAKey generateRsa() throws NoSuchAlgorithmException {
        KeyPair keyPair = generateRsaKey();
        RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
        RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();

        return new RSAKey.Builder(publicKey)
                .privateKey(privateKey)
                .keyID(UUID.randomUUID().toString())
                .build();
    }

    private static KeyPair generateRsaKey() throws NoSuchAlgorithmException {
        KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
        keyPairGenerator.initialize(2048);

        return keyPairGenerator.generateKeyPair();
    }

    @Bean
    public AuthorizationServerSettings authorizationServerSettings() {
        return AuthorizationServerSettings.builder()
                .build();
    }

    @Bean
    public RegisteredClientRepository registeredClientRepository() {
        RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString())
                .clientId("aoizora")
                .clientSecret("{noop}123456")
                .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_POST)
                .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
                .redirectUri("https://oidcdebugger.com/debug")
                .scope("test")
                .build();

        return new InMemoryRegisteredClientRepository(registeredClient);
    }

    @Bean
    public UserDetailsService users() {
        UserDetails user = User.withDefaultPasswordEncoder()
                .username("admin")
                .password("password")
                .roles("ADMIN")
                .build();

        return new InMemoryUserDetailsManager(user);
    }
}

Конфигурация Spring Security:

package org.aoizora.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
public class SpringSecurityConfiguration {

    @Bean
    SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
        http
                .authorizeHttpRequests(authorizeRequests ->
                        authorizeRequests.anyRequest().authenticated()
                )
                .formLogin(Customizer.withDefaults());

        return http.build();
    }

}

И защищенный контроллер:

package org.aoizora.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController {
    @GetMapping("/")
    public String hello() {
        return "Hello!";
    }
}

Авторизация через форму, расположенную по адресу localhost:9000/login, работает нормально и меня перебрасывает на хелло-контроллер. Но попытка авторизоваться по OAuth через Postman приводит к провалу: введите сюда описание изображения

введите сюда описание изображения

Получаю в постмане ошибку invalid client, а в логах на сервере вижу такое:

> 2024-09-16T16:06:53.788+02:00 TRACE 663946 --- [nio-9000-exec-1]
> o.s.a.w.OAuth2ClientAuthenticationFilter : Client authentication
> failed: [invalid_client] Client authentication failed: client_id
> 
> org.springframework.security.oauth2.core.OAuth2AuthenticationException:
> Client authentication failed: client_id   at
> org.springframework.security.oauth2.server.authorization.authentication.ClientSecretAuthenticationProvider.throwInvalidClient(ClientSecretAuthenticationProvider.java:163)
> ~[spring-security-oauth2-authorization-server-1.2.4.jar:1.2.4]    at
> org.springframework.security.oauth2.server.authorization.authentication.ClientSecretAuthenticationProvider.authenticate(ClientSecretAuthenticationProvider.java:100)
> ~[spring-security-oauth2-authorization-server-1.2.4.jar:1.2.4]    at
> org.springframework.security.authentication.ProviderManager.authenticate(ProviderManager.java:182)
> ~[spring-security-core-6.2.4.jar:6.2.4]   at
> org.springframework.security.oauth2.server.authorization.web.OAuth2ClientAuthenticationFilter.doFilterInternal(OAuth2ClientAuthenticationFilter.java:122)
> ~[spring-security-oauth2-authorization-server-1.2.4.jar:1.2.4]    at
> org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
> ~[spring-web-6.1.6.jar:6.1.6]     at
> org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374)
> ~[spring-security-web-6.2.4.jar:6.2.4]    at
> org.springframework.security.oauth2.server.authorization.web.NimbusJwkSetEndpointFilter.doFilterInternal(NimbusJwkSetEndpointFilter.java:85)
> ~[spring-security-oauth2-authorization-server-1.2.4.jar:1.2.4]    at
> org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
> ~[spring-web-6.1.6.jar:6.1.6]     at
> org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374)
> ~[spring-security-web-6.2.4.jar:6.2.4]    at
> org.springframework.security.oauth2.server.authorization.web.OAuth2DeviceVerificationEndpointFilter.doFilterInternal(OAuth2DeviceVerificationEndpointFilter.java:139)
> ~[spring-security-oauth2-authorization-server-1.2.4.jar:1.2.4]    at
> org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
> ~[spring-web-6.1.6.jar:6.1.6]     at
> org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374)
> ~[spring-security-web-6.2.4.jar:6.2.4]    at
> org.springframework.security.oauth2.server.authorization.web.OAuth2AuthorizationEndpointFilter.doFilterInternal(OAuth2AuthorizationEndpointFilter.java:157)
> ~[spring-security-oauth2-authorization-server-1.2.4.jar:1.2.4]    at
> org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
> ~[spring-web-6.1.6.jar:6.1.6]     at
> org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374)
> ~[spring-security-web-6.2.4.jar:6.2.4]    at
> org.springframework.security.oauth2.server.authorization.web.OAuth2AuthorizationServerMetadataEndpointFilter.doFilterInternal(OAuth2AuthorizationServerMetadataEndpointFilter.java:84)
> ~[spring-security-oauth2-authorization-server-1.2.4.jar:1.2.4]    at
> org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
> ~[spring-web-6.1.6.jar:6.1.6]     at
> org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374)
> ~[spring-security-web-6.2.4.jar:6.2.4]    at
> org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:107)
> ~[spring-security-web-6.2.4.jar:6.2.4]    at
> org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:93)
> ~[spring-security-web-6.2.4.jar:6.2.4]    at
> org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374)
> ~[spring-security-web-6.2.4.jar:6.2.4]    at
> org.springframework.security.web.csrf.CsrfFilter.doFilterInternal(CsrfFilter.java:117)
> ~[spring-security-web-6.2.4.jar:6.2.4]    at
> org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
> ~[spring-web-6.1.6.jar:6.1.6]     at
> org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374)
> ~[spring-security-web-6.2.4.jar:6.2.4]    at
> org.springframework.web.filter.CorsFilter.doFilterInternal(CorsFilter.java:91)
> ~[spring-web-6.1.6.jar:6.1.6]     at
> org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
> ~[spring-web-6.1.6.jar:6.1.6]     at
> org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374)
> ~[spring-security-web-6.2.4.jar:6.2.4]    at
> org.springframework.security.web.header.HeaderWriterFilter.doHeadersAfter(HeaderWriterFilter.java:90)
> ~[spring-security-web-6.2.4.jar:6.2.4]    at
> org.springframework.security.web.header.HeaderWriterFilter.doFilterInternal(HeaderWriterFilter.java:75)
> ~[spring-security-web-6.2.4.jar:6.2.4]    at
> org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
> ~[spring-web-6.1.6.jar:6.1.6]     at
> org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374)
> ~[spring-security-web-6.2.4.jar:6.2.4]    at
> org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.AuthorizationServerContextFilter.doFilterInternal(AuthorizationServerContextFilter.java:61)
> ~[spring-security-oauth2-authorization-server-1.2.4.jar:1.2.4]    at
> org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
> ~[spring-web-6.1.6.jar:6.1.6]     at
> org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374)
> ~[spring-security-web-6.2.4.jar:6.2.4]    at
> org.springframework.security.web.context.SecurityContextHolderFilter.doFilter(SecurityContextHolderFilter.java:82)
> ~[spring-security-web-6.2.4.jar:6.2.4]    at
> org.springframework.security.web.context.SecurityContextHolderFilter.doFilter(SecurityContextHolderFilter.java:69)
> ~[spring-security-web-6.2.4.jar:6.2.4]    at
> org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374)
> ~[spring-security-web-6.2.4.jar:6.2.4]    at
> org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter.doFilterInternal(WebAsyncManagerIntegrationFilter.java:62)
> ~[spring-security-web-6.2.4.jar:6.2.4]    at
> org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
> ~[spring-web-6.1.6.jar:6.1.6]     at
> org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374)
> ~[spring-security-web-6.2.4.jar:6.2.4]    at
> org.springframework.security.web.session.DisableEncodeUrlFilter.doFilterInternal(DisableEncodeUrlFilter.java:42)
> ~[spring-security-web-6.2.4.jar:6.2.4]    at
> org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
> ~[spring-web-6.1.6.jar:6.1.6]     at
> org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374)
> ~[spring-security-web-6.2.4.jar:6.2.4]    at
> org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:233)
> ~[spring-security-web-6.2.4.jar:6.2.4]    at
> org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:191)
> ~[spring-security-web-6.2.4.jar:6.2.4]    at
> org.springframework.web.filter.CompositeFilter$VirtualFilterChain.doFilter(CompositeFilter.java:113)
> ~[spring-web-6.1.6.jar:6.1.6]     at
> org.springframework.web.servlet.handler.HandlerMappingIntrospector.lambda$createCacheFilter$3(HandlerMappingIntrospector.java:195)
> ~[spring-webmvc-6.1.6.jar:6.1.6]  at
> org.springframework.web.filter.CompositeFilter$VirtualFilterChain.doFilter(CompositeFilter.java:113)
> ~[spring-web-6.1.6.jar:6.1.6]     at
> org.springframework.web.filter.CompositeFilter.doFilter(CompositeFilter.java:74)
> ~[spring-web-6.1.6.jar:6.1.6]     at
> org.springframework.security.config.annotation.web.configuration.WebMvcSecurityConfiguration$CompositeFilterChainProxy.doFilter(WebMvcSecurityConfiguration.java:230)
> ~[spring-security-config-6.2.4.jar:6.2.4]     at
> org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:352)
> ~[spring-web-6.1.6.jar:6.1.6]     at
> org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:268)
> ~[spring-web-6.1.6.jar:6.1.6]     at
> org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:175)
> ~[tomcat-embed-core-10.1.20.jar:10.1.20]  at
> org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:150)
> ~[tomcat-embed-core-10.1.20.jar:10.1.20]  at
> org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100)
> ~[spring-web-6.1.6.jar:6.1.6]     at
> org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
> ~[spring-web-6.1.6.jar:6.1.6]     at
> org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:175)
> ~[tomcat-embed-core-10.1.20.jar:10.1.20]  at
> org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:150)
> ~[tomcat-embed-core-10.1.20.jar:10.1.20]  at
> org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93)
> ~[spring-web-6.1.6.jar:6.1.6]     at
> org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
> ~[spring-web-6.1.6.jar:6.1.6]     at
> org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:175)
> ~[tomcat-embed-core-10.1.20.jar:10.1.20]  at
> org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:150)
> ~[tomcat-embed-core-10.1.20.jar:10.1.20]  at
> org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201)
> ~[spring-web-6.1.6.jar:6.1.6]     at
> org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
> ~[spring-web-6.1.6.jar:6.1.6]     at
> org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:175)
> ~[tomcat-embed-core-10.1.20.jar:10.1.20]  at
> org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:150)
> ~[tomcat-embed-core-10.1.20.jar:10.1.20]  at
> org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:167)
> ~[tomcat-embed-core-10.1.20.jar:10.1.20]  at
> org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:90)
> ~[tomcat-embed-core-10.1.20.jar:10.1.20]  at
> org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:482)
> ~[tomcat-embed-core-10.1.20.jar:10.1.20]  at
> org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:115)
> ~[tomcat-embed-core-10.1.20.jar:10.1.20]  at
> org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:93)
> ~[tomcat-embed-core-10.1.20.jar:10.1.20]  at
> org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74)
> ~[tomcat-embed-core-10.1.20.jar:10.1.20]  at
> org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:344)
> ~[tomcat-embed-core-10.1.20.jar:10.1.20]  at
> org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:391)
> ~[tomcat-embed-core-10.1.20.jar:10.1.20]  at
> org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:63)
> ~[tomcat-embed-core-10.1.20.jar:10.1.20]  at
> org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:896)
> ~[tomcat-embed-core-10.1.20.jar:10.1.20]  at
> org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1736)
> ~[tomcat-embed-core-10.1.20.jar:10.1.20]  at
> org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:52)
> ~[tomcat-embed-core-10.1.20.jar:10.1.20]  at
> org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1191)
> ~[tomcat-embed-core-10.1.20.jar:10.1.20]  at
> org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659)
> ~[tomcat-embed-core-10.1.20.jar:10.1.20]  at
> org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:63)
> ~[tomcat-embed-core-10.1.20.jar:10.1.20]  at
> java.base/java.lang.Thread.run(Thread.java:840) ~[na:na]

Что я сделал неправильно в реализации авторизации OAuth? Как делать правильно?

Вот зависимости:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.aoizora</groupId>
    <artifactId>oauth-server</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <spring.boot.version>3.2.5</spring.boot.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <version>${spring.boot.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
            <version>${spring.boot.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-oauth2-authorization-server</artifactId>
            <version>${spring.boot.version}</version>
        </dependency>
    </dependencies>

</project>

Ответы (0 шт):