Search code examples
springkotlinservletsspring-securityspring-security-oauth2

Spring Security Authorization Server 0.2.0 with kotlin gives WhiteLabel Error Page for Authorization Code Flow


I'm trying to implement the official Authorization Server template (https://github.com/spring-projects/spring-authorization-server/tree/main/samples/default-authorizationserver) using kotlin.

The authentication of the users in memory works very fine but when I try to use the Authorization Code Flow I'm receiving an annoying Whitelabel Error Page: enter image description here

The code I'm implementing is available at https://github.com/RichardSobreiro/kotlin-spring-security-5-simple

The process to reproduce are the following:

Make a GET request using the browser: http://localhost:9000/authorize?response_type=code&scope=openid&client_id=yourClientId&state=STATE&redirect_uri=http://127.0.0.1:8080/login/oauth2/code/messaging-client-oidc

You will be redirected for the login page. After entering the credentials username as "pele" and password as "123456" the 404 error appears.

I've already checked the packages hierarchy of my project in order to avoid component scan problems and also entered the following entry in my etc/host file [127.0.0.1 auth-server] but nothing helped me to solve my problem.

Here is my AuthorizationServerConfig.kt class:

package br.com.cbauthserver.authserverconfig

import br.com.cbauthserver.jwks.KeyGeneratorUtils
import com.nimbusds.jose.jwk.JWKSet
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.core.Ordered
import org.springframework.core.annotation.Order
import org.springframework.security.config.Customizer
import org.springframework.security.config.Customizer.withDefaults
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration
import org.springframework.security.config.annotation.web.configurers.FormLoginConfigurer
import org.springframework.security.core.userdetails.User
import org.springframework.security.core.userdetails.UserDetailsService
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
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.jwt.JwtDecoder
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.ProviderSettings
import org.springframework.security.oauth2.server.authorization.config.TokenSettings
import org.springframework.security.provisioning.InMemoryUserDetailsManager
import org.springframework.security.web.SecurityFilterChain
import java.time.Duration
import java.util.*

import org.springframework.security.oauth2.server.authorization.client.InMemoryRegisteredClientRepository

@Configuration(proxyBeanMethods = false)
class AuthorizationServerConfig(
) {
    private val issuerUrl = "http://auth-server:9000"
    private val yourClientId = "yourClientId"
    private val yourSecret = "yourSecret"

    @Bean
    @Order(Ordered.HIGHEST_PRECEDENCE)
    fun authorizationServerSecurityFilterChain(http: HttpSecurity): SecurityFilterChain {
        OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
        return http.formLogin(Customizer.withDefaults()).build();

        return http.build()
    }

    @Bean
    fun registeredClientRepository (): RegisteredClientRepository  {
        val registeredClient: RegisteredClient = RegisteredClient.withId(UUID.randomUUID().toString())
            .clientId(yourClientId)
            .clientSecret(yourSecret)
            .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
            .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
            .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
            .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
            .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_POST)
            .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_JWT)
            .redirectUri("http://127.0.0.1:8080/login/oauth2/code/messaging-client-oidc")
            .scope(OidcScopes.OPENID)
            //.clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build())
            .build()

        return InMemoryRegisteredClientRepository(listOf(registeredClient))
    }

    @Bean
    fun jwkSource(): JWKSource<SecurityContext> {
        val rsaKey = KeyGeneratorUtils.generateRSAKey()
        val jwkSet = JWKSet(rsaKey)
        return JWKSource<SecurityContext> { jwkSelector, _ -> jwkSelector.select(jwkSet) }
    }

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

    @Bean
    fun providerSettings(): ProviderSettings {
        return ProviderSettings.builder().issuer(issuerUrl).build()
    }

    @Bean
    fun passwordEncoder() = BCryptPasswordEncoder(10)

    @Bean
    fun users(): UserDetailsService {
        val user = User.builder()
            .username("pele")
            .password("\$2a\$10\$b.Rm.8NeuT7hS3Qwy1RPGuuHNMzjEk01vM7ExvW/h11KAHainYBfK")
            //.password("123456")
            .roles("USER")
            .build()
        val admin = User.builder()
            .username("garrincha")
            .password("\$2a\$10\$b.Rm.8NeuT7hS3Qwy1RPGuuHNMzjEk01vM7ExvW/h11KAHainYBfK")
            //.password("123456")
            .roles("USER", "ADMIN")
            .build()
        return InMemoryUserDetailsManager(user, admin)
    }

}

Could someone lend me some help?


Solution

  • You are mixing password encodings without providing a PasswordEncoder that can handle multiple encodings.

    You have defined a BCryptPasswordEncoder bean, which will replace the default password encoder

    @Bean
    fun passwordEncoder() = BCryptPasswordEncoder(10)
    

    However, when the RegisteredClient is created, it uses a plaintext password

    val registeredClient: RegisteredClient = RegisteredClient.withId(UUID.randomUUID().toString())
            .clientId("yourClientId")
            .clientSecret("yourSecret")
    

    One solution is to encode the clientSecret

    val registeredClient: RegisteredClient = RegisteredClient.withId(UUID.randomUUID().toString())
            .clientId("yourClientId")
            .clientSecret(passwordEncoder.encode("yourSecret"))
    

    Another option is to use a DelegatingPasswordEncoder if you need to store your passwords / secrets using different encodings.