-
Notifications
You must be signed in to change notification settings - Fork 368
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
feat: Adding form_post support #509
Conversation
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.
Thank you for your hard work on this and the tests and sorry for the late review but it's a bit of a complicated review :)
I think we can improve this PR a lot - it looks like there is a lot of duplication between AddForm
and AddFragment
. I think we should have a general ResponseMode()
method instead of checking for ar.GetRequestForm().Get("response_mode") == "form_post"
. Since Form()
and Fragment
and Query
are almost all very similar I think we could simplify this also and avoid duplication.
One question is how response_mode: form_post
behaves when we're dealing with implicit flows. I couldn't find any guidance on this but to me it seems non-sensical to have implicit flows work together with form_post
as implicit applications are typically javascript SPAs or mobile apps where you can't POST to. Do you have any guidance or sources on this?
…the initial handler and avoiding duplicate code in setting query,fragment and form parameter in authorize response
No, I couldn't find any guidance around this. My thinking was if the response_mode is in the request process it accordingly otherwise process according to the default response mode for the flow. if it doesn't make sense to support |
@aeneasr Did you get a chance to review the latest changes? |
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.
Sorry for the slow review, my week was extremely busy. Thank you for the hard work, I have some more ideas for improvement!
So this is the spec for form post. In there it says:
Moreover, a non-normative request/response/request example they have in there does have I must say that I do not understand why server simply does not do the form POST directly to the callback, why they have this JavaScript doing it? Is this done just so that any cookies to the callback URL which browser has go together with this form POST? Not sure why that would be needed, but this seems to be the spec. I am not sure if it is really possible to talk about implicit/explicit flows here. I think from post is just its own beast. If implemented using the JavaScript post in the client, in fact it can work for SPAs as well if SPA implements a service worker which intercepts the POST (there are maybe other ways, too). I think that the whole idea of |
I think it should be allowed for client to specify which response modes it allows. We should maybe have |
authorize_request_handler.go
Outdated
@@ -210,6 +210,23 @@ func (f *Fosite) validateResponseTypes(r *http.Request, request *AuthorizeReques | |||
request.ResponseTypes = responseTypes | |||
return nil | |||
} | |||
func (f *Fosite) validateResponseMode(r *http.Request, request *AuthorizeRequest) error { |
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 validation function should also respect security considerations from here:
There are security implications to encoding response values in the query string. The HTTP Referer header includes query parameters, and so any values encoded in query parameters will leak to third parties. Thus, while it is safe to encode an Authorization Code as a query parameter when using a Confidential Client (because it can't be used without the Client Secret, which third parties won't have), more sensitive information such as Access Tokens and ID Tokens MUST NOT be encoded in the query string. In no case should a set of Authorization Response parameters whose default Response Mode is the fragment encoding be encoded using the query encoding.
Also see description of combinations.
Based on that it is not really true that all combinations are possible. For example, if default uses fragment, then query cannot be used. Moreover, for errors, it seems that you can use always whichever the client requested.
So this is really that if by default query string is used, you can request that fragment is used. But not the opposite.
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.
Given that for error any mode can be used, then maybe checking for valid combinations cannot be done here but in write handler?
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.
Nice catch @mitar - so we should check for response_type
and response_mode
to define the output. It is also true that for errors, this check has to be disabled. But I also think that this has to be handled in the writer, because we first need to execute the "adapters" to figure out the default response mode and only then should we make assumptions about the default state.
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.
Added the check for using query
while the default mode is fragment
and returns an error in the autheorize_response_writer.go
.
authorize_helper.go
Outdated
<body onload="javascript:document.forms[0].submit()"> | ||
<form method="post" action="{{ .RedirURL }}"> | ||
{{ range $key,$value := .Parameters }} | ||
<input type="hidden" name="{{$key}}" value="{{index $value 0}}"/> |
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.
Just for completeness (Because other encoding methods like query and fragment supports that), can you loop here over the value (which is a slice) and repeat <input
with same key
multiple times? This should never happen. But you never know if somebody directly modifies Parameters
to add that (given that you defined it as url.Values
which allows repeated values for same field).
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.
Also, HTML-escape key and value 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.
And add test 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.
html/template
escapes all variables unless told otherwise.
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.
Now the template supports adding multiple values. Also added test case for illegal character encoding.
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.
Wow @ajanthan this is a really good PR! I think I found a couple more question marks and suggestions for improvement but we're very close to having this merged. Thank you for all the hard work, this is superb :)
handler/openid/flow_hybrid.go
Outdated
@@ -54,7 +54,9 @@ func (c *OpenIDConnectHybridHandler) HandleAuthorizeEndpointRequest(ctx context. | |||
if !(ar.GetResponseTypes().Matches("token", "id_token", "code") || ar.GetResponseTypes().Matches("token", "code") || ar.GetResponseTypes().Matches("id_token", "code")) { | |||
return nil | |||
} | |||
|
|||
if ar.GetResponseMode() == fosite.ResponseModeNone { |
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 should not run if fosite.ResponseModeQuery
and maybe also not with the FormPost
one. Please confirm @mitar . Also use ar.SetDefaultResponseMode
.
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.
added ar.SetDefaultResponseMode
. Using query
in this flow is already restricted but do we want to restrict form_post
? According to the spec, it seems not necessary to restrict form_post
as it more secure than query
and fragment
.
}, | ||
}, | ||
{ | ||
description: "Default responseMode 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.
We should add a test for the concrete response modes and their expected results (e.g. error)
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.
Agree. Added error test case in authorize_response_writer_test.go
handler/openid/flow_implicit.go
Outdated
@@ -51,6 +51,10 @@ func (c *OpenIDConnectImplicitHandler) HandleAuthorizeEndpointRequest(ctx contex | |||
return nil | |||
} | |||
|
|||
if ar.GetResponseMode() == fosite.ResponseModeNone { |
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 should not run if fosite.ResponseModeQuery
and maybe also not with the FormPost
one. Please confirm @mitar . Also use ar.SetDefaultResponseMode
.
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.
Added ar.SetDefaultResponseMode
. Using query
in this flow is already restricted but do we want to restrict form_post
?
handler/openid/flow_implicit_test.go
Outdated
}, | ||
}, | ||
{ | ||
description: "default responseMode 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.
Same as for the others, check for other response modes with expected success or errors.
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.
+1 but it doesn't make sense to check success/error for other modes because the validation happens in authorize_response_writer
. I have added a test case there.
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.
Also added integration test to cover a various combination of response types and modes
}{ | ||
{ | ||
description: "implicit grant test with form_post", | ||
responseType: "token", |
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.
Should this be allowed @mitar ?
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.
Changed it to id_token token
.
Quick update from my side - this looks great! My plan would be to introduce an automated test pipeline using https://gitlab.com/openid/conformance-suite to ensure that all conformance tests pass as well. Unfortunately, this is quite the amount of work so it might need a bit of time before this is implemented. I have some other stuff to finish but this is next on my list afterwards! |
I started working on this and already gotten pretty far (the set up works and the first tests are passing). I still have to figure out how to do this using automated tools (maybe I'll just use Cypress). But my feeling says this should be workable pretty soon! |
I like Cypress. BTW, you can also check here how they did it. |
Ah nice, I see how they solved the browser interaction. I was looking for this all night yesterday :D |
I am making good progress here with the automation. Looks like almost everything is working as expected. Bad news is, the recent clean up of I'll try to get all the tests working, then I will do a rewrite to this fosite branch with hydra and to check if |
Nice!
Oh, what? Maybe open an issue with more information about this. This is surprising. |
BTW, have you disabled some checks? Because I would assume #409 would fail the testing. |
Certification works in such a way that you choose certain profiles. Also, not everything has to pass (e.g. none signature is not required to be implemented). So "orange" indicators are ok most of the times, black/red indicators are failures. |
Ok so as far as I can tell the only thing that doesn't work is properly extracting the response_mode from the request object URL or JWT if one is set. We had this bug actually for |
Please do not be confused by the random pushes, I am trying to debug something... The PR itself is good to go IMO |
form: url.Values{"scope": {"openid"}, "response_type": {"code"}, "response_mode": {"none"}, "request": {validRequestObject}}, | ||
client: &DefaultOpenIDConnectClient{JSONWebKeys: jwks, RequestObjectSigningAlgorithm: "RS256"}, | ||
// The values from form are overwritten by the request object. | ||
expectForm: url.Values{"response_type": {"token"}, "response_mode": {"post_form"}, "scope": {"foo openid"}, "request": {validRequestObject}, "foo": {"bar"}, "baz": {"baz"}}, |
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.
Any particular reason why this file contains post_form
and not correct form_post
? What is this testing? Why it is not failing?
So changelog currently reads:
I do not think this is correct. This PR did not introduce |
Also, it seems this PR introduced a backwards incompatibility into fosite: if before this change, clients were providing I see two solutions:
I think this maybe speaks more about fosite approach to backwards compatibility. I think it is great that compiler helps you point out what has to change to get things upgraded and I would prefer the first approach above. With a good message in the changelog I would know to expect this and would decide to do the upgrade with enough time budgeted to do the code change. I think the introduction of a new interface makes things look like backwards compatible but in fact it was at the end much more work to discover the problem, get it reported internally, fix it, review the fix, and now me reporting it back here. I know that I proposed a new interface but I was doing that based on prior practice in fosite. I think, @aeneasr, maybe this practice should be changed and we should embrace changing interfaces more instead of introducing new interfaces? |
Related issue
#470
Proposed changes
Introduces Form attribute to AuthorizeResponse
Populates Form attributes in the relevant grant type handlers if the
response_mode
=form_post
presents in requestWrite the response as defined in the spec if the Form attributes are not empty
Checklist
vulnerability. If this pull request addresses a security vulnerability, I
confirm that I got green light (please contact
[email protected]) from the maintainers to push
the changes.
Further comments
I haven't added support for
response_mode
=query
orfragment
or any other