Generate nonce in an Spring Security application using OpenID connect - spring-security

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
}
}

Related

CORS exception with Spring Data Rest + Spring Security when posting via '/login' path but other paths are fine

Spring Boot 2.7.0
I faced CORS exception that triggered XMLHttpRequestError when trying to post via the '/login' path, other paths such as '/api/v1/members' are fine.
I followed the below guide,
https://github.com/amigoscode/spring-boot-security-course/tree/8-JTW
I am currently using SpringDataRest with configuration in RepositoryRestConfigurer as follow
#Component
class AppRepositoryRestConfigurer : RepositoryRestConfigurer {
override fun configureRepositoryRestConfiguration(config: RepositoryRestConfiguration?, cors: CorsRegistry?) {
cors?.addMapping("/**")?.allowedOriginPatterns("http://localhost:[*]")
}
}
Where else do I need to add CORS mapping in order to fulfil the post request via web. Right now posting via mobile app(iOS + Android) is ok with the Flutter framework, but Flutter Web is not ok.
I think the issue might be related to the OncePerRequestFilter? Below is my code from my WebSecurityConfigurerAdapter
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
class AppWebSecurityConfigurerAdapter(
private val passwordEncoder: PasswordEncoder,
private val appUserDetailsService: AppUserDetailsService,
private val jwtConfiguration: JwtConfiguration,
private val secretKey: SecretKey,
private val repository: MemberRepository
) : WebSecurityConfigurerAdapter() {
override fun configure(http: HttpSecurity?) {
http {
csrf {
disable()
}
sessionManagement {
sessionCreationPolicy = SessionCreationPolicy.STATELESS
}
addFilterAt<UsernamePasswordAuthenticationFilter>(
JwtUsernameAndPasswordAuthenticationFilter(
authenticationManager(),
jwtConfiguration,
secretKey,
repository
)
)
addFilterAfter<JwtUsernameAndPasswordAuthenticationFilter>(JwtTokenVerifier(jwtConfiguration, secretKey))
authorizeRequests {
authorize(anyRequest, permitAll)
}
}
}
override fun configure(auth: AuthenticationManagerBuilder?) {
auth?.authenticationProvider(daoAuthenticationProvider())
}
#Bean
fun daoAuthenticationProvider() =
DaoAuthenticationProvider().apply {
setPasswordEncoder(passwordEncoder)
setUserDetailsService(appUserDetailsService)
}
}
My code for UsernameAndPasswordAuthenticationFilter
class JwtUsernameAndPasswordAuthenticationFilter(
authenticationManager: AuthenticationManager,
private val configuration: JwtConfiguration,
private val secretKey: SecretKey,
private val repository: MemberRepository
) : UsernamePasswordAuthenticationFilter(authenticationManager) {
private val objectMapper = jacksonObjectMapper().apply {
registerModule(JavaTimeModule())
disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
}
override fun attemptAuthentication(request: HttpServletRequest?, response: HttpServletResponse?): Authentication {
val authenticationRequest: UsernameAndPasswordAuthenticationRequest? =
request?.inputStream?.let { jacksonObjectMapper().readValue(it) }
return authenticationManager.authenticate(
UsernamePasswordAuthenticationToken(authenticationRequest?.username, authenticationRequest?.password)
)
}
override fun successfulAuthentication(
request: HttpServletRequest?,
response: HttpServletResponse?,
chain: FilterChain?,
authResult: Authentication?
) {
val token = Jwts.builder()
.setSubject(authResult?.name)
.claim("authorities", authResult?.authorities)
.setIssuedAt(Date())
.setExpiration(java.sql.Date.valueOf(LocalDate.now().plusDays(configuration.daysToExpire)))
.signWith(secretKey)
.compact()
val body = objectMapper.writeValueAsString(authResult?.name?.let(repository::findByEmail))
response?.apply {
addHeader(configuration.authorizationHeader, "${configuration.tokenPrefix} $token")
addHeader(HttpHeaders.CONTENT_TYPE, "application/json;charset=utf-8")
writer.write(body)
}
}
}
App.kt
#SpringBootApplication
#ConfigurationPropertiesScan
class Application
fun main(args: Array<String>) {
runApplication<Application>(*args)
}
Much help is appreciated:)
I figured out the issue. This relates to the cors configuration in Spring Security. If you use web, the browser changed the POST request into an OPTIONS request, therefore blocked by the default CORS configuration because there default does not allow OPTION requests
override fun configure(http: HttpSecurity?) {
http {
csrf {
disable()
}
cors {} //Enable CORS here
sessionManagement {
sessionCreationPolicy = SessionCreationPolicy.STATELESS
}
addFilterAt<UsernamePasswordAuthenticationFilter>(
JwtUsernameAndPasswordAuthenticationFilter(
authenticationManager(),
jwtConfiguration,
secretKey,
repository
)
)
addFilterAfter<JwtUsernameAndPasswordAuthenticationFilter>(JwtTokenVerifier(jwtConfiguration, secretKey))
authorizeRequests {
authorize(anyRequest, permitAll)
}
}
}
Configure your CORS here
#Bean
fun corsConfigurationSource(): CorsConfigurationSource {
val configuration = CorsConfiguration()
configuration.allowedOrigins = listOf("https://example.com")
configuration.allowedMethods = listOf("*") //Allow all http methods
val source = UrlBasedCorsConfigurationSource()
source.registerCorsConfiguration("/**", configuration)
return source
}
This is no longer needed after implementing CORS in Spring Security
#Component
class AppRepositoryRestConfigurer : RepositoryRestConfigurer {
override fun configureRepositoryRestConfiguration(config: RepositoryRestConfiguration?, cors: CorsRegistry?) {
cors?.addMapping("/**")?.allowedOriginPatterns("http://localhost:[*]")
}
}

