-
Notifications
You must be signed in to change notification settings - Fork 6k
Added support for the CAS gateway feature #14193
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
Thank you very much @leleuj. I'll try to review this PR for 6.3. |
Thanks. Can I do something to help you in the review process? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As far as I understood from #9881 and #40 the idea behind this feature is to add a filter that would trigger the redirect to the CAS service with the gateway=true
parameter.
Based on previous reviews from @rwinch, the design was pretty much aligned and there were some details left like having a mechanism to avoid hitting the CAS server for every request.
I might be missing something but I do not see how the changes and the new components proposed in this PR work together to achieve the desired behavior. If you could outline the idea or maybe put together a sample (you can use this) it would be great to help me understand the proposed design.
cas/src/main/java/org/springframework/security/cas/web/CasAuthenticationEntryPoint.java
Outdated
Show resolved
Hide resolved
cas/src/main/java/org/springframework/security/cas/web/CasAuthenticationFilter.java
Outdated
Show resolved
Hide resolved
"the idea behind this feature is to add a filter that would trigger the redirect to the CAS service with the gateway=true parameter": Not exactly, the idea behind this feature is not to add a new filter, this is just a possible implementation. The idea behind this feature is to be able to call the CAS server with |
I had an issue with my test demo. I will test again and keep you posted. |
Forget my previous comments for now. |
You were right: I don't understand how the provider was called in the gateway mode. Anyway, I restarted my work from the provided demo: https://github.com/spring-projects/spring-security-samples/tree/main/servlet/spring-boot/java/cas/login |
OK. I just updated the PR. I used the provided demo: https://github.com/spring-projects/spring-security-samples/tree/main/servlet/spring-boot/java/cas/login I added: And to test the The changes are now even easier. They mainly impact the |
I still keep the idea that no additional filter is required, the Multiple filters can be defined on different URLs if need be. And I still prefer to have the |
The This is used to know if a specific token must be created for the "gateway mode" or if the regular CAS login must be performed: if (serviceTicket == null) {
if (this.gateway) {
this.logger.debug("Gateway request with no CAS ticket");
return new CasGatewayNoServiceTicketAuthenticationToken();
}
else {
this.logger.debug("Failed to obtain an artifact (cas ticket)");
serviceTicket = "";
}
} This is the final authentication available: |
For each request, the final Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (this.gateway && auth instanceof CasGatewayNoServiceTicketAuthenticationToken token) {
final Instant creationDate = token.getCreationDate();
final Instant startRetainDate = Instant.now().minusSeconds(this.gatewayAuthenticationRetainDelayInSeconds);
// removes authentication after a certain retain delay
if (creationDate.isBefore(startRetainDate)) {
this.logger.debug("Removes authentication because of gateway setting");
SecurityContextHolder.getContext().setAuthentication(null);
}
} If so, it is removed from the context and a new authentication is triggered, meaning a new round-trip to the CAS server is done (once again using the |
Sorry for the false start. Are things clearer now? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, @leleuj, I left some feedback inline.
I still think that we should have a new filter with the responsibility of redirecting to the CAS server with gateway=true
if needed.
I was taking a look at #9881 and it looks like there is support to prevent making too many requests via CasCookieGatewayRequestMatcher
It is fine to have the gateway
property inside ServiceProperties
, that way the CasAuthenticationFilter
can proper handle an unsuccessfulAuthentication
if there is no service ticket.
* @author Jerome LELEU | ||
* @since 6.3.0 | ||
*/ | ||
public final class CasGatewayNoServiceTicketAuthenticationToken extends AbstractAuthenticationToken { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this would be better by extending AnonymousAuthenticationToken
. My concern is that there might be some places where the code checks if token instanceof AnonymousAuthenticationToken
. This might not even be needed depending on the strategy that we adopt.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK. I get the point, but indeed, this may not be needed depending on the chosen strategy.
serviceTicket = ""; | ||
if (this.gateway) { | ||
this.logger.debug("Gateway request with no CAS ticket"); | ||
return new CasGatewayNoServiceTicketAuthenticationToken(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What happens on a public URL when there is no AuthenticationException
? Will the CasAuthenticationEntryPoint
be invoked?
To be able to invoke this filter to different URLs, the filterProcessingUrl
should allow customization, however, I'm still not convinced that it is the responsibility of this filter to be aware of the gateway request.
If we take a look at how OAuth2 components work, there are at least two filters, one that performs the redirect to the Authorization Server and another that processes the authorization response.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The CasAuthenticationEntryPoint
will only be invoked if the URL is configured with authenticated
.
And the CasGatewayNoServiceTicketAuthenticationToken
cannot be an AnonymousAuthenticationToken
as it leads to an infinite loop.
On the other hand, this prevents triggering a new CAS round-trip and only leads to a 403 error page for another URL protected with non-gateway CAS login. It would certainly need a custom AccessDeniedHandler
.
So although re-using the CasAuthenticationFilter
for the gateway
setting seems an alluring idea, I think I was wrong on this: I will create a new filter. I will do that next Monday.
I think it's better to close this PR and submit a new PR, isn't it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can use the same PR if you want, just force-push the new commits. You can also cherry-pick the commit from #9881 and polish it to achieve the desired result.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK. I will do that today. Thanks for your time and advices
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@marcusdacoregio I just submitted a polished-to-main-branch PR from #9881.
Just let me know if you have additional comments.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, @leleuj. It is looking nice.
I've left some feedback inline.
HttpServletResponse response = (HttpServletResponse) res; | ||
|
||
if (this.requestMatcher.matches(request)) { | ||
throw new TriggerCasGatewayException("Try a CAS gateway authentication"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I understand. I think the solution here is to use the CasAuthenticationEntryPoint
to perform the gateway redirection.
*/ | ||
public class TriggerCasGatewayFilter extends GenericFilterBean { | ||
|
||
private RequestMatcher requestMatcher; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The default could be the CasCookieGatewayRequestMatcher
and it can be customized via setRequestMatcher
.
The constructor can be removed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see your point. I can set the CasCookieGatewayRequestMatcher
by default.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is probably no need to change this class since we will not rely on it to redirect to the CAS server, instead the TriggerCasGatewayFilter
will do that for us.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is some logic in the CasAuthenticationEntryPoint
we may not want to duplicate in the TriggerCasGatewayFilter
(like the encodeServiceUrlWithSessionId
property).
Are you sure you don't want to use the CasAuthenticationEntryPoint
in the `TriggerCasGatewayFilter?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My concern with throwing the exception in TriggerCasGatewayFilter
is that it is not a failed authentication attempt, therefore I would refrain from using the CasAuthenticationEntryPoint
. One thing that we can do is to create a package-private CasUrlUtils
, something like:
final class CasUrlUtils {
static String createServiceUrl(HttpServletResponse response, ServiceProperties serviceProperties, boolean encodeServiceUrlWithSessionId) {
return WebUtils.constructServiceUrl(null, response, serviceProperties.getService(), null,
serviceProperties.getArtifactParameter(), encodeServiceUrlWithSessionId);
}
static String createRedirectUrl(String loginUrl, String serviceUrl, ServiceProperties serviceProperties, boolean isGateway) {
return CommonUtils.constructRedirectUrl(loginUrl, serviceProperties.getServiceParameter(), serviceUrl,
serviceProperties.isSendRenew(), isGateway);
}
}
Then we can use this util class in both CasAuthenticationFilter
and TriggerCasGatewayFilter
. Is the encodeServiceUrlWithSessionId
still needed in recent versions of CAS? If not, TriggerCasGatewayFilter
might not even care about it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, we can forget about the encodeServiceUrlWithSessionId
which is related to CAS 2 (15 years old). I won't use the CasAuthenticationEntryPoint
and ignore this setting.
It should even be removed (at least deprecated) in the CasAuthenticationEntryPoint
.
if (StringUtils.hasText(obtainArtifact(request))) { | ||
super.unsuccessfulAuthentication(request, response, failed); | ||
} | ||
else { | ||
SavedRequest savedRequest = this.requestCache.getRequest(request, response); | ||
if (savedRequest != null) { | ||
this.redirectStrategy.sendRedirect(request, response, savedRequest.getRedirectUrl()); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm finding this strategy to decide if that was a gateway request too sensitive.
Is it possible to make CAS redirect to the service URL with a query param? Something like my.company.com/cas/login?gateway=true&service=https://server3.company.com/webapp/login/cas%3Fgateway=true
. With that, upon successful/failed authentication, the CAS server would redirect to https://server3.company.com/webapp/login/cas?gateway=true
and we can be sure whether that was a gateway request or not.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're right, this strategy is a bit too sensitive (checking if we already have a saved request).
Though, using a parameter in the serviceUrl
will trigger a lot of complexity. Indeed, the service tickets created by the CAS server are linked to the original service URL.
It means that we can't reuse the existing ticketValidator
to validate service tickets created on a gateway request as the service URL won't match, it has gateway=true
for example while we try to validate the service ticket for the service without gateway=true
.
I can give it a try if you want. I just want you to confirm.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's discard the parameter for now. What about setting some attribute in the session (if the session exists) and checking that attribute later on
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
By the way, sorry for the back and forth on this, I haven't had time to test how this works on my own yet.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK. I will try to use the session for that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No problem in that case, the OAuth2AuthorizationRequestRedirectFilter
also saves the request before the redirect.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK. Good
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@marcusdacoregio I have updated the PR to take into account all your feedbacks:
- the exception has been removed
- the
TriggerCasGatewayFilter
performs the redirection to the CAS server withgateway=true
- the
CasAuthenticationEntryPoint
is no longer changed - the
gateway
nature of the CAS round-trip is saved into the web session.
BTW, I have added a TriggerCasGatewayFilterTests
to test the TriggerCasGatewayFilter
and cropped all commits into one.
We should be good for merging. Just let me know.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@marcusdacoregio Did you get some time to check this PR?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll take a look at it today
a4ccfd4
to
60b3466
Compare
Hello, @leleuj. I rebased your branch and applied a polish commit on top of yours. I'll wait for the CI to pass to merge it. As soon as the SNAPSHOTs are available I'll update the sample to include the gateway feature. |
Thanks @leleuj, this is now merged into |
Excellent! Thank your very much! As it's a not a breaking change, can I open a new PR for the 5.8.x stream? |
Sorry, but that is not possible. We only add enhancements to minor versions. |
I'm not sure I understand your answer: "We only add enhancements to minor versions." Would it be possible in a new version 5.9.x or only in version 6.3? |
Sorry if I was not clear. We follow the semantic versioning (major.minor.patch), and our policy is to add new features only when we increment the minor version. Since the next minor release is The |
OK. That makes sense. Thanks for your time and help. |
This PR aims to support the CAS
gateway
feature in thespring-security-cas
library.It follows: #40 and especially: #9881 as discussed with @marcusdacoregio.
Unit tests pass and I tested it manually.