Skip to content
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

Merged
merged 25 commits into from
Nov 9, 2020
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
27ea33c
feat: Adding form_post support
ajanthan Oct 9, 2020
54a9d50
refactor: introducing responseMode enum, setting responseMode during …
ajanthan Oct 14, 2020
d359581
Merge branch 'master' into feat-formpost
ajanthan Oct 14, 2020
ec3e000
refactor: Reusing response mode enum in error handling
ajanthan Oct 20, 2020
0ebd04d
fix: bulding authrize error message according to responseMode
ajanthan Oct 21, 2020
f6dea63
refactor: Moving ParseFormPostResponse test helper to internal package
ajanthan Oct 28, 2020
ded62be
refactor: Renaming ResponseModeNone
ajanthan Oct 29, 2020
c3c5ea7
refactor: intoducing function to do the fragment encoding
ajanthan Oct 29, 2020
e92ec55
refactor: Making formPostHTLML Template configurable
ajanthan Oct 29, 2020
56234b4
Apply suggestions from code review
aeneasr Oct 29, 2020
10b2f25
Merge branch 'master' into feat-formpost
aeneasr Oct 29, 2020
d7c0743
Adding special characters test case
ajanthan Oct 30, 2020
b8db770
Adding ability to client to specify which response modes it allows
ajanthan Oct 30, 2020
9ee157e
Validating response_mode for insecure mode
ajanthan Nov 1, 2020
9ed9df8
Adding test cases for none default response modes
ajanthan Nov 1, 2020
8676b8b
Merge branch 'master' into feat-formpost
aeneasr Nov 4, 2020
eee5473
fix: respect request object
aeneasr Nov 6, 2020
69b2208
u
aeneasr Nov 6, 2020
4b29d15
u
aeneasr Nov 6, 2020
fefcdeb
u
aeneasr Nov 6, 2020
a8be9f2
u
aeneasr Nov 6, 2020
da08c73
u
aeneasr Nov 6, 2020
3e6a1bd
u
aeneasr Nov 6, 2020
6b741ef
u
aeneasr Nov 9, 2020
b812014
u
aeneasr Nov 9, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions authorize_error.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,6 @@ import (
"encoding/json"
"fmt"
"net/http"

"github.com/pkg/errors"
)