Spring Security SAML2 issue signing SAMLRequest

I'm using spring boot 2.4.1 and spring security SAML2 support
I successfully configured my Service Provider. I created a self-signed certificate and I'm trying to use an IDP that requires signed AuthnRequests.
This is my RelyingPartyRegistrationRepository configuration:
#Bean
public RelyingPartyRegistrationRepository relyingPartyRegistrations() throws Exception{
KeyStore ks = KeyStore.getInstance(this.keyStoreType);
char[] pwd = keyStorePassword != null ? keyStorePassword.toCharArray() : null;
String ksName = keyStoreName.replaceAll("classpath:", "");
Resource keystoreRes = new ClassPathResource(ksName);
ks.load(keystoreRes.getInputStream(), pwd);
PrivateKey privateKey = (PrivateKey)ks.getKey(keyStoreAlias, keyStoreKeyPassword.toCharArray());
X509Certificate cert = (X509Certificate) ks.getCertificate(keyStoreAlias);
RelyingPartyRegistration registration = RelyingPartyRegistrations
.fromMetadataLocation(assertingPartyMetadataLocation)
.registrationId(registrationId)
.entityId(spEntityId)
.signingX509Credentials((c) -> c.add(Saml2X509Credential.signing(privateKey, cert)))
.decryptionX509Credentials((c)->c.add(Saml2X509Credential.decryption(privateKey, cert)))
.build();
return new InMemoryRelyingPartyRegistrationRepository(registration);
}
The application successfully starts but,, every time I makes a new request, I got an exception on the IDP side because no KeyInfo element is found in the AuthnRequest
By seeing my application logs, I found this log:
2021-01-06 12:20:35,650 23472 [XNIO-1 task-7] INFO o.o.x.s.support.SignatureSupport - No KeyInfoGenerator was supplied in parameters or resolveable for credential type org.opensaml.security.x509.X509Credential, No KeyInfo will be generated for Signature
I can't understand if I'm missing something in the configuration.
Please note that the same happens also with a certificate released by a trusted CA and not only with self-signed certificate. So I'm thinking it's a kind of configuration mistake I'm doing or a kind of bug.
May you kindly give me tip in how to solve this issue?
Angelo
UPDATE
I solved my current issue. Anyway I think it's a my mistake. Basically I modified the org.springframework.security.saml2.provider.service.authentication.OpenSamlAuthenticationRequestFactory
I added the following method:
private KeyInfoGenerator x509KeyInfoGenerator() {
X509KeyInfoGeneratorFactory generator = new X509KeyInfoGeneratorFactory();
generator.setEmitEntityCertificate(true);
generator.setEmitEntityCertificateChain(true);
return generator.newInstance();
}
I called this method here:
private SignatureSigningParameters resolveSigningParameters(RelyingPartyRegistration relyingPartyRegistration) {
List<Credential> credentials = resolveSigningCredentials(relyingPartyRegistration);
List<String> algorithms = Collections.singletonList(SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA256);
List<String> digests = Collections.singletonList(SignatureConstants.ALGO_ID_DIGEST_SHA256);
String canonicalization = SignatureConstants.ALGO_ID_C14N_EXCL_OMIT_COMMENTS;
SignatureSigningParametersResolver resolver = new SAMLMetadataSignatureSigningParametersResolver();
CriteriaSet criteria = new CriteriaSet();
BasicSignatureSigningConfiguration signingConfiguration = new BasicSignatureSigningConfiguration();
signingConfiguration.setSigningCredentials(credentials);
signingConfiguration.setSignatureAlgorithms(algorithms);
signingConfiguration.setSignatureReferenceDigestMethods(digests);
signingConfiguration.setSignatureCanonicalizationAlgorithm(canonicalization);
criteria.add(new SignatureSigningConfigurationCriterion(signingConfiguration));
try {
SignatureSigningParameters parameters = resolver.resolveSingle(criteria);
parameters.setKeyInfoGenerator(x509KeyInfoGenerator());
Assert.notNull(parameters, "Failed to resolve any signing credential");
return parameters;
}
catch (Exception ex) {
throw new Saml2Exception(ex);
}
}
Now I don't have errors on IdP side but I'm thinking I'm missing something in my configuration. This is my whole web security configuration:
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true,
securedEnabled = true,
jsr250Enabled = true)
public class ApplicazioneMockWebSecurityCfg extends WebSecurityConfigurerAdapter {
static {
OpenSamlInitializationService.requireInitialize((registry) -> {
X509KeyInfoGeneratorFactory generator = new X509KeyInfoGeneratorFactory();
generator.setEmitEntityCertificate(true);
generator.setEmitEntityCertificateChain(true);
NamedKeyInfoGeneratorManager manager = new NamedKeyInfoGeneratorManager();
manager.registerDefaultFactory(generator);
});
}
#Value("${applicazione.mock.external.idp.metadata.location}")
private String assertingPartyMetadataLocation;
#Value("${applicazione.mock.external.idp.metadata.registration.id}")
private String registrationId;
#Value("${server.ssl.key-alias}")
private String keyStoreAlias;
#Value("${server.ssl.key-password}")
private String keyStoreKeyPassword;
#Value("${server.ssl.key-store-password}")
private String keyStorePassword;
#Value("${server.ssl.keystore}")
private String keyStoreName;
#Value("${server.ssl.key-store-type}")
private String keyStoreType;
#Value("${sael.spid.service.provider.applicazione.mock.metadata.entity.id}")
private String spEntityId;
public static final String LOGOUT_URL = "/public/logout";
public static final String LOGIN_PAGE = "/public/home";
#Override
protected void configure(HttpSecurity http) throws Exception {
OpenSamlAuthenticationProvider authenticationProvider = new OpenSamlAuthenticationProvider();
authenticationProvider.setResponseAuthenticationConverter(responseToken -> {
// Saml2Authentication authentication = OpenSamlAuthenticationProvider
// .createDefaultResponseAuthenticationConverter()
// .convert(responseToken);
Assertion assertion = responseToken.getResponse().getAssertions().get(0);
String username = assertion.getSubject().getNameID().getValue();
List<AttributeStatement> attrStatements = assertion.getAttributeStatements();
String valoreAttributo = null;
Map<String, String> samlAttributes = new HashMap<>();
for (AttributeStatement attrStatement : attrStatements) {
List<Attribute> attrs = attrStatement.getAttributes();
for (Attribute attr : attrs) {
String nomeAttributo = attr.getName();
List<XMLObject> valoriAttributo = attr.getAttributeValues();
//In genere la lista dei valori รจ di 1 elemento
XMLObject valueObj = valoriAttributo.get(0);
valoreAttributo = getValue(valueObj, valoreAttributo);
samlAttributes.put(nomeAttributo, valoreAttributo);
}
}
if( !StringUtils.hasText(valoreAttributo) ) {
throw new IllegalStateException("Impossibile proseguire. Codice Fiscale non trovato tra gli attributi SAML");
}
UserDetails userDetails = new ApplicazioneMockLoggedUser(username, "[PROTECTED]", samlAttributes, AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_USER"));
return new SaelSamlAuthentication(userDetails);
});
Converter<HttpServletRequest, RelyingPartyRegistration> relyingPartyRegistrationResolver =
new DefaultRelyingPartyRegistrationResolver(this.relyingPartyRegistrations());
http
.authorizeRequests()
.antMatchers("/protected/**")
.authenticated()
.antMatchers("/public/**")
.permitAll()
.and()
.saml2Login(authorize ->{
authorize
.loginPage(LOGIN_PAGE)
.authenticationManager(new ProviderManager(authenticationProvider))
;
})
.logout(logout->{
logout
.logoutUrl(LOGOUT_URL)
.logoutSuccessHandler(saelLogoutSuccessHanlder())
.logoutRequestMatcher(saelRequestMatcher())
.invalidateHttpSession(true)
.deleteCookies("JSESSIONID")
//.logoutSuccessUrl(LOGIN_PAGE+"?logout")
.permitAll();
})
.addFilterBefore(new Saml2MetadataFilter(relyingPartyRegistrationResolver, new OpenSamlMetadataResolver()), Saml2WebSsoAuthenticationFilter.class);
}
#Bean
public RequestMatcher saelRequestMatcher() {
return new SaelRequestMatcher();
}
#Bean
public LogoutSuccessHandler saelLogoutSuccessHanlder() {
return new SaelLogoutSuccessHandler();
}
#Bean
Saml2AuthenticationRequestFactory authenticationRequestFactory(
AuthnRequestConverter authnRequestConverter) {
OpenSamlAuthenticationRequestFactory authenticationRequestFactory =
new OpenSamlAuthenticationRequestFactory();
authenticationRequestFactory.setAuthenticationRequestContextConverter(authnRequestConverter);
return authenticationRequestFactory;
}
#Bean
public RelyingPartyRegistrationRepository relyingPartyRegistrations() throws Exception{
KeyStore ks = KeyStore.getInstance(this.keyStoreType);
char[] pwd = keyStorePassword != null ? keyStorePassword.toCharArray() : null;
String ksName = keyStoreName.replaceAll("classpath:", "");
Resource keystoreRes = new ClassPathResource(ksName);
ks.load(keystoreRes.getInputStream(), pwd);
PrivateKey privateKey = (PrivateKey)ks.getKey(keyStoreAlias, keyStoreKeyPassword.toCharArray());
X509Certificate cert = (X509Certificate) ks.getCertificate(keyStoreAlias);
RelyingPartyRegistration registration = RelyingPartyRegistrations
.fromMetadataLocation(assertingPartyMetadataLocation)
.registrationId(registrationId)
.entityId(spEntityId)
.signingX509Credentials((c) -> c.add(Saml2X509Credential.signing(privateKey, cert)))
.decryptionX509Credentials((c)->c.add(Saml2X509Credential.decryption(privateKey, cert)))
.build();
return new InMemoryRelyingPartyRegistrationRepository(registration);
}
private String getValue( XMLObject valueObj, String defaultValue ) {
if( valueObj instanceof XSStringImpl ) {
XSStringImpl stringImpl = (XSStringImpl)valueObj;
return stringImpl.getValue();
}
return defaultValue;
}
}
May you help me in understanding if I'm missing something (I think I'm missing something)

