Manually set authentication with ReactiveSecurityContextHolder - spring-security

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));

Related

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)

Connect to Office365 via backend service using OAuth2 in NON interactive way (bar initial setup)

I have a background service which reads & sends from a mailbox. It is created in a web ui, but after the schedule is created and mailbox set, it should run automatically, without further user prompt.
I have used the various combinations of the MSAL and both public and confidential clients (either would be acceptable as the server can maintain the client secret.
I have used the EWS client and got that working, but there is a note that the client_credentials flow won't work for IMAP/POP/SMTP.
I have a small console app working, but each time it runs, it needs to login interactively, and so long as I don't restart the application, it will keep authenticating, and I can call the AquireTokenSilently.
The Question
How can I make the MSAL save the tokens/data such that when it next runs, I can authenticate without user interaction again? I can store whatever is needed to make this work when the user authenticates, but I don't know what that should be nor how to reinstate it to make a new request, if the console app is restarted.
The Code
internal async Task<string> Test()
{
PublicClientApplication =
PublicClientApplicationBuilder.Create( "5896de31-e251-460c-9dc2-xxxxxxxxxxxx" )
.WithRedirectUri( "https://login.microsoftonline.com/common/oauth2/nativeclient" )
.WithAuthority( AzureCloudInstance.AzurePublic, ConfigurationManager.AppSettings["tenantId"] )
.Build();
//var scopes = new string[] { "email", "offline_access", "profile", "User.Read", "Mail.Read" };
var scopes = new string[] { "https://outlook.office.com/IMAP.AccessAsUser.All" };
var accounts = await PublicClientApplication.GetAccountsAsync();
var firstAccount = accounts.FirstOrDefault();
AuthenticationResult authResult;
if (firstAccount == null )
{
authResult = await PublicClientApplication.AcquireTokenInteractive( scopes ).ExecuteAsync();
}
else
{
//The firstAccount is null when the console app is run again
authResult = await PublicClientApplication.AcquireTokenSilent( scopes, firstAccount ).ExecuteAsync();
}
if(authResult == null)
{
authResult = await PublicClientApplication.AcquireTokenInteractive( scopes ).ExecuteAsync();
}
MailBee.Global.LicenseKey = "MN120-569E9E8D9E5B9E8D9EC8C4BC83D3-D428"; // (demo licence only)
MailBee.ImapMail.Imap imap = new MailBee.ImapMail.Imap();
var xOAuthkey = MailBee.OAuth2.GetXOAuthKeyStatic( authResult.Account.Username, authResult.AccessToken );
imap.Connect( "imap.outlook.com", 993 );
imap.Login( null, xOAuthkey, AuthenticationMethods.SaslOAuth2, AuthenticationOptions.None, null );
imap.SelectFolder( "INBOX" );
var count = imap.MessageCount.ToString();
return authResult.AccessToken;
}
It feels very much like a step missed, which can store the information to make subsequent requests and I would love a pointer in the right direction please.
When you create your PublicClientApplication, it provides you with the UserTokenCache.
UserTokenCache implements interface ITokenCache, which defines events to subscribe to token cache serialization requests as well as methods to serialize or de-serialize the cache at various formats.
You should create your own TokenCacheBuilder, which can store the tokens in file/memory/database etc.. and then use the events to subscribe to to token cache request.
An example of a FileTokenCacheProvider:
public abstract class MsalTokenCacheProviderBase
{
private Microsoft.Identity.Client.ITokenCache cache;
private bool initialized = false;
public MsalTokenCacheProviderBase()
{
}
public void InitializeCache(Microsoft.Identity.Client.ITokenCache tokenCache)
{
if (initialized)
return;
cache = tokenCache;
cache.SetBeforeAccessAsync(OnBeforeAccessAsync);
cache.SetAfterAccessAsync(OnAfterAccessAsync);
initialized = true;
}
private async Task OnAfterAccessAsync(TokenCacheNotificationArgs args)
{
if (args.HasStateChanged)
{
if (args.HasTokens)
{
await StoreAsync(args.Account.HomeAccountId.Identifier,
args.TokenCache.SerializeMsalV3()).ConfigureAwait(false);
}
else
{
// No token in the cache. we can remove the cache entry
await DeleteAsync<bool>(args.SuggestedCacheKey).ConfigureAwait(false);
}
}
}
private async Task OnBeforeAccessAsync(TokenCacheNotificationArgs args)
{
if (!string.IsNullOrEmpty(args.SuggestedCacheKey))
{
byte[] tokenCacheBytes = await GetAsync<byte[]>(args.SuggestedCacheKey).ConfigureAwait(false);
args.TokenCache.DeserializeMsalV3(tokenCacheBytes, shouldClearExistingCache: true);
}
}
protected virtual Task OnBeforeWriteAsync(TokenCacheNotificationArgs args)
{
return Task.CompletedTask;
}
public abstract Task StoreAsync<T>(string key, T value);
public abstract Task DeleteAsync<T>(string key);
public abstract Task<T> GetAsync<T>(string key);
public abstract Task ClearAsync();
}
And the MsalFileTokenCacheProvider:
public sealed class MsalFileTokenCacheProvider : MsalTokenCacheProviderBase
{
private string basePath;
public MsalFileTokenCacheProvider(string basePath)
{
this.basePath = basePath;
}
public override Task ClearAsync()
{
throw new NotImplementedException();
}
public override Task DeleteAsync<T>(string key)
{
if (string.IsNullOrEmpty(key))
{
throw new ArgumentException("Key MUST have a value");
}
string path = Path.Combine(basePath, key + ".json");
if (File.Exists(path))
File.Delete(path);
return Task.FromResult(true);
}
public override Task<T> GetAsync<T>(string key)
{
if (string.IsNullOrEmpty(key))
{
throw new ArgumentException("Key MUST have a value");
}
string path = Path.Combine(basePath, key + ".json");
if (File.Exists(path))
{
T value = JsonConvert.DeserializeObject<T>(File.ReadAllText(path));
return Task.FromResult(value);
}
else
return Task.FromResult(default(T));
}
public override Task StoreAsync<T>(string key, T value)
{
string contents = JsonConvert.SerializeObject(value);
string path = Path.Combine(basePath, key + ".json");
File.WriteAllText(path, contents);
return Task.FromResult(value);
}
}
So based on your code you will have:
PublicClientApplication =
PublicClientApplicationBuilder.Create( "5896de31-e251-460c-9dc2-xxxxxxxxxxxx" )
.WithRedirectUri( "https://login.microsoftonline.com/common/oauth2/nativeclient" )
.WithAuthority( AzureCloudInstance.AzurePublic, ConfigurationManager.AppSettings["tenantId"] )
.Build();
MsalFileTokenCacheProvider cacheProvider = new MsalFileTokenCacheProvider("TokensFolder");
cacheProvider.InitializeCache(PublicClientApplication.UserTokenCache);
//var scopes = new string[] { "email", "offline_access", "profile", "User.Read", "Mail.Read" };
var scopes = new string[] { "https://outlook.office.com/IMAP.AccessAsUser.All" };
// when you call the below code, the PublicClientApplication will use your token cache
//provider in order to get the required Account. You should also use the
//PublicClientApplication.GetAccountAsync(key) which will use the token cache provider for
//the specific account that you want to get the token. If there is an account you could
//just call the AcquireTokenSilent method. The acquireTokenSilent method will take care of the token expiration and will refresh if needed.
//Please bare in mind that in some circumstances the AcquireTokenSilent method will fail and you will have to use the AcquireTokenInteractive method again. //Example of this would be when the user changes password, or has removed the access to your Application via their Account.
var accounts = await PublicClientApplication.GetAccountsAsync();
var firstAccount = accounts.FirstOrDefault();
Please refer to the following documentation from Microsoft.
https://learn.microsoft.com/en-us/azure/active-directory/develop/msal-net-token-cache-serialization

what's the advantage of java security framework?

Compare to save user identity to session and use interceptor to authorize.
Like:
authentication:
#RequestMapping("/checkLogin.do")
public String checkLogin(Map map, HttpSession httpSession, String username, String password) {
JSONObject userJson = userService.checkLogin(username, password);
if ((Integer) userJson.get("success") == 0) {
httpSession.setAttribute("userinfo", userJson);
return "redirect:/index.do";
} else {
map.put("error", -1);
return "login";
}
authorization:
#Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse arg1, Object arg2) throws Exception {
String requestURI = request.getRequestURI();
HttpSession session = request.getSession();
String userinfo= (String) session.getAttribute("userinfo");
if (userinfo!= null) {
return true;
} else {
arg1.sendRedirect("/login.do");
return false;
}
}
what's the benefit using security framework such as shiro or spring-security?
In short, the benefit comes in how fast you can incorporate security related requirements into your project; Basic security requirements might be permissions management and role based authorization while more complex requirements might be cross platform session management.

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

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