From c471503ede9aad91193ff6f93966196e6aff15d6 Mon Sep 17 00:00:00 2001 From: Vitaly Takmazov Date: Wed, 4 Jan 2023 03:38:19 +0300 Subject: OAuth authentication for Mastodon and ActivityPub C2S --- src/main/java/com/juick/config/SecurityConfig.java | 104 +++++++++++++++++---- 1 file changed, 84 insertions(+), 20 deletions(-) (limited to 'src/main/java/com/juick/config') diff --git a/src/main/java/com/juick/config/SecurityConfig.java b/src/main/java/com/juick/config/SecurityConfig.java index d2030a62..d60abe00 100644 --- a/src/main/java/com/juick/config/SecurityConfig.java +++ b/src/main/java/com/juick/config/SecurityConfig.java @@ -25,20 +25,37 @@ import com.juick.service.security.HTTPSignatureAuthenticationFilter; import com.juick.service.security.HashParamAuthenticationFilter; import com.juick.service.security.JuickUserDetailsService; import com.juick.service.security.entities.JuickUser; +import com.nimbusds.jose.jwk.JWKSet; +import com.nimbusds.jose.jwk.RSAKey; +import com.nimbusds.jose.jwk.source.ImmutableJWKSet; +import com.nimbusds.jose.jwk.source.JWKSource; +import com.nimbusds.jose.proc.SecurityContext; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; import org.springframework.http.HttpMethod; +import org.springframework.jdbc.core.JdbcTemplate; +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.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.oauth2.jwt.JwtDecoder; +import org.springframework.security.oauth2.server.authorization.client.JdbcRegisteredClientRepository; +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.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer; +import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings; import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.security.web.RedirectStrategy; import org.springframework.security.web.SecurityFilterChain; -import org.springframework.security.web.authentication.AuthenticationSuccessHandler; -import org.springframework.security.web.authentication.RememberMeServices; -import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler; +import org.springframework.security.web.authentication.*; import org.springframework.security.web.authentication.rememberme.TokenBasedRememberMeServices; import org.springframework.security.web.authentication.www.BasicAuthenticationEntryPoint; import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; @@ -48,8 +65,12 @@ import org.springframework.web.cors.CorsConfigurationSource; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import javax.inject.Inject; +import java.io.IOException; +import java.security.interfaces.RSAPrivateKey; +import java.security.interfaces.RSAPublicKey; import java.util.Arrays; import java.util.Collections; +import java.util.UUID; /** * Created by aalexeev on 11/21/16. @@ -61,7 +82,8 @@ public class SecurityConfig { private UserService userService; @Inject private KeystoreManager keystoreManager; - + @Inject + private JdbcTemplate jdbcTemplate; private static final String COOKIE_NAME = "juick-remember-me"; @Bean UserDetailsService userDetailsService() { @@ -77,6 +99,7 @@ public class SecurityConfig { configuration.setAllowedHeaders(Collections.singletonList("*")); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/oauth/**", configuration); source.registerCorsConfiguration("/api/**", configuration); source.registerCorsConfiguration("/u/**", configuration); source.registerCorsConfiguration("/n/**", configuration); @@ -124,14 +147,60 @@ public class SecurityConfig { services.setUseSecureCookie(false); // TODO set true if https is supports return services; } + @Bean + @Order(Ordered.HIGHEST_PRECEDENCE) + public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) + throws Exception { + OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http); + http.getConfigurer(OAuth2AuthorizationServerConfigurer.class) + .authorizationServerSettings(AuthorizationServerSettings.builder() + .authorizationEndpoint("/oauth/authorize") + .tokenEndpoint("/oauth/token") + .build()) + .oidc(Customizer.withDefaults()); // Enable OpenID Connect 1.0 + http + // Redirect to the login page when not authenticated from the + // authorization endpoint + .exceptionHandling((exceptions) -> exceptions + .authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/login")) + ) + // Accept access tokens for User Info and/or Client Registration + .oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt); + + return http.formLogin(Customizer.withDefaults()).build(); + } + @Bean + public RegisteredClientRepository registeredClientRepository() { + return new JdbcRegisteredClientRepository(jdbcTemplate); + } + + @Bean + public JWKSource jwkSource() { + RSAPublicKey publicKey = (RSAPublicKey) keystoreManager.getPublicKey(); + RSAPrivateKey privateKey = (RSAPrivateKey) keystoreManager.getPrivateKey(); + RSAKey rsaKey = new RSAKey.Builder(publicKey) + .privateKey(privateKey) + .keyID(UUID.randomUUID().toString()) + .build(); + JWKSet jwkSet = new JWKSet(rsaKey); + return new ImmutableJWKSet<>(jwkSet); + } + @Bean + public JwtDecoder jwtDecoder(JWKSource jwkSource) { + return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource); + } + @Bean + public AuthorizationServerSettings authorizationServerSettings() { + return AuthorizationServerSettings.builder().build(); + } @Bean + @Order(2) SecurityFilterChain apiChain(HttpSecurity http) throws Exception { http.securityMatcher("/api/**") .addFilterBefore(apiAuthenticationFilter(), BasicAuthenticationFilter.class) .addFilterBefore(new HTTPSignatureAuthenticationFilter(signatureManager, userService), BasicAuthenticationFilter.class) - .addFilterBefore(bearerTokenAuthenticationFilter(), BasicAuthenticationFilter.class) .authorizeHttpRequests(requests -> requests .requestMatchers(HttpMethod.OPTIONS).permitAll() .requestMatchers("/api/", "/api/messages", "/api/avatar", @@ -142,15 +211,16 @@ public class SecurityConfig { "/api/_vklogin", "/api/_tglogin", "/api/_google", "/api/_applelogin", "/api/signup", "/api/inbox", "/api/events", "/api/u/", - "/api/info/**", - "/api/nodeinfo/2.0") + "/api/info/**", "/api/v1/apps", "/api/v1/instance", + "/api/nodeinfo/2.0", "/oauth/**") .permitAll() - .anyRequest().hasRole("USER")) + .anyRequest().hasAnyAuthority("SCOPE_write", "ROLE_USER")) .anonymous(anonymous -> anonymous.principal(JuickUser.ANONYMOUS_USER) .authorities(JuickUser.ANONYMOUS_AUTHORITY)) .httpBasic(httpBasic -> httpBasic .authenticationEntryPoint(apiAuthenticationEntryPoint())) .cors(cors -> cors.configurationSource(corsConfigurationSource())) + .oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt) .sessionManagement(sessionManagement -> sessionManagement .sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .exceptionHandling(exceptionHandling -> exceptionHandling @@ -159,14 +229,6 @@ public class SecurityConfig { .headers().defaultsDisabled().cacheControl(); return http.build(); } - - @Bean - AuthenticationSuccessHandler successHandler() { - SimpleUrlAuthenticationSuccessHandler handler = new SimpleUrlAuthenticationSuccessHandler(); - handler.setUseReferer(true); - return handler; - } - @Bean SecurityFilterChain h2ConsoFilterChain(HttpSecurity http) throws Exception { http.securityMatcher("/h2-console/**") @@ -182,7 +244,12 @@ public class SecurityConfig { .headers().defaultsDisabled().cacheControl(); return http.build(); } - + @Bean + AuthenticationSuccessHandler successHandler() { + var handler = new SavedRequestAwareAuthenticationSuccessHandler(); + handler.setUseReferer(true); + return handler; + } @Bean SecurityFilterChain wwwChain(HttpSecurity http) throws Exception { http.addFilterBefore(wwwAuthenticationFilter(), BasicAuthenticationFilter.class) @@ -197,9 +264,6 @@ public class SecurityConfig { .authorities(JuickUser.ANONYMOUS_AUTHORITY)) .cors(cors -> cors .configurationSource(corsConfigurationSource())) - .sessionManagement( - sessionManagement -> sessionManagement - .sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .logout(logout -> logout .logoutRequestMatcher(new AntPathRequestMatcher("/logout")) .invalidateHttpSession(true) -- cgit v1.2.3