How to get a new access token by checking the lifetime of the previous one if it's expired or not in spring webffux?

I am working on a project where the server provide only an access token without the refresh token for some company needs.
Referring to official docs, the following code will automatically refresh the access token when it's expired if a refresh token exists. if not it will retrieve a new one for every request.
Here is the optimal code :
#Bean
WebClient webClient(ClientRegistrationRepository clientRegistrations,
OAuth2AuthorizedClientRepository authorizedClients) {
ServletOAuth2AuthorizedClientExchangeFilterFunction oauth =
new ServletOAuth2AuthorizedClientExchangeFilterFunction(clientRegistrations, authorizedClients);
// (optional) explicitly opt into using the oauth2Login to provide an access token implicitly
// oauth.setDefaultOAuth2AuthorizedClient(true);
// (optional) set a default ClientRegistration.registrationId
// oauth.setDefaultClientRegistrationId("client-registration-id");
return WebClient.builder()
.apply(oauth2.oauth2Configuration())
.build();
}
Here is the code that i am using :
#Configuration
public class OauthEmployeConfig{
/**
** ... String baseUrl, String accessUrl for the access token url
**/
#Bean
public WebClient webClient(UserRegistration userRegistr) {
ClientRequest clientRequest = ClientRequest
.create(HttpMethod.POST, URI.create(accessUrl))
.header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE)
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE)
.headers(headers -> headers.setBasicAuth(userRegistr.getClientId(), userRegistr.getClientSecret()))
.body(BodyInserters.fromFormData("grant_type", userRegistr.getAuthorizGrantType())
.with("scope", userRegistr.getScope().replaceAll(",", "")))
.build();
return WebClient.builder()
.baseUrl(baseUrl)
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.defaultHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE)
.filter((request, next) -> next.exchange(clientRequest)
.flatMap(response -> response.body(org.springframework.security.oauth2.core.web.reactive.function.OAuth2BodyExtractors.oauth2AccessTokenResponse()))
.map(accessToken -> accessToken.getAccessToken().getTokenValue())
.map(token -> setBearer(request, token))
.flatMap(next::exchange))
.filter(logRequest())
.filter(handleResponseError())
.build();
}
private ClientRequest setBearer(ClientRequest request, String token) {
return ClientRequest.from(request)
.header("Authorization", "Bearer " + token).build();
}
private static ExchangeFilterFunction handleResponseError() {
return ExchangeFilterFunction.ofResponseProcessor(
response -> response.statusCode().isError()
? response.bodyToMono(String.class)
.flatMap(errorBody -> Mono.error(new RuntimeException(errorBody, response.statusCode())))
: Mono.just(response));
}
private static ExchangeFilterFunction logRequest() {
return ExchangeFilterFunction.ofRequestProcessor(clientRequest -> {
clientRequest.headers().forEach((name, values) -> values.forEach(value -> LOG.info("{}={}", name, value)));
return Mono.just(clientRequest);
});
}
}
I would like to know if there is an approach to get the access token once and keep it for every request while it's not expire, and when it's the case I get a new one!

