spring-projects / spring-authorization-server

Spring Authorization Server
https://spring.io/projects/spring-authorization-server
Apache License 2.0
4.78k stars 1.25k forks source link

allow /authorize without being authenticated first #1612

Closed xenoterracide closed 1 month ago

xenoterracide commented 1 month ago

According to my research doing the pkce authorization code flow the authorize endpoint is called first, then you login, then you get your token. Some providers such as microsoft allow you to combine /authorize and /login, but it seems that authorization server requires that you either login first, or combine them.

I'm going to reference other vendor documentation here

The reason I believe what I'm saying is true is because my code is terminating at this method which does not allow for unauthenticated or anonymous users. However, my understanding of the flow at this point is that user authentication should be optional here.

https://github.com/spring-projects/spring-authorization-server/blob/af5284974a699d1acff775a2f8b1f6a3b474c71d/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/authentication/OAuth2AuthorizationCodeRequestAuthenticationProvider.java#L156

I'm making this feature request since my goal is to mimic Auth0 so that my CI doesn't need the internet, and that I could also develop locally without the internet, or attachment to a single service provider.

my code at this time

// © Copyright 2024 Caleb Cushing
// SPDX-License-Identifier: AGPL-3.0-or-later

package com.xenoterracide.test.authorization.server;

import com.xenoterracide.tools.java.annotation.ExcludeFromGeneratedCoverageReport;
import java.util.UUID;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.core.annotation.Order;
import org.springframework.http.MediaType;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.core.oidc.OidcScopes;
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.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer;
import org.springframework.security.oauth2.server.authorization.settings.ClientSettings;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
import org.springframework.security.web.util.matcher.MediaTypeRequestMatcher;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

/**
 * Test Authorization Server to mimick Auth0.
 */
@SpringBootApplication(proxyBeanMethods = false)
public class AuthorizationServer {

  AuthorizationServer() {}

  /**
   * Main.
   *
   * @param args arguments to the program
   */
  @ExcludeFromGeneratedCoverageReport
  public static void main(String[] args) {
    SpringApplication.run(AuthorizationServer.class, args);
  }

  @Bean
  @Order(1)
  public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
    OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
    http.getConfigurer(OAuth2AuthorizationServerConfigurer.class).oidc(Customizer.withDefaults()); // Enable OpenID Connect 1.0
    http
      // Redirect to the login page when not authenticated from the
      // authorization endpoint
      .exceptionHandling(
        exceptions ->
          exceptions.defaultAuthenticationEntryPointFor(
            new LoginUrlAuthenticationEntryPoint("/login"),
            new MediaTypeRequestMatcher(MediaType.TEXT_HTML)
          )
      )
      // Accept access tokens for User Info and/or Client Registration
      .oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()));

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

  @Bean
  @Order(2)
  public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
    http
      .authorizeHttpRequests(authorize -> authorize.requestMatchers("/authorize").permitAll())
      .authorizeHttpRequests(authorize -> authorize.anyRequest().authenticated())
      // Form login handles the redirect to the login page from the
      // authorization server filter chain
      .formLogin(Customizer.withDefaults());

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

  @Bean
  public CorsConfigurationSource corsConfigurationSource() {
    UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    CorsConfiguration config = new CorsConfiguration();
    config.addAllowedHeader("*");
    config.addAllowedMethod("*");
    config.addAllowedOrigin("http://localhost:3000");
    config.setAllowCredentials(true);
    source.registerCorsConfiguration("/**", config);
    return source;
  }

  @Bean
  RegisteredClientRepository registeredClientRepository() {
    var publicClient = RegisteredClient.withId(UUID.randomUUID().toString())
      .clientId("client")
      .clientAuthenticationMethod(ClientAuthenticationMethod.NONE)
      .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
      .redirectUri("http://localhost:3000")
      .scope(OidcScopes.OPENID)
      .scope(OidcScopes.PROFILE)
      .scope(OidcScopes.EMAIL)
      .clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).requireProofKey(true).build())
      .build();

    return new InMemoryRegisteredClientRepository(publicClient);
  }
}
DEBUG 2032305 - o.apac.hc.clie.http.wire                                     : http-outgoing-0 >> "GET /authorize?client_id=client&scope=openid+profile+email&redirect_uri=http://localhost:3000&audience=http://localhost&response_type=code&response_mode=query&state=sUmww5GH&nonce=FVO5cA3&code_challenge=g0bA5&code_challenge_method=S256&auth0Client=eyJuY HTTP/1.1[\r][\n]"
DEBUG 2032305 - o.apac.hc.clie.http.wire                                     : http-outgoing-0 >> "Accept-Encoding: gzip, x-gzip, deflate[\r][\n]"
DEBUG 2032305 - o.apac.hc.clie.http.wire                                     : http-outgoing-0 >> "Host: localhost:41517[\r][\n]"
DEBUG 2032305 - o.apac.hc.clie.http.wire                                     : http-outgoing-0 >> "Connection: keep-alive[\r][\n]"
DEBUG 2032305 - o.apac.hc.clie.http.wire                                     : http-outgoing-0 >> "User-Agent: Apache-HttpClient/5.2.3 (Java/21.0.2)[\r][\n]"
DEBUG 2032305 - o.apac.hc.clie.http.wire                                     : http-outgoing-0 >> "[\r][\n]"
org.springframework.security:spring-security-oauth2-authorization-server:1.2.4=compileClasspath,runtimeClasspath,testCompileClasspath,testFixturesRuntimeClasspath,testRuntimeClasspath
xenoterracide commented 1 month ago

closing, perhaps temporarily, looks like I may have been bit by the gradle...