Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Generate nonce in an Spring Security application using OpenID connect

i'm plugging a Spring security application to an IDP/OP (IDentity Provider, or Openid connect Identity Provider according to the OpenID connect terminology)

I'm using the authorization code flow. I used this implementation to start my code : https://github.com/gazbert/openid-connect-spring-client

It's working with several IDP, until i found one that requires the nonce parameter. However i could not managed to configure my application to generate a nonce, and add it in the url (I know that's the nonce because when i add it manually : it works)

It's when the application redirect the user to the IDP (authorization endpoint) that i wish to have a nonce. And it would be perfect if the nonce could be verified on the return.

I searched the web for 2 hours, i found this may be the thing to use org.springframework.security.oauth.provider.nonce but didn't found any example, or clue on how to add it in my code

Here is the interesting part of the code where i think i have to tell Spring to use the nonce :

   public OAuth2RestTemplate getOpenIdConnectRestTemplate(@Qualifier("oauth2ClientContext")
                                                                         OAuth2ClientContext clientContext) {
        return new OAuth2RestTemplate(createOpenIdConnectCodeConfig(), clientContext);

    }



    public OAuth2ProtectedResourceDetails createOpenIdConnectCodeConfig() {
        final AuthorizationCodeResourceDetails resourceDetails = new AuthorizationCodeResourceDetails();
        resourceDetails.setClientAuthenticationScheme(AuthenticationScheme.form); // include client credentials in POST Content
        resourceDetails.setClientId(clientId);
        resourceDetails.setClientSecret(clientSecret);
        resourceDetails.setUserAuthorizationUri(authorizationUri);
        resourceDetails.setAccessTokenUri(tokenUri);

        final List<String> scopes = new ArrayList<>();
        scopes.add("openid"); // always need this
        scopes.addAll(Arrays.asList(optionalScopes.split(",")));
        resourceDetails.setScope(scopes);

        resourceDetails.setPreEstablishedRedirectUri(redirectUri);
        resourceDetails.setUseCurrentUri(false);
        return resourceDetails;
    }

If there is a modification i believe it's there. If that's a duplicate i apologies, and i'll never shame myself again.

Any help would be appreciated, i can post more details if needed, i didn't want to confuse by posting too much

Thanks for reading me

like image 694
triton oidc Avatar asked Sep 18 '25 04:09

triton oidc


1 Answers

I struggled with this as well. Fortunately, there is some recent developments in Spring Security documentation, and after some back and forth with one of the GitHub developers, I came up with a solution in Kotlin (translating to Java should be fairly easy). The original discussion can be found here.

Ultimately, my SecurityConfig class ended up looking like this:

@EnableWebSecurity
class SecurityConfig @Autowired constructor(loginGovConfiguration: LoginGovConfiguration) : WebSecurityConfigurerAdapter() {

    @Autowired
    lateinit var clientRegistrationRepository: ClientRegistrationRepository

    private final val keystore: MutableMap<String, String?> = loginGovConfiguration.keystore
    private final val keystoreUtil: KeystoreUtil = KeystoreUtil(
            keyStore = keystore["file"],
            keyStorePassword = keystore["password"],
            keyAlias = keystore["alias"],
            keyPassword = null,
            keyStoreType = keystore["type"]
    )
    private final val allowedOrigin: String = loginGovConfiguration.allowedOrigin

    companion object {
        const val LOGIN_ENDPOINT = DefaultLoginPageGeneratingFilter.DEFAULT_LOGIN_PAGE_URL
        const val LOGIN_SUCCESS_ENDPOINT = "/login_success"
        const val LOGIN_FAILURE_ENDPOINT = "/login_failure"
        const val LOGIN_PROFILE_ENDPOINT = "/login_profile"
        const val LOGOUT_ENDPOINT = "/logout"
        const val LOGOUT_SUCCESS_ENDPOINT = "/logout_success"
    }

