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
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
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With