func (f *Fosite) WriteAuthorizeError(rw http.ResponseWriter, ar AuthorizeRequester, err error) {
Expand Down Expand Up @@ -66,7 +64,11 @@ func (f *Fosite) WriteAuthorizeError(rw http.ResponseWriter, ar AuthorizeRequest
query.Add("state", ar.GetState())

var redirectURIString string
if !(len(ar.GetResponseTypes()) == 0 || ar.GetResponseTypes().ExactOne("code")) && !errors.Is(err, ErrUnsupportedResponseType) {
if ar.GetResponseMode() == ResponseModePost {
rw.Header().Add("Content-Type", "text/html;charset=UTF-8")
WriteAuthorizeFormPostResponse(redirectURI.String(), query, rw)
return
} else if ar.GetResponseMode() == ResponseModeFragment {
redirectURIString = redirectURI.String() + "#" + query.Encode()
} else {
for key, values := range redirectURI.Query() {
Expand Down
29 changes: 29 additions & 0 deletions authorize_error_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ func TestWriteAuthorizeError(t *testing.T) {
req.EXPECT().GetRedirectURI().Return(copyUrl(purls[0]))
req.EXPECT().GetState().Return("foostate")
req.EXPECT().GetResponseTypes().MaxTimes(2).Return(Arguments([]string{"code"}))
req.EXPECT().GetResponseMode().Return(ResponseModeQuery).Times(2)
rw.EXPECT().Header().Times(3).Return(header)
rw.EXPECT().WriteHeader(http.StatusFound)
},
Expand All @@ -106,6 +107,7 @@ func TestWriteAuthorizeError(t *testing.T) {
req.EXPECT().GetRedirectURI().Return(copyUrl(purls[0]))
req.EXPECT().GetState().Return("foostate")
req.EXPECT().GetResponseTypes().MaxTimes(2).Return(Arguments([]string{"code"}))
req.EXPECT().GetResponseMode().Return(ResponseModeNone).Times(2)
rw.EXPECT().Header().Times(3).Return(header)
rw.EXPECT().WriteHeader(http.StatusFound)
},
Expand All @@ -124,6 +126,7 @@ func TestWriteAuthorizeError(t *testing.T) {
req.EXPECT().GetRedirectURI().Return(copyUrl(purls[1]))
req.EXPECT().GetState().Return("foostate")
req.EXPECT().GetResponseTypes().MaxTimes(2).Return(Arguments([]string{"code"}))
req.EXPECT().GetResponseMode().Return(ResponseModeQuery).Times(2)
rw.EXPECT().Header().Times(3).Return(header)
rw.EXPECT().WriteHeader(http.StatusFound)
},
Expand All @@ -142,6 +145,7 @@ func TestWriteAuthorizeError(t *testing.T) {
req.EXPECT().GetRedirectURI().Return(copyUrl(purls[1]))
req.EXPECT().GetState().Return("foostate")
req.EXPECT().GetResponseTypes().MaxTimes(2).Return(Arguments([]string{"foobar"}))
req.EXPECT().GetResponseMode().Return(ResponseModeFragment).Times(2)
rw.EXPECT().Header().Times(3).Return(header)
rw.EXPECT().WriteHeader(http.StatusFound)
},
Expand All @@ -160,6 +164,7 @@ func TestWriteAuthorizeError(t *testing.T) {
req.EXPECT().GetRedirectURI().Return(copyUrl(purls[0]))
req.EXPECT().GetState().Return("foostate")
req.EXPECT().GetResponseTypes().MaxTimes(2).Return(Arguments([]string{"token"}))
req.EXPECT().GetResponseMode().Return(ResponseModeFragment).Times(2)
rw.EXPECT().Header().Times(3).Return(header)
rw.EXPECT().WriteHeader(http.StatusFound)
},
Expand All @@ -178,6 +183,7 @@ func TestWriteAuthorizeError(t *testing.T) {
req.EXPECT().GetRedirectURI().Return(copyUrl(purls[1]))
req.EXPECT().GetState().Return("foostate")
req.EXPECT().GetResponseTypes().MaxTimes(2).Return(Arguments([]string{"token"}))
req.EXPECT().GetResponseMode().Return(ResponseModeFragment).Times(2)
rw.EXPECT().Header().Times(3).Return(header)
rw.EXPECT().WriteHeader(http.StatusFound)
},
Expand All @@ -196,6 +202,7 @@ func TestWriteAuthorizeError(t *testing.T) {
req.EXPECT().GetRedirectURI().Return(copyUrl(purls[0]))
req.EXPECT().GetState().Return("foostate")
req.EXPECT().GetResponseTypes().MaxTimes(2).Return(Arguments([]string{"code", "token"}))
req.EXPECT().GetResponseMode().Return(ResponseModeFragment).Times(2)
rw.EXPECT().Header().Times(3).Return(header)
rw.EXPECT().WriteHeader(http.StatusFound)
},
Expand All @@ -214,6 +221,7 @@ func TestWriteAuthorizeError(t *testing.T) {
req.EXPECT().GetRedirectURI().Return(copyUrl(purls[1]))
req.EXPECT().GetState().Return("foostate")
req.EXPECT().GetResponseTypes().MaxTimes(2).Return(Arguments([]string{"code", "token"}))
req.EXPECT().GetResponseMode().Return(ResponseModeFragment).Times(2)
rw.EXPECT().Header().Times(3).Return(header)
rw.EXPECT().WriteHeader(http.StatusFound)
},
Expand All @@ -233,6 +241,7 @@ func TestWriteAuthorizeError(t *testing.T) {
req.EXPECT().GetRedirectURI().Return(copyUrl(purls[1]))
req.EXPECT().GetState().Return("foostate")
req.EXPECT().GetResponseTypes().MaxTimes(2).Return(Arguments([]string{"code", "token"}))
req.EXPECT().GetResponseMode().Return(ResponseModeFragment).Times(2)
rw.EXPECT().Header().Times(3).Return(header)
rw.EXPECT().WriteHeader(http.StatusFound)
},
Expand All @@ -252,6 +261,7 @@ func TestWriteAuthorizeError(t *testing.T) {
req.EXPECT().GetRedirectURI().Return(copyUrl(purls[1]))
req.EXPECT().GetState().Return("foostate")
req.EXPECT().GetResponseTypes().MaxTimes(2).Return(Arguments([]string{"id_token"}))
req.EXPECT().GetResponseMode().Return(ResponseModeFragment).Times(2)
rw.EXPECT().Header().Times(3).Return(header)
rw.EXPECT().WriteHeader(http.StatusFound)
},
Expand All @@ -271,6 +281,7 @@ func TestWriteAuthorizeError(t *testing.T) {
req.EXPECT().GetRedirectURI().Return(copyUrl(purls[1]))
req.EXPECT().GetState().Return("foostate")
req.EXPECT().GetResponseTypes().MaxTimes(2).Return(Arguments([]string{"token"}))
req.EXPECT().GetResponseMode().Return(ResponseModeFragment).Times(2)
rw.EXPECT().Header().Times(3).Return(header)
rw.EXPECT().WriteHeader(http.StatusFound)
},
Expand All @@ -282,6 +293,24 @@ func TestWriteAuthorizeError(t *testing.T) {
assert.Equal(t, "no-cache", header.Get("Pragma"))
},
},
{
debug: true,
err: ErrInvalidRequest.WithDebug("with-debug"),
mock: func(rw *MockResponseWriter, req *MockAuthorizeRequester) {
req.EXPECT().IsRedirectURIValid().Return(true)
req.EXPECT().GetRedirectURI().Return(copyUrl(purls[1]))
req.EXPECT().GetState().Return("foostate")
req.EXPECT().GetResponseTypes().MaxTimes(2).Return(Arguments([]string{"token"}))
req.EXPECT().GetResponseMode().Return(ResponseModePost).Times(1)
rw.EXPECT().Header().Times(3).Return(header)
rw.EXPECT().Write(gomock.Any()).AnyTimes()
},
checkHeader: func(t *testing.T, k int) {
assert.Equal(t, "no-store", header.Get("Cache-Control"))
assert.Equal(t, "no-cache", header.Get("Pragma"))
assert.Equal(t, "text/html;charset=UTF-8", header.Get("Content-Type"))
},
},
} {
t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) {
oauth2 := &Fosite{
Expand Down
122 changes: 122 additions & 0 deletions authorize_helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,34 @@
package fosite

import (
"html/template"
"io"
"net/url"
"regexp"
"strconv"
"strings"
"time"

"golang.org/x/net/html"
goauth "golang.org/x/oauth2"

"github.com/asaskevich/govalidator"
"github.com/pkg/errors"
)

var formPostTemplate = template.Must(template.New("form_post").Parse(`<html>
<head>
<title>Submit This Form</title>
</head>
<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}}"/>
Copy link
Contributor

@mitar mitar Oct 24, 2020

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).

Copy link
Contributor

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.

Copy link
Contributor

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.

Copy link
Member

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.

Copy link
Contributor Author

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.

{{ end }}
</form>
</body>
</html>`))

// MatchRedirectURIWithClientRedirectURIs if the given uri is a registered redirect uri. Does not perform
// uri validation.
//
Expand Down Expand Up @@ -182,3 +202,105 @@ func IsLocalhost(redirectURI *url.URL) bool {
hn := redirectURI.Hostname()
return strings.HasSuffix(hn, ".localhost") || hn == "127.0.0.1" || hn == "::1" || hn == "localhost"
}

func WriteAuthorizeFormPostResponse(redirectURL string, parameters url.Values, rw io.Writer) {
_ = formPostTemplate.Execute(rw, struct {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the error should at least be logged somehow. Maybe return it and log it in the caller?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The caller of this method is WriteAuthorizeError and WriteAuthorizeResponse. None of them return an error or have log. Any idea how this error can be handled? Maybe panicking from here?

RedirURL string
Parameters url.Values
}{
RedirURL: redirectURL,
Parameters: parameters,
})
}
func ParseFormPostResponse(redirectURL string, resp io.ReadCloser) (authorizationCode, stateFromServer, iDToken string, token goauth.Token, rFC6749Error RFC6749Error, err error) {

token = goauth.Token{}
rFC6749Error = RFC6749Error{}

doc, err := html.Parse(resp)
if err != nil {
return "", "", "", token, rFC6749Error, err
}
//doc>html>body
body := findBody(doc.FirstChild.FirstChild)
if body.Data != "body" {
return "", "", "", token, rFC6749Error, errors.New("Malformed html")
}
htmlEvent := body.Attr[0].Key
if htmlEvent != "onload" {
return "", "", "", token, rFC6749Error, errors.New("onload event is missing")
}
onLoadFunc := body.Attr[0].Val
if onLoadFunc != "javascript:document.forms[0].submit()" {
return "", "", "", token, rFC6749Error, errors.New("onload function is missing")
}
form := getNextNoneTextNode(body.FirstChild)
if form.Data != "form" {
return "", "", "", token, rFC6749Error, errors.New("html form is missing")
}
for _, attr := range form.Attr {
if attr.Key == "method" {
if attr.Val != "post" {
return "", "", "", token, rFC6749Error, errors.New("html form post method is missing")
}
} else {
if attr.Val != redirectURL {
return "", "", "", token, rFC6749Error, errors.New("html form post url is wrong")
}
}
}

for node := getNextNoneTextNode(form.FirstChild); node != nil; node = getNextNoneTextNode(node.NextSibling) {
var k, v string
for _, attr := range node.Attr {
if attr.Key == "name" {
k = attr.Val
} else if attr.Key == "value" {
v = attr.Val
}

}
switch k {
case "state":
stateFromServer = v
case "code":
authorizationCode = v
case "expires_in":
expires, err := strconv.Atoi(v)
if err != nil {
return "", "", "", token, rFC6749Error, err
}
token.Expiry = time.Now().UTC().Add(time.Duration(expires) * time.Second)
case "access_token":
token.AccessToken = v
case "token_type":
token.TokenType = v
case "refresh_token":
token.RefreshToken = v
case "error":
rFC6749Error.Name = v
case "error_description":
rFC6749Error.Description = v
case "id_token":
iDToken = v
}
}
return
}

func getNextNoneTextNode(node *html.Node) *html.Node {
nextNode := node.NextSibling
if nextNode != nil && nextNode.Type == html.TextNode {
nextNode = getNextNoneTextNode(node.NextSibling)
}
return nextNode
}
func findBody(node *html.Node) *html.Node {
if node != nil {
if node.Data == "body" {
return node
}
return findBody(node.NextSibling)
}
return nil
}
13 changes: 13 additions & 0 deletions authorize_helper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
package fosite

import (
"bytes"
"io/ioutil"
"net/url"
"testing"

Expand Down Expand Up @@ -257,6 +259,17 @@ func TestIsRedirectURISecure(t *testing.T) {
}
}

func TestWriteAuthorizeFormPostResponse(t *testing.T) {
var responseBuffer bytes.Buffer
redirectURL := "https://localhost:8080/cb"
parameters := url.Values{"code": {"lshr755nsg39fgur"}, "state": {"924659540232"}}
WriteAuthorizeFormPostResponse(redirectURL, parameters, &responseBuffer)
code, state, _, _, _, err := ParseFormPostResponse(redirectURL, ioutil.NopCloser(bytes.NewReader(responseBuffer.Bytes())))
assert.NoError(t, err)
assert.Equal(t, parameters.Get("code"), code)
assert.Equal(t, parameters.Get("state"), state)
}

func TestIsRedirectURISecureStrict(t *testing.T) {
for d, c := range []struct {
u string
Expand Down
27 changes: 23 additions & 4 deletions authorize_request.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,22 @@ import (
"net/url"
)

type ResponseModeType string

const (
ResponseModeNone = ResponseModeType("")
ResponseModePost = ResponseModeType("form_post")
ResponseModeQuery = ResponseModeType("query")
ResponseModeFragment = ResponseModeType("fragment")
)

// AuthorizeRequest is an implementation of AuthorizeRequester
type AuthorizeRequest struct {
ResponseTypes Arguments `json:"responseTypes" gorethink:"responseTypes"`
RedirectURI *url.URL `json:"redirectUri" gorethink:"redirectUri"`
State string `json:"state" gorethink:"state"`
HandledResponseTypes Arguments `json:"handledResponseTypes" gorethink:"handledResponseTypes"`
ResponseTypes Arguments `json:"responseTypes" gorethink:"responseTypes"`
RedirectURI *url.URL `json:"redirectUri" gorethink:"redirectUri"`
State string `json:"state" gorethink:"state"`
HandledResponseTypes Arguments `json:"handledResponseTypes" gorethink:"handledResponseTypes"`
ResponseMode ResponseModeType `json:"ResponseMode" gorethink:"ResponseMode"`

Request
}
Expand All @@ -41,6 +51,7 @@ func NewAuthorizeRequest() *AuthorizeRequest {
RedirectURI: &url.URL{},
HandledResponseTypes: Arguments{},
Request: *NewRequest(),
ResponseMode: ResponseModeNone,
}
}

Expand Down Expand Up @@ -86,3 +97,11 @@ func (d *AuthorizeRequest) DidHandleAllResponseTypes() bool {

return len(d.ResponseTypes) > 0
}

func (d *AuthorizeRequest) GetResponseMode() ResponseModeType {
return d.ResponseMode
}

func (d *AuthorizeRequest) SetResponseMode(responseMode ResponseModeType) {
d.ResponseMode = responseMode
}
21 changes: 21 additions & 0 deletions authorize_request_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Copy link
Contributor

@mitar mitar Oct 24, 2020

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.

Copy link
Contributor

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?

Copy link
Member

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.

Copy link
Contributor Author

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.

responseMode := r.Form.Get("response_mode")

switch responseMode {
case string(ResponseModeNone):
request.ResponseMode = ResponseModeNone
case string(ResponseModeFragment):
request.ResponseMode = ResponseModeFragment
case string(ResponseModeQuery):
request.ResponseMode = ResponseModeQuery
case string(ResponseModePost):
request.ResponseMode = ResponseModePost
default:
return errors.WithStack(ErrUnsupportedResponseType.WithHintf("Request with unsupported response_mode \"%s\".", responseMode))
}
return nil
}

func (f *Fosite) NewAuthorizeRequest(ctx context.Context, r *http.Request) (AuthorizeRequester, error) {
request := &AuthorizeRequest{
Expand All @@ -228,6 +245,10 @@ func (f *Fosite) NewAuthorizeRequest(ctx context.Context, r *http.Request) (Auth
state := request.Form.Get("state")
request.State = state

if err := f.validateResponseMode(r, request); err != nil {
return request, err
}

client, err := f.Store.GetClient(ctx, request.GetRequestForm().Get("client_id"))
if err != nil {
return request, errors.WithStack(ErrInvalidClient.WithHint("The requested OAuth 2.0 Client does not exist.").WithCause(err).WithDebug(err.Error()))
Expand Down
Loading