Manually set authentication with ReactiveSecurityContextHolder

I'm trying to setup Spring Security with Spring Web Flux. I don't understand how to manually set the SecurityContext with ReactiveSecurityContextHolder. Do you have any resource or hint?
Take for example this filter I've written that reads a JWT token and needs to set the authentication manually:
#Slf4j
public class JwtTokenAuthenticationFilter implements WebFilter {
private final JwtAuthenticationConfig config;
private final JwtParser jwtParser = Jwts.parser();
public JwtTokenAuthenticationFilter(JwtAuthenticationConfig config) {
this.config = config;
jwtParser.setSigningKey(config.getSecret().getBytes());
}
#Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
String token = exchange.getRequest().getHeaders().getFirst(config.getHeader());
if (token != null && token.startsWith(config.getPrefix() + " ")) {
token = token.replace(config.getPrefix() + " ", "");
try {
Claims claims = jwtParser.parseClaimsJws(token).getBody();
String username = claims.getSubject();
#SuppressWarnings("unchecked")
List<String> authorities = claims.get("authorities", List.class);
if (username != null) {
UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(username, null,
authorities.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList()));
// TODO set authentication into ReactiveSecurityContextHolder
}
} catch (Exception ex) {
log.warn(ex.toString(), ex);
ReactiveSecurityContextHolder.clearContext();
}
}
return chain.filter(exchange);
}
}
I managed to update the SecurityContext by calling:
return chain.filter(exchange).subscriberContext(ReactiveSecurityContextHolder.withAuthentication(auth));
Correct me if I'm wrong or if there is a better way to manage it.
I searched a lot about this issue and get this thing worked.
You can try setting the context while passing the filter chain like below.
return chain.filter(exchange).contextWrite(ReactiveSecurityContextHolder.withAuthentication(authentication));

