-
Notifications
You must be signed in to change notification settings - Fork 457
Implement stateless AAD authentication filter which uses appRoles (#494) #512
Implement stateless AAD authentication filter which uses appRoles (#494) #512
Conversation
@wmitzel-airplus |
Hi @Incarnation-p-lee I agree that working on smaller PR can lead to more efficiency. This PR currently consists of 3 parts:
I would suggest to split it into two PRs:
|
@wmitzel-airplus
Just personal suggestion, you can make you own choose there. |
Hi, sorry for the long delay. Today is the first day of work after my Christmas leave. I've removed the sample module and will add it as a separate PR later. |
@wmitzel-airplus |
…-63) Squashed commit: [39c54c5] Extract sample application and put it into separate PR. [b47604b] CJ-63 Minor change after rebasing on master. [0db219d] CJ-63 Remove client-secret, restructure tests and refactor default role. [db1fe3f] CJ-63 renaming classes [32825af] Add autoconfiguration, documentation and an example project. [f75728f] CJ-63 First version of the stateless filter
f455cba
to
0a9d5dd
Compare
@Incarnation-p-lee |
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 I understand, if we leverage stateless filter we cannot use normal session based auth together. So from AuthProperty you may need to add some option like statelessEnabled to tell this. If then you can add configuration to AuthProperty instead of inherit from it.
azure-spring-boot-starters/azure-active-directory-spring-boot-starter/README.md
Outdated
Show resolved
Hide resolved
} | ||
} | ||
|
||
filterChain.doFilter(request, 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.
As stateless, you may need to cleanup SecurityContext after all filterChain done.
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 type of "cleanup" do you have in mind?
In the default configuration, Spring uses a ThreadLocal
to store the security context and also makes sure to clear it once the request is processed.
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 mean set the security context to null after request processed to make sure the stateless. Yes the spring security framewrok will did that for us if we config stateless session policy. But from our filter review, we cannot trust the user config and need to make sure the context is stateless. How do you think of 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.
I was referring to the first paragraph of SecurityContextHolder, SecurityContext and Authentication Objects in the spring security docs where it says:
[...] Using a ThreadLocal in this way is quite safe if care is taken to clear the thread after the present principal’s request is processed. Of course, Spring Security takes care of this for you automatically, so there is no need to worry about it.
I've validated this behaviour and seen that this code cleans up each request: https://github.com/spring-projects/spring-security/blob/f234a5fbdbf43dbceecfdfe3d41f10b09aa5f197/web/src/main/java/org/springframework/security/web/context/SecurityContextPersistenceFilter.java#L111-L116
But as a defense in depth measure, we can clear the security context after the doFilter
call. I suggest to do:
boolean cleanupRequired = false;
[...]
SecurityContextHolder.getContext().setAuthentication(authentication);
cleanupRequired = true;
[...]
try {
filterChain.doFilter(request, response);
} finally {
if (cleanupRequired) {
//Clear context after execution
SecurityContextHolder.clearContext();
}
}
What do you think?
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, Spring security will do that for you as you mentioned in manual but I mean the authentication in spring context. Sorry for misleading here.
It is OK that if user configuration sessionless, here I just want to make sure the authentication of context is clear before the session filter process(only if user forget to enable stateless policy). Like this SecurityContextHolder.getContext().setAuthentication(null);
Even if use forget to enable stateless, the SecurityContextHolder.getContext().setAuthentication(null);
will ensure the null authentication will be store to repo in below code.
finally {
SecurityContext contextAfterChainExecution = SecurityContextHolder
.getContext();
// Crucial removal of SecurityContextHolder contents - do this before anything
// else.
SecurityContextHolder.clearContext();
repo.saveContext(contextAfterChainExecution, holder.getRequest(),
holder.getResponse()); // clean auth before finally to make sure auth in context is null
request.removeAttribute(FILTER_APPLIED);
if (debug) {
logger.debug("SecurityContextHolder now cleared, as request processing completed");
}
}
...c/main/java/com/microsoft/azure/spring/autoconfigure/aad/AADAppRoleAuthenticationFilter.java
Outdated
Show resolved
Hide resolved
SecurityContextHolder.getContext().setAuthentication(authentication); | ||
} catch (BadJWTException ex) { | ||
log.warn("Invalid JWT: " + ex.getMessage()); | ||
throw new ServletException(ex); |
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.
better to add some message with cause here. And for BadJOSEException and JOSEException, should we put it together with BadJWTException ? as it all validation related.
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 added BadJWTException
as a separate catch block and log statement as this exception is thrown by com.microsoft.azure.spring.autoconfigure.aad.UserPrincipalManager
in case the time-related claims of the JWT are invalid:
- the JWT is expired
- or before the JWT is valid
I think this is a widespread type of exception that will occur regularly and is not something that should be logged with error
level but instead with warn
or even info
.
In contrast BadJOSEException
and JOSEException
are used if there are cryptographic errors (wrong signature, bad encryption) which should not occur during regular operation.
Regarding the log message: do you have any suggestion? In the described cases of expired tokens the result is: Invalid JWT: Expired JWT
or Invalid JWT: JWT before use time
.
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.
For log message for exception, my suggestion may looks like this throw new Exception("Invalid JWT: Expired xxxxx", ex);
which include some message to user, as well as the cause.
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 am not sure I understand what you mean. When this exception is catched, ex.getMessage()
will contain either Expired JWT
or JWT before use time
. From my point of view, there is no additional information I can provide in order to make more sense out of this exception. Or do you mean the fact that here throw new ServletException(ex);
no message is specified and only available in the causedBy?
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 mean if there are some message here, we can get the hint from log instead of debugging or something like that ex.getMessage()
to find the cause. It is not big issue here, just from the users view about the exception throw.
...icrosoft/azure/spring/autoconfigure/aad/AADAppRoleAuthenticationFilterAutoConfiguration.java
Outdated
Show resolved
Hide resolved
import org.springframework.validation.annotation.Validated; | ||
|
||
@Validated | ||
@ConfigurationProperties("azure.aad.app-role") |
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.
please follow the naming conversion as AADAuthenticationProperties, use azure.activedirectory
instead of azure.aad.app-role
.
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 property "namespace" azure.activedirectory
is already bound to com.microsoft.azure.spring.autoconfigure.aad.AADAuthenticationProperties
. I did not manage to use azure.activedirectory.app-role
for my new configuration without having side effects of AADAuthenticationProperties
being instantiated and beans being created automatically. As I didn't want to break the existing feature I've chosen this different property name.
Is there a way of doing it without the side effects I've described?
|
||
private static final Logger log = LoggerFactory.getLogger(AADAppRoleAuthenticationFilter.class); | ||
|
||
private static final String TOKEN_HEADER = "Authorization"; |
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.
Please use HttpHeaders.AUTHORIZATION
.
private static final JSONArray DEFAULT_ROLE_CLAIM = new JSONArray().appendElement("USER"); | ||
private static final String ROLE_PREFIX = "ROLE_"; | ||
|
||
private UserPrincipalManager principalManager; |
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.
final ?
final String authHeader = request.getHeader(TOKEN_HEADER); | ||
boolean cleanupRequired = false; | ||
|
||
if (authHeader != null && authHeader.startsWith(TOKEN_TYPE)) { |
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.
hasText instead of != null ?
@Configuration | ||
@ConditionalOnWebApplication | ||
@ConditionalOnProperty(prefix = "azure.activedirectory", value = {"client-id", "client-secret"}) | ||
@ConditionalOnProperty(prefix = "azure.activedirectory", value = {"client-id"}) |
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.
leverage below PROPERTY_PREFIX
?
this.aadAuthProps = aadAuthProps; | ||
this.explicitAudienceCheck = explicitAudienceCheck; | ||
if (explicitAudienceCheck) { | ||
//client-id for "normal" check |
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.
space after '//'
ResourceRetriever resourceRetriever, | ||
boolean explicitAudienceCheck) { | ||
this.aadAuthProps = aadAuthProps; | ||
this.explicitAudienceCheck = explicitAudienceCheck; |
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.
duplicated space after =
1efaa8e
to
3f23534
Compare
* Use available constants and util methods * remove double spaces / add spaces after comment
3f23534
to
f6b0832
Compare
Thank you very much for the code review. I've fixed all of the issues. |
…entication-filter
@wmitzel-airplus |
I like the idea of adding "Stateless" to the name of the filter. However, I'd prefer not to abbreviate "AppRole" to "Role" as the official feature in AAD is called "approle". If you search on the internet for the term "app role" you directly get to the documentation on how to add application roles to the manifest of your application registration. |
sounds good, please help to update the name. |
Rename the filter to reflect its stateless nature.
Thanks a lot but could u please following the convention use |
Thanks a lot for you contribution, very appreciate. Could you please help to add sample code for this scenario ? like this |
👍 |
Any idea on the ETA to make a maven release for this code in master ? |
Summary
This pull request implements a stateless authentication filter that uses
id_token
and theappRole
feature of AAD. Some details and motivation why this makes sense have been discussed in #494.Issue Type
Starter Names
Additional Information
I've created a new authentication filter which is similar to the already existing
AADAuthenticationFilter
. However, it does not useHttpSession
and thus can be used for stateless Spring backends. The basic JWT claims are checked by theUserPrincipalManager
. Additionally, theroles
scope is read and the roles are converted toGrantedAuthory
which are then attached to the principal.I've also added an example project which demonstrates the usage.