Реализация авторизации 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>