-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Authorization Endpoint filter for Authorization Code flow #77
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
Authorization Endpoint filter for Authorization Code flow #77
Conversation
|
||
@Override | ||
protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException { | ||
return !authorizationEndpiontMatcher.matches(request); |
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.
Minor typo in authorizationEndpiontMatcher
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.
Fixed. Thanks.
if (client==null) | ||
throw new OAuth2AuthorizationException(CLIENT_ID_NOT_FOUND_ERROR); | ||
|
||
boolean isAuthoirzationGrantAllowed = Stream.of(client.getAuthorizationGrantTypes()) |
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.
Minor typo in isAuthoirzationGrantAllowed
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.
Fixed. Thanks.
if (errorCode.equals(OAuth2ErrorCodes.ACCESS_DENIED)) | ||
errorStatus=HttpStatus.FORBIDDEN.value(); | ||
else errorStatus=HttpStatus.INTERNAL_SERVER_ERROR.value(); | ||
response.sendError(errorStatus, authorizationError.getErrorCode()+":"+authorizationError.getDescription()); |
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 find not using curly braces in "if" statements to be very bug prone. I'd suggest introducing them here and in other other locations in this PR. I've noticed this in numerous locations within 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.
@jgrandja do you have any suggestions on which coding guidelines we should follow? I found this document in the spring-framework repository. But it does say its still a work-in-progress
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.
@pgerhard Take a look at the Getting Started, specifically:
Submitted work via pull requests should follow the same coding style/conventions and adopt the same or similar design patterns that have been established in Spring Security’s OAuth 2.0 support
Essentially, we are looking for the same coding style/patterns that are implemented in the OAuth 2.0 codebase in Spring Security 5.x. Consistency is key, so the Authorization Server code should align as well.
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 for the updates @paurav-munshi. Please see my review comments.
Also, I noticed quite a few issues with code formatting and style, eg. missing curly braces in if
blocks, missing space between if
and (
, etc. Again, please review Getting Started, as it's very imprortant that the code style/conventions are adopted from what exists today in Spring Security 5 OAuth 2.0 support classes.
|
||
private static final String DEFAULT_ENDPOINT = "/oauth2/authorize"; | ||
|
||
private static final OAuth2Error CLIENT_ID_ABSENT_ERROR = new OAuth2Error(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2AuthorizationServerMessages.REQUEST_MISSING_CLIENT_ID, null); |
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 remove these constants as well as OAuth2AuthorizationServerMessages
. We only need to set OAuth2Error.errorCode
at this point - messages/localization will be implemented at a much later point.
As well, let's create OAuth2Error
at the point in code where it's required instead of reusing the constant instances. We can optimize object creation later, if need be.
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.
Fixed in PR. Thanks.
|
||
|
||
public OAuth2AuthorizationEndpointFilter() { | ||
authorizationEndpiontMatcher = new AntPathRequestMatcher(DEFAULT_ENDPOINT); |
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.
Class members should be prefixed with this
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.
Fixed in PR. Thanks.
RegisteredClient client = null; | ||
OAuth2AuthorizationRequest authorizationRequest = null; | ||
OAuth2Authorization authorization = null; | ||
|
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 would have expected to see the use of authorizationEndpointMatcher
as the very first call here. However, it's not being used at all. Please review 4.1.1. Authorization Request. At a minimum, there should be a RequestMatcher
that matches on a valid Authorization Request. A valid authorization request should at least match on the configured endpoint path (using AntPathRequestMatcher
) and the required parameters response_type=code
, client_id=*
. This is a common pattern used throughout Spring Security, where Filter
use a RequestMatcher
to filter out request or process them.
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 have used the RequestMatcher in the shouldNotFilter method. That method is called by OncePerRequestFilter.doFilter method effectively calling it before doFilterInternal. I thought it is much more natural to filter this way for checking endpoint. And I also think that validity of the query parameters is not supposed to be done in RequestMatcher because it is only responsible to check if filter should be invoked for that request or not. Once invoked, I do not think RequestMatcher should validate the request and therefore I have those validations in separate methods. Let me know if it is not to be done in that way.
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 have used the RequestMatcher in the shouldNotFilter method
Sorry missed that.
I also think that validity of the query parameters is not supposed to be done in RequestMatcher
A valid Authorization Request requires response_type=code
and client_id != empty
so both of these need to be applied in 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.
A valid Authorization Request requires response_type=code and client_id != empty so both of these need to be applied in RequestMatcher
So here is where my confusion lies. RequestMatcher
by its nature, it seems, is supposed to decide whether request is supported by that filter or not. So if the RequestMatcher
does not find a match it will silently allow the request to proceed to FilterChain
. But if we look at first line of 4.1.2.1 Error Response, it says :-
If the request fails due to a missing, invalid, or mismatching
redirection URI, or if the client identifier is missing or invalid,
the authorization server SHOULD inform the resource owner of the
error
So if we are putting response_type=code and client_id != empty
in RequestMatcher
it will not send the error response back to resource owner's user-agent. So how are supposed to handle this scenario if we want to perform this validation in RequestMatcher
. I think that in future if Implicit grant is going to be implemented using a different filter then we can add response_type=code
in RequestMatcher
but I would still not think of validating client id in ReqeustMatcher
.
Can you please provide some more light here ?
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.
At a minimum I have added the check for response_type in RequestMatcher. So if the response_type is blank or if its not code then this filter will not be invoked and filter chain will proceeded with. Tests have been added to check the same.
Can you please review the PR ?
OAuth2Authorization authorization = null; | ||
|
||
try { | ||
checkUserAuthenticated(); |
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.
After the RequestMatcher
determines to process the request, it should delegate to authorizationRequestConverter
to convert to OAuth2AuthorizationRequest
. Then the OAuth2AuthorizationRequest
should be used to perform the required validations as the next step.
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 see my comment below. There I have tried to explain why I have used request instead of OAuth2AuthorizationRequest.
client = fetchRegisteredClient(request); | ||
|
||
authorizationRequest = authorizationRequestConverter.convert(request); | ||
validateAuthorizationRequest(request, client); |
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.
Why not pass authorizationRequest
here instead of request
?
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.
Actually, when we build authorizationRequest
, it automatically populates authorization code when creating the Builder using the OAuth2AuthorizationRequest.authorizationCode
method. So even if we get blank response_type
from request
, authorizationRequest
would still have response type as code. So instead of doing one validation on request
and other validations on authorizationRequest
, it felt more obvious to do all validations on request
.
I had to separate out client id validation to be done in fetchRegisteredClient
because other validation like redirect_uri
validations or scope
validations(future story) are dependent on registered client configuration. Otherwise I earlier planned to also perform client id validation in validateAuthorizationRequest
method. Infact I was just thinking, I can move
if (client==null) {
throw new OAuth2AuthorizationException(new OAuth2Error(OAuth2ErrorCodes.ACCESS_DENIED));
}
boolean isAuthorizationGrantAllowed = Stream.of(client.getAuthorizationGrantTypes())
.anyMatch(grantType -> grantType.contains(AuthorizationGrantType.AUTHORIZATION_CODE));
if (!isAuthorizationGrantAllowed) {
throw new OAuth2AuthorizationException(new OAuth2Error(OAuth2ErrorCodes.ACCESS_DENIED));
}
from fetchRegisteredClient to validateAuthorizationRequest method so that there is only one universal validation method.
Hope if makes sense. Please suggest if you think there is a better approach.
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.
So even if we get blank response_type from request
As mentioned in other comment, a valid Authorization Request requires response_type=code
so this should be applied in RequestMatcher
. When OAuth2AuthorizationRequest.authorizationCode()
is called, we know that response_type=code
(based on match) or else authorizationRequestConverter
would never be called.
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.
Modified the validation method to validate on OAuth2AuthorizationRequest
instead of request
. Please review. Thanks.
OAuth2Authorization authorization = OAuth2Authorization.createBuilder() | ||
.clientId(authorizationRequest.getClientId()) | ||
.addAttribute(OAuth2ParameterNames.CODE, code) | ||
.addAttribute(OAuth2Authorization.ISSUED_AT, Instant.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.
Please remove ISSUED_AT
, CODE_USED
and SCOPE
. These will be added in a later story. We only need to store CODE
and OAuth2AuthorizationRequest
attributes
as part of this story.
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.
Fixed in PR. Thanks.
|
||
} | ||
|
||
protected OAuth2Authorization buildOAuth2Authorization(RegisteredClient client, |
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.
We typically encapsulate in the initial design and only expose when needed. Please change to private
for all exposed methods.
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.
Fixed in PR. Thanks.
this.authorizationRequestConverter = authorizationRequestConverter; | ||
} | ||
|
||
public RegisteredClientRepository getRegisteredClientRepository() { |
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 remove getters. The convention is to provide final
setters for optional attributes and required attributes are provided via constructors. See OAuth2AuthorizationRequestRedirectFilter
and DefaultOAuth2AuthorizedClientManager
for example
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.
Fixed in PR. Thanks.
* @author Paurav Munshi | ||
* @since 0.0.1 | ||
*/ | ||
public class AuthorizationCodeKeyGenerator implements StringKeyGenerator { |
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 remove this and instead leverage the same that is used in DefaultOAuth2AuthorizationRequestResolver.stateGenerator
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.
That state generator uses Base64StringKeyGenerator internally. Earlier I looked at that option but because Base64StringKeyGenerator uses SecureRandom internally I thought its better to use something which explicitly guarantees of uniqueness. Therefore I used a generator based on UUID. But it seems UUID also uses SecureRandom internally for type 4 UUID. So I would change my code to use Base64StringKeyGenerator.
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.
Fixed in PR. Thanks.
} | ||
|
||
@Test | ||
public void testFilterRedirectsWithCodeOnValidReq() throws Exception { |
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 a naming convention for test methods. For an example, please see DefaultOAuth2AuthorizationRequestResolverTests
and OAuth2AuthorizationCodeGrantFilterTests
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.
Fixed in PR. Thanks.
I have not found a definitive guide for code formatting but I mainly followed this document and this document. And I looked on other existing classes from Spring Security OAuth Core for validating my understanding of code formatting. I have applied following changes which are now incorporated in PR :-
Do let me know if you find more discrepancies. Thanks. |
@paurav-munshi I don't think you should follow this particular code formatting:
Reference files: OAuth2AuthorizationRequestRedirectFilter.java and HttpSessionOAuth2AuthorizationRequestRepository.java |
Actually I saw class OncePerRequestFilter from which I followed the styling and also I came across link https://github.com/spring-projects/spring-framework/wiki/Code-Style which states
|
Functionality Covered ------------------------- - Validation of client id & response type - Generate authorization code - Save Authorization - Redirect to client redirect uri - Send error in http response - Send error as query params in redirect uri Validations covered ------------------- - Client Id should be mandatory - Client Id should be registered - Client Id should be configured with Authorization grant type - Response type is mandaotry - Response type should be code
- On successfull redirect code was not being sent in the Redirect URL - validations methods were private, so made them protected
- Adding key constants for issues time and code used flag in OAuth2Authorization - Changing error code when Authorization grant is not supported by client - Change in error method for Unauthorized client - Corrected the validation for AuthorizationGrantType check - Adding attributes Issue time and Code use flag in attributes map - Adding state to redirect uri for both success and failure - Changing method name from get to set as it was wrongly used - Adding a basic test class which will be enhanced more
- Added a UUID based Authorization Code generator - Added an Ant Path request matcher to check endpoint validity - Added a new OAuth2 authorization request converter - Use DefaultRedirectStrategy from Spring - Added a check for user authentication present and authenticated - Added check for respone type parameter validity in request - Added check for redirect uri validity in request - Replaced StringBuilder with UriComponentsBuilder - Added JUnit tests for validation in section 4.1.1 of the RFC specs - Added a class which will be responsible for holding sever level messages as constants
- Added space after commans where ever applicable - Removed trailing whilte spaces - Added white space after if condition - Added headers before package declaration
- Removed Constants from the filter class - Remove ServerMessages file - Removed AuthorizationCodeKeyGenerator instead used Base64StringKeyGenerator - Removed getter methods - Only provide setters for default and options fields - Added mandatory fields to ctor - Add braces to single statement if conditions - Adopted method declaration seq to be ctor, setter, overridden methods and private methods - Used this keyword where ever access instance members - Added tests for ctor and setters - Renamed tests to methodName-When-Then format
- Removed response type checks from validation method and added to RequestMatcher - Changed the validation method to accept OAuth2AuthorizationRequest instead of HttpServletRequest - Changed the response_type tests to expect proceeding with filter chain instead of getting an error
df6f818
to
7a3e658
Compare
- Updated the api to build OAuth2Authorization in endpoint filter - Changed the api to create the builder for OAuth2Authorization - Changed the api to add attributes to OAuth2Authorization - Changed the key of attribute with which authorization code was added - Set up test user to be returned by mock object
|
||
/** | ||
* @author Joe Grandja | ||
* @author Paurav Munshi | ||
* @since 0.0.1 |
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.
Javadoc to be added
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 thinking of adding Javadoc once everything is finalized
*/ | ||
public class OAuth2AuthorizationEndpointFilter extends OncePerRequestFilter { | ||
private Converter<HttpServletRequest, OAuth2AuthorizationRequest> authorizationRequestConverter; | ||
|
||
private static final String DEFAULT_ENDPOINT = "/oauth2/authorize"; |
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.
This should probably be named DEFAULT_AUTHORIZATION_ENDPOINT_URI
@Override | ||
protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException { | ||
boolean pathMatch = this.authorizationEndpointMatcher.matches(request); | ||
String responseType = request.getParameter(OAuth2ParameterNames.RESPONSE_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.
According to the spec of Error response for Authorization request, the server should respond with invalid_request
if missing a required parameter or unsupported_response_type
for an invalid response type. So ideally the check for response_type
should be added at the place of validating the auth request as response_type
is a required param according to the spec of Authorization request.
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.
That was how it was. I, initially, had the request validation also perform response_type check. But @jgrandja suggested to perform this check at the time of verifying if filter is applicable or not. Please look at this comment. And I think this is also a good approach because response_type
could also be token
and in that case a different Filter
will handle the request and I think for erroneous response_type
we can have an separate error filter. I am fine with both the options, but just wanted to point out that this was kept this way deliberately.
OAuth2Authorization authorization = OAuth2Authorization.withRegisteredClient(client) | ||
.principalName(auth.getPrincipal().toString()) | ||
.attribute(TokenType.AUTHORIZATION_CODE.getValue(), code) | ||
.attributes(attirbutesMap -> attirbutesMap.putAll(authorizationRequest.getAttributes())) |
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.
Minor typo in attirbutesMap
Thanks for all the updates @paurav-munshi. I decided to add a polish commit in order to get this merged, as I'm targeting to complete the first iteration of Please take a look at the changes applied in the polish commit and let me know if you have any questions. I would also recommend doing a comparison of |
Fixes #66
Functionality Covered
Validations covered