I've built a SAML SP based on this project: https://github.com/vdenotaris/spring-boot-security-saml-sample and would like to display the SAML token on the error page for debugging purposes.
I've added an error controller
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import javax.servlet.http.HttpServletRequest;
#Controller
public class ErrorController implements org.springframework.boot.autoconfigure.web.ErrorController {
private static final Logger log = LoggerFactory.getLogger(SSOController.class);
private static final String PATH = "/error";
#RequestMapping(value = PATH)
public String error(HttpServletRequest request, Model model, Exception exception) {
model.addAttribute(exception);
return "error";
}
#Override
public String getErrorPath() {
return PATH;
}
}
I'm testing it with bad requests, but the Exception doesn't contain much helpful info. I would like to display the SAML token on the error page or at least in the logs for troubleshooting purposes.
How can I get access to the SAML token or at least the attributes that of the SAML token.
Thanks in advance.
Also, is there a way to manufacture the same SAML token using Postman (or a similar tool) to make testing easier. Right now, I'm deploying to AWS everytime because the third part IdP is not configured to handle localhost.
Thanks again.
To show/access SAML assertion(SAML assertion is part of SAML response) anywhere in your application (of course after successfull
authentication), you can use following code.
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
SAMLCredential credential = (SAMLCredential) authentication.getCredentials();
System.out.println("assertion is:" + XMLHelper.nodeToString(SAMLUtil.marshallMessage(credential.getAuthenticationAssertion())))
If you want to print theSAMl response in the logs you can set the logging level of spring-security to debug and spring will print SAML response in the log.
is there a way to manufacture the same SAML token using Postman
No. To get SAML assertion you have to follow the complete flow. Also you can not generate the SAML response by yourself because you can not sign it with same private key as of IDP. It also depends on your IDP if it supports such feature. What you can do is configure IDP to generate SAML token with really long expiration time say 24 hrs. Then you can use same SAML token for testing in your local application.
Related
With spring security saml2 provider version 5.7.x mandatory validation of InResponseTo was introduced if it is provided in the authentication response.
Validation logic expects to find saved Saml2AuthenticationRequest in HttpSession. However that is only possible if SameSite attribute is not set.
According security requirements of current project I'm working on it is set to Lax or Strict. This configuration is done outside of the application. This causes loss of the session and request data.
Maybe someone already have dealed with an issue and knows how to deal with it? I don't see any way to disable validation or alternative way of saving request available in the library.
While upgrading spring security from 5.x to 6, I ran into the same issue with spring session cookie. There doesn't seem to be any standard solution for this. For now, I have provided an implementation of Saml2AuthenticationRequestRepository which saves the SAML request in database based on RelayState parameter. Following is the sample implementation using Mongo. Any backend (In-memory cache, Redis, MySQL etc.) can be used -
import java.util.Optional;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.security.saml2.core.Saml2ParameterNames;
import org.springframework.security.saml2.provider.service.authentication.AbstractSaml2AuthenticationRequest;
import org.springframework.security.saml2.provider.service.web.Saml2AuthenticationRequestRepository;
import org.springframework.stereotype.Repository;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
#Repository
#RequiredArgsConstructor
#Slf4j
public class MongoSaml2AuthenticationRequestRepository implements Saml2AuthenticationRequestRepository<AbstractSaml2AuthenticationRequest> {
public static final String SAML2_REQUEST_COLLECTION = "saml2RequestsRepository";
private final #NonNull MongoTemplate mongoTemplate;
#Override
public AbstractSaml2AuthenticationRequest loadAuthenticationRequest(HttpServletRequest request) {
String relayState = request.getParameter(Saml2ParameterNames.RELAY_STATE);
if (relayState == null) {
return null;
}
log.debug("Fetching SAML2 Authentication Request by relay state : {}", relayState.get());
Query query = Query.query(Criteria.where("relayState").is(relayState.get()));
AbstractSaml2AuthenticationRequest authenticationRequest = mongoTemplate.findOne(query, AbstractSaml2AuthenticationRequest.class, SAML2_REQUEST_COLLECTION);
if (!authenticationRequest.getRelayState().equals(relayState.get())) {
log.error("Relay State received from request '{}' is different from saved request '{}'.", relayState.get(), authenticationRequest.getRelayState());
return null;
}
log.debug("SAML2 Request retrieved : {}", authenticationRequest);
return authenticationRequest;
}
#Override
public void saveAuthenticationRequest(AbstractSaml2AuthenticationRequest authenticationRequest,
HttpServletRequest request, HttpServletResponse response) {
//As per OpenSamlAuthenticationRequestResolver, it will always have value. However, one validation can be added to check for null and regenerate.
String relayState = authenticationRequest.getRelayState();
log.debug("Relay State Received: {}", relayState);
mongoTemplate.save(authenticationRequest, SAML2_REQUEST_COLLECTION);
}
#Override
public AbstractSaml2AuthenticationRequest removeAuthenticationRequest(HttpServletRequest request,
HttpServletResponse response) {
AbstractSaml2AuthenticationRequest authenticationRequest = loadAuthenticationRequest(request);
if (authenticationRequest == null) {
return null;
}
mongoTemplate.remove(authenticationRequest, SAML2_REQUEST_COLLECTION);
return authenticationRequest;
}
}
Post adding this, all the SAML validations including InResponseTo validation are passed successfully. Since RelayState is not supposed to change between SAML requests as per specifications, this seemed like reasonable alternative (in absence of any standard solution so far). I am running this through standard security testing and will update the solution with my findings if any.
I have a secured Spring Cloud Gateway application using ServerHttpSecurity.oauth2Login() that can successfully renew expired access tokens using the refresh token. However, when the refresh token also expires and the application tries to renew the access token with it, I get a 500 Internal Server Error [seems to be caused by a 400 Bad Request error just before it] with the following exception:
org.springframework.security.oauth2.client.ClientAuthorizationException: [invalid_grant] Token is not active
at org.springframework.security.oauth2.client.RefreshTokenReactiveOAuth2AuthorizedClientProvider.lambda$authorize$0(RefreshTokenReactiveOAuth2AuthorizedClientProvider.java:97) ~[spring-security-oauth2-client-5.4.1.jar:5.4.1]
Full logs here: https://github.com/spring-projects/spring-security/files/8319348/logs.txt
Only if I re-issue the request (refresh browser with the call to the secured endpoint), I will get redirected to the login page (desired behavior).
While debugging, I noticed that re-issuing the request after the 500 Internal Server Error under the hood results in the following exception:
org.springframework.security.oauth2.client.ClientAuthorizationRequiredException: [client_authorization_required] Authorization required for Client Registration Id: <client-id>.
and that is probably what causes the redirect to the login page.
Request execution details here
My question: Can I avoid getting the 500 Internal Server Error and instead be redirected to the login page? If yes, how can I accomplish that?
Environment details
Spring Boot: 2.4.0
Spring Cloud: 2020.0.0
Spring Security: 5.4.1
The solution was to catch the 500 caused while refreshing a token and then initiating a new authorization flow, using the next classes:
import org.springframework.security.oauth2.client.*;
import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository;
import org.springframework.security.oauth2.client.web.DefaultReactiveOAuth2AuthorizedClientManager;
import org.springframework.security.oauth2.client.web.server.ServerOAuth2AuthorizedClientRepository;
import org.springframework.util.Assert;
import reactor.core.publisher.Mono;
/**
* A delegating implementation of ReactiveOAuth2AuthorizedClientManager to help deal with a 500 Internal Server Error
* that is a result of an expired access token. With ReactiveOAuth2AuthorizedClientManagerCustom, we manage to redirect
* to the login page instead of returning a 500 Internal Server Error to the user/client.
*/
public class ReactiveOAuth2AuthorizedClientManagerCustom implements ReactiveOAuth2AuthorizedClientManager {
private final ReactiveClientRegistrationRepository clientRegistrationRepository;
private final ServerOAuth2AuthorizedClientRepository authorizedClientRepository;
private final ReactiveOAuth2AuthorizedClientManager authorizedClientManager;
public ReactiveOAuth2AuthorizedClientManagerCustom(ReactiveClientRegistrationRepository clientRegistrationRepository,
ServerOAuth2AuthorizedClientRepository authorizedClientRepository) {
this.clientRegistrationRepository = clientRegistrationRepository;
this.authorizedClientRepository = authorizedClientRepository;
this.authorizedClientManager = new DefaultReactiveOAuth2AuthorizedClientManager(
this.clientRegistrationRepository, this.authorizedClientRepository
);
}
public Mono<OAuth2AuthorizedClient> authorize(OAuth2AuthorizeRequest authorizeRequest) {
Assert.notNull(authorizeRequest.getClientRegistrationId(), "Client registration id cannot be null");
return this.authorizedClientManager.authorize(authorizeRequest)
// The token has expired, therefore we initiate a new grant flow
.onErrorMap(
ClientAuthorizationException.class,
error -> new ClientAuthorizationRequiredException(authorizeRequest.getClientRegistrationId())
);
}
}
And then adding the next #Bean
public ReactiveOAuth2AuthorizedClientManagerCustomConfig(ReactiveClientRegistrationRepository clientRegistrationRepository,
ServerOAuth2AuthorizedClientRepository authorizedClientRepository) {
this.clientRegistrationRepository = clientRegistrationRepository;
this.authorizedClientRepository = authorizedClientRepository;
}
#Bean
#Primary
ReactiveOAuth2AuthorizedClientManager authorizedClientManager() {
return new ReactiveOAuth2AuthorizedClientManagerCustom(
this.clientRegistrationRepository, this.authorizedClientRepository
);
}
I was working on getting a client credential flow with Auth0 to work using Spring Security 5.4.1. I created a little demo application for reference: https://github.com/mathias-ewald/spring-security-auth0-clientcredentials-demo
Everything works fine, but I was wondering how to handle multiple OAuth2 clients. As far as I understand, the configuration made in OAuth2ClientSecurityConfig is valid for all client credential flows to any provider, correct?
What if I have another provider and don't want to convert RequestEntity in the same way?
There's usually no perfect answer for multi-tenancy since a lot depends on how early in the request you want to fork the behavior.
In Spring Security's OAuth 2.0 Client support, the ClientRegistration is the tenant, and that tenant information is available in most of the client APIs.
For example, your Auth0RequestEntityConverter could have different behavior based on the ClientRegistration in the request:
public RequestEntity<?> convert(
OAuth2ClientCredentialsGrantRequest request) {
ClientRegistration client = request.getClientRegistration();
if (client ...) {
} else if (client ...) {
} ...
}
Or, if you need to configure more things than the request entity converter, you could instead fork the behavior earlier by constructing a OAuth2AuthorizedClientManager for each provider:
public class ClientsOAuth2AuthorizedClientManager implements OAuth2AuthorizedClientManager {
private final Map<String, OAuth2AuthorizedClientManager> managers;
// ...
public OAuth2AuthorizedClient authorize(OAuth2AuthorizeRequest request) {
String clientRegistrationId = request.getClientRegistrationId();
return this.managers.get(clientRegistrationId).authorize(request);
}
}
I have one service and Keycloak 11 as Authentication server. Now I want to write tests. To mock the accesstoken, I use #WithMockKeycloakAuth. This works well and I get an unauthorized when I pass a bad role for example. Now I want to document it with spring rest docs the therefor I have to add the acesstoken as header field ( Bearer tokenAsBearerString ). Because of the annotation, the mocked token is added to the SecurityContext and I can extract it before doing the mvc.perform.
#Test
#Order(5)
#WithMockKeycloakAuth(authorities = "ROLE_owner")
void createProduct_RealmRoleOwner_HttpStatusCreated() throws Exception {
SecurityContext context = SecurityContextHolder.getContext();
KeycloakAuthenticationToken authentication =(KeycloakAuthenticationToken) context.getAuthentication();
AccessToken token = authentication.getAccount().getKeycloakSecurityContext().getToken();
The problem is that I need the accesstoken as Bearer string representation. I'm not yet very familiar with the jwt topic but I expected that if I use the mocked acces token and convert it to a jwt format / Base 64 encoded String the header should be correct.
In addition: I'm running a Keycloak container via docker in a seperate network so it is not reachable while I run my automated test. So mocking would be the only solution.
This question was also asked (and answered) here with a little more context.
The code snippet provided above doesn't show that test class is decorated with #AutoConfigureMockMvc(addFilters = false), reason why the security context is not attached to the MockMvc HTTP request (this is normally done by a spring-security filter).
The complete stack trace isn't provided neither, but it's very likely that the exception occurring when filters are enabled is due to JwtDecoder wiring from Keycloak boot lib. #MockBean JwtDecoder jwtDecoder; should be enough to fix it.
Finally, it is one of main features of the lib #WithMockKeycloakAuth is taken from to skip fetching, decoding and parsing an actual JWT from Keycloak instance. Trying to build an authorization header with valid JWT from mocked spring authentication is ...
I'm 50+ hours deep on this solution and would appreciate any input.
I have a JHipster 4.x generated application using Angular + Spring + JWT stateless authentication (myApp). I am wiring up a 3rd party OAuth 2 interface (battle.net) for authenticated myApp users to OAuth against battle.net so we can prove they own the battle.net account and pull their battle.net user id so the accounts are linked in myApp. So JWT southbound, OAuth2 northbound.
JWT works fine and OAuth appears to work fine. I am struggling because myApp uses a stateless JWT token and Spring #EnableOAuth2Client uses JSESSIONID, and I can't seem to bring the two together so I can relate data returned from the battle.net calls to the myApp Principal. battle.net uses a callback URL upon successful authentication, and I can see valid data in both myApp PrincipalExtractor as well as myApp AuthenticationSuccessHandler, but as there is no JWT token supplied, I have no way to link the battle.net data to the myApp user.
** User Initiates OAuth **
User -- JWT --> myApp /login/battlenet --> battle.net /oauth/*
** battle.net Callback Success **
battle.net --> myApp /callback/battlenet - This is good battlenet data but no JWT token so Principal is anonymousUser.
I see '&redirectUri=xxx&response_type=yyy&code=xxx' being passed to battle.net on the '/oauth/authorize' request. Is there a way to pass linking data to battle.net that is returned on the callback per the OAuth2 spec with #EnableOAuth2Client? I think that would solve my problem.
spring-core-4.3.13
spring-boot-starter-security-1.5.9
spring-security-core-4.2.4
spring-security-oauth2-2.0.14
Thanks!
I found a way to pass linking data. I hope it helps someone else. :)
#Bean
public OAuth2ClientContextFilter oauth2ClientContextFilter() {
OAuth2ClientContextFilter oauth2ClientContextFilter = new OAuth2ClientContextFilter();
oauth2ClientContextFilter.setRedirectStrategy(new BMAOAuthRedirectStrategy());
return oauth2ClientContextFilter;
}
class BMAOAuthRedirectStrategy extends DefaultRedirectStrategy {
#Override
public void sendRedirect(HttpServletRequest request, HttpServletResponse response, String url) throws IOException {
url = url.concat("&bma_uuid=MY_LINKING_DATA");
String redirectUrl = calculateRedirectUrl(request.getContextPath(), url);
redirectUrl = response.encodeRedirectURL(redirectUrl);
if (logger.isDebugEnabled()) {
logger.debug("Custom BMA SecurityConfiguration Redirecting to '" + redirectUrl + "'");
}
response.sendRedirect(redirectUrl);
}
}