    override fun configure(http: HttpSecurity) {
        http.authorizeRequests()
            // login, login failure, and index are allowed by anyone
            .antMatchers(
                    LOGIN_ENDPOINT,
                    LOGIN_SUCCESS_ENDPOINT,
                    LOGIN_PROFILE_ENDPOINT,
                    LOGIN_FAILURE_ENDPOINT,
                    LOGOUT_ENDPOINT,
                    LOGOUT_SUCCESS_ENDPOINT,
                    "/"
            )
                .permitAll()
            // any other requests are allowed by an authenticated user
            .anyRequest()
                .authenticated()
            .and()
            // custom logout behavior
            .logout()
                .logoutRequestMatcher(AntPathRequestMatcher(LOGOUT_ENDPOINT))
                .logoutSuccessUrl(LOGOUT_SUCCESS_ENDPOINT)
                .deleteCookies("JSESSIONID")
                .invalidateHttpSession(true)
                .logoutSuccessHandler(LoginGovLogoutSuccessHandler())
            .and()
            // configure authentication support using an OAuth 2.0 and/or OpenID Connect 1.0 Provider
            .oauth2Login()
                .authorizationEndpoint()
                .authorizationRequestResolver(LoginGovAuthorizationRequestResolver(clientRegistrationRepository))
                .authorizationRequestRepository(authorizationRequestRepository())
                .and()
                .tokenEndpoint()
                .accessTokenResponseClient(accessTokenResponseClient())
                .and()
                .failureUrl(LOGIN_FAILURE_ENDPOINT)
                .successHandler(LoginGovAuthenticationSuccessHandler())
    }

    @Bean
    fun corsFilter(): CorsFilter {
        // fix OPTIONS preflight login profile request failure with 403 Invalid CORS request
        val config = CorsConfiguration()
        config.addAllowedOrigin(allowedOrigin)
        config.allowCredentials = true
        config.allowedHeaders = listOf("x-auth-token", "Authorization", "cache", "Content-Type")
        config.addAllowedMethod(HttpMethod.OPTIONS)
        config.addAllowedMethod(HttpMethod.GET)

        val source = UrlBasedCorsConfigurationSource()
        source.registerCorsConfiguration(LOGIN_PROFILE_ENDPOINT, config)

        return CorsFilter(source)
    }

    @Bean
    fun authorizationRequestRepository(): AuthorizationRequestRepository<OAuth2AuthorizationRequest> {
        return HttpSessionOAuth2AuthorizationRequestRepository()
    }

    @Bean
    fun accessTokenResponseClient(): OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> {
        val accessTokenResponseClient = DefaultAuthorizationCodeTokenResponseClient()
        accessTokenResponseClient.setRequestEntityConverter(LoginGovTokenRequestConverter(clientRegistrationRepository, keystoreUtil))
        return accessTokenResponseClient
    }
}

And my custom authorization resolver LoginGovAuthorizationRequestResolver:

class LoginGovAuthorizationRequestResolver(clientRegistrationRepository: ClientRegistrationRepository) : OAuth2AuthorizationRequestResolver {

    private val REGISTRATION_ID_URI_VARIABLE_NAME = "registrationId"
    private var defaultAuthorizationRequestResolver: OAuth2AuthorizationRequestResolver = DefaultOAuth2AuthorizationRequestResolver(
            clientRegistrationRepository, OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI
    )
    private val authorizationRequestMatcher: AntPathRequestMatcher = AntPathRequestMatcher(
            OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI + "/{" + REGISTRATION_ID_URI_VARIABLE_NAME + "}")

    override fun resolve(request: HttpServletRequest?): OAuth2AuthorizationRequest? {
        val authorizationRequest: OAuth2AuthorizationRequest? = defaultAuthorizationRequestResolver.resolve(request)
        return if(authorizationRequest == null)
        { null } else { customAuthorizationRequest(authorizationRequest) }
    }

    override fun resolve(request: HttpServletRequest?, clientRegistrationId: String?): OAuth2AuthorizationRequest? {
        val authorizationRequest: OAuth2AuthorizationRequest? = defaultAuthorizationRequestResolver.resolve(request, clientRegistrationId)
        return if(authorizationRequest == null)
        { null } else { customAuthorizationRequest(authorizationRequest) }
    }

    private fun customAuthorizationRequest(authorizationRequest: OAuth2AuthorizationRequest?): OAuth2AuthorizationRequest {

        val registrationId: String = this.resolveRegistrationId(authorizationRequest)
        val additionalParameters = LinkedHashMap(authorizationRequest?.additionalParameters)

        // set login.gov specific params
        // https://developers.login.gov/oidc/#authorization
        if(registrationId == LOGIN_GOV_REGISTRATION_ID) {
            additionalParameters["nonce"] = "1234567890" // generate your nonce here (should actually include per-session state and be unguessable)
            // add other custom params...
        }

        return OAuth2AuthorizationRequest
            .from(authorizationRequest)
            .additionalParameters(additionalParameters)
            .build()
    }

    private fun resolveRegistrationId(authorizationRequest: OAuth2AuthorizationRequest?): String {
        return authorizationRequest!!.additionalParameters[OAuth2ParameterNames.REGISTRATION_ID] as String
    }

}
like image 104
forgo Avatar answered Sep 22 '25 10:09

forgo