Spring security OAuth2 - invalidate session after authentication

We are securing out REST services using spring security OAuth2. Applications can call into either the /oauth/authorize, /oauth/token or /rest-api endpoints. The token and rest-api endpoints are stateless and do not need a session.
Can we invalidate the session after the user is authenticated? If so, what is the best approach. We want the user to sign-in always whenever a call to /oauth/authorize is made. Currently, calls to /oauth/authorize are skipping authentication whenever a session exists.
Understanding that the question is a bit old, I hope that the following could be helpful for those who search for the correct answer for the question
OP asked not about tokens invalidation, but how to invalidate httpSession on Spring OAuth2 server right after user authentication successfully passed and a valid access_token or authorization_code (for subsequent getting of access_token) returned to a client.
There is no out-of-the-box solution for this use-case still. But working workaround from the most active contributor of spring-security-oauth, Dave Syer, could be found here on GitHub
Just copy of the code from there:
#Service
#Aspect
public class SessionInvalidationOauth2GrantAspect {
private static final String FORWARD_OAUTH_CONFIRM_ACCESS = "forward:/oauth/confirm_access";
private static final Logger logger = Logger.getLogger(SessionInvalidationOauth2GrantAspect.class);
#AfterReturning(value = "within(org.springframework.security.oauth2.provider.endpoint..*) && #annotation(org.springframework.web.bind.annotation.RequestMapping)", returning = "result")
public void authorizationAdvice(JoinPoint joinpoint, ModelAndView result) throws Throwable {
// If we're not going to the confirm_access page, it means approval has been skipped due to existing access
// token or something else and they'll be being sent back to app. Time to end session.
if (!FORWARD_OAUTH_CONFIRM_ACCESS.equals(result.getViewName())) {
invalidateSession();
}
}
#AfterReturning(value = "within(org.springframework.security.oauth2.provider.endpoint..*) && #annotation(org.springframework.web.bind.annotation.RequestMapping)", returning = "result")
public void authorizationAdvice(JoinPoint joinpoint, View result) throws Throwable {
// Anything returning a view and not a ModelView is going to be redirecting outside of the app (I think).
// This happens after the authorize approve / deny page with the POST to /oauth/authorize. This is the time
// to kill the session since they'll be being sent back to the requesting app.
invalidateSession();
}
#AfterThrowing(value = "within(org.springframework.security.oauth2.provider.endpoint..*) && #annotation(org.springframework.web.bind.annotation.RequestMapping)", throwing = "error")
public void authorizationErrorAdvice(JoinPoint joinpoint) throws Throwable {
invalidateSession();
}
private void invalidateSession() {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes())
.getRequest();
HttpSession session = request.getSession(false);
if (session != null) {
logger.warn(String.format("As part of OAuth application grant processing, invalidating session for request %s", request.getRequestURI()));
session.invalidate();
SecurityContextHolder.clearContext();
}
}
}
add pom.xml
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
</dependency>
Another solution could be to set session time out to some very small value. The simplest way to achieve that is put the following to application.yml config:
server:
session:
timeout: 1
But it's not ideal solution as the minimum value could be provider is 1 (zero is reserved for infinite sessions) and it is in minutes not in seconds
From what I understand, you are trying to programmatically logout after you have undertaken certain set of actions. Probably you should look into the SecurityContextLogoutHandler and see how it works. There is a method for logout there. I think calling it as an advice will solve your problem.
public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
Assert.notNull(request, "HttpServletRequest required");
if (invalidateHttpSession) {
HttpSession session = request.getSession(false);
if (session != null) {
session.invalidate();
}
}
SecurityContextHolder.clearContext();
}
First: in your configuration declare bean with token store for oauth
#Bean
#Primary
public TokenStore tokenStore() {
return new InMemoryTokenStore();
}
For controller approach we made the following class
#Controller
public class TokenController {
#RequestMapping(value = "/oauth/token/revoke", method = RequestMethod.POST)
public #ResponseBody void create(#RequestParam("token") String value) {
this.revokeToken(value);
}
#Autowired
TokenStore tokenStore;
public boolean revokeToken(String tokenValue) {
OAuth2AccessToken accessToken = tokenStore.readAccessToken(tokenValue);
if (accessToken == null) {
return false;
}
if (accessToken.getRefreshToken() != null) {
tokenStore.removeRefreshToken(accessToken.getRefreshToken());
}
tokenStore.removeAccessToken(accessToken);
return true;
}
}
If you don't wan't to use this approach you can grab current user's token autowiring Principal:
OAuth2Authentication authorization = (OAuth2Authentication) principal;
OAuth2AuthenticationDetails details = (OAuth2AuthenticationDetails) authorization.getDetails();
String token = details.getTokenValue();
Or even autowiring OAuth2Authentication:
OAuth2AuthenticationDetails details = (OAuth2AuthenticationDetails) authentication.getDetails();
String token = details.getTokenValue();
I can offer such an option (according to #de_xtr recomendation):
import static org.springframework.web.context.request.RequestContextHolder.currentRequestAttributes;
#Slf4j
#Component
#Aspect
public class InvalidateSessionAspect {
private final LogoutHandler logoutHandler;
public InvalidateSessionAspect() {
logoutHandler = new SecurityContextLogoutHandler();
}
#Pointcut("execution(* org.springframework.security.oauth2.provider.endpoint.TokenEndpoint.postAccessToken(..))")
public void postAccessTokenPointcut() {
}
#AfterReturning(value = "postAccessTokenPointcut()", returning = "entity")
public void invalidateSession(JoinPoint jp, Object entity) {
log.debug("[d] Trying to invalidate the session...");
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) currentRequestAttributes();
HttpServletRequest request = requestAttributes.getRequest();
logoutHandler.logout(request, null, null);
log.debug("[d] Session has been invalidated");
}
}
And the option without any aspects:
#Slf4j
class LogoutHandlerInterceptor implements HandlerInterceptor {
#Override
public void postHandle(HttpServletRequest req, HttpServletResponse resp, Object h, ModelAndView view) {
HttpSession session = req.getSession(false);
if (session != null) {
log.debug("[d] Trying to invalidate the session...");
session.invalidate();
SecurityContext context = SecurityContextHolder.getContext();
context.setAuthentication(null);
SecurityContextHolder.clearContext();
log.debug("[d] Session has been invalidated");
}
}
}
#Configuration
#EnableAuthorizationServer
public class AuthServerConfig extends AuthorizationServerConfigurerAdapter {
//...
#Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
endpoints.addInterceptor(new LogoutHandlerInterceptor())
// ...
;
}
}

Resources