From ed9ed83bc8ea8bdb7b20fde51b0e3479a4a299d6 Mon Sep 17 00:00:00 2001 From: Ayato Date: Sun, 25 Feb 2024 02:22:26 +0900 Subject: [PATCH] feat(op): Add response_mode: form_post --- pkg/oidc/authorization.go | 1 + pkg/op/auth_request.go | 56 +++++++++++++++++++++++++++++++++++++ pkg/op/auth_request_test.go | 28 +++++++++++++++++++ pkg/op/form_post.html.tmpl | 14 ++++++++++ 4 files changed, 99 insertions(+) create mode 100644 pkg/op/form_post.html.tmpl diff --git a/pkg/oidc/authorization.go b/pkg/oidc/authorization.go index 511e396..b502d92 100644 --- a/pkg/oidc/authorization.go +++ b/pkg/oidc/authorization.go @@ -48,6 +48,7 @@ const ( ResponseModeQuery ResponseMode = "query" ResponseModeFragment ResponseMode = "fragment" + ResponseModeFormPost ResponseMode = "form_post" // PromptNone (`none`) disallows the Authorization Server to display any authentication or consent user interface pages. // An error (login_required, interaction_required, ...) will be returned if the user is not already authenticated or consent is needed diff --git a/pkg/op/auth_request.go b/pkg/op/auth_request.go index 7058ebc..3da53b5 100644 --- a/pkg/op/auth_request.go +++ b/pkg/op/auth_request.go @@ -2,8 +2,10 @@ package op import ( "context" + _ "embed" "errors" "fmt" + "html/template" "net" "net/http" "net/url" @@ -464,6 +466,17 @@ func AuthResponseCode(w http.ResponseWriter, r *http.Request, authReq AuthReques Code: code, State: authReq.GetState(), } + + if authReq.GetResponseMode() == oidc.ResponseModeFormPost { + err = AuthResponseFormPost(w, authReq.GetRedirectURI(), &codeResponse, authorizer.Encoder()) + if err != nil { + AuthRequestError(w, r, authReq, err, authorizer) + return + } + + return + } + callback, err := AuthResponseURL(authReq.GetRedirectURI(), authReq.GetResponseType(), authReq.GetResponseMode(), &codeResponse, authorizer.Encoder()) if err != nil { AuthRequestError(w, r, authReq, err, authorizer) @@ -484,6 +497,17 @@ func AuthResponseToken(w http.ResponseWriter, r *http.Request, authReq AuthReque AuthRequestError(w, r, authReq, err, authorizer) return } + + if authReq.GetResponseMode() == oidc.ResponseModeFormPost { + err = AuthResponseFormPost(w, authReq.GetRedirectURI(), resp, authorizer.Encoder()) + if err != nil { + AuthRequestError(w, r, authReq, err, authorizer) + return + } + + return + } + callback, err := AuthResponseURL(authReq.GetRedirectURI(), authReq.GetResponseType(), authReq.GetResponseMode(), resp, authorizer.Encoder()) if err != nil { AuthRequestError(w, r, authReq, err, authorizer) @@ -535,6 +559,38 @@ func AuthResponseURL(redirectURI string, responseType oidc.ResponseType, respons return mergeQueryParams(uri, params), nil } +//go:embed form_post.html.tmpl +var formPostTemplate string + +// AuthResponseFormPost responds a html page that automatically submits the form which contains the auth response parameters +func AuthResponseFormPost(w http.ResponseWriter, redirectURI string, response any, encoder httphelper.Encoder) error { + t, err := template.New("form_post").Parse(formPostTemplate) + if err != nil { + return oidc.ErrServerError().WithParent(err) + } + + values := make(map[string][]string) + err = encoder.Encode(response, values) + if err != nil { + return oidc.ErrServerError().WithParent(err) + } + + params := &struct { + RedirectURI string + Params any + }{ + RedirectURI: redirectURI, + Params: values, + } + + err = t.Execute(w, params) + if err != nil { + return oidc.ErrServerError().WithParent(err) + } + + return nil +} + func setFragment(uri *url.URL, params url.Values) string { uri.Fragment = params.Encode() return uri.String() diff --git a/pkg/op/auth_request_test.go b/pkg/op/auth_request_test.go index 18880f0..5959a2e 100644 --- a/pkg/op/auth_request_test.go +++ b/pkg/op/auth_request_test.go @@ -9,6 +9,7 @@ import ( "net/url" "reflect" "testing" + "time" "github.com/golang/mock/gomock" "github.com/stretchr/testify/assert" @@ -1111,6 +1112,33 @@ func TestAuthResponseCode(t *testing.T) { wantBody: "", }, }, + { + name: "success form_post", + args: args{ + authReq: &storage.AuthRequest{ + ID: "id1", + CallbackURI: "https://example.com/callback", + TransferState: "state1", + ResponseMode: "form_post", + }, + authorizer: func(t *testing.T) op.Authorizer { + ctrl := gomock.NewController(t) + storage := mock.NewMockStorage(ctrl) + storage.EXPECT().SaveAuthCode(context.Background(), "id1", "id1") + + authorizer := mock.NewMockAuthorizer(ctrl) + authorizer.EXPECT().Storage().Return(storage) + authorizer.EXPECT().Crypto().Return(&mockCrypto{}) + authorizer.EXPECT().Encoder().Return(schema.NewEncoder()) + return authorizer + }, + }, + res: res{ + wantCode: http.StatusOK, + wantLocationHeader: "", + wantBody: "\n\n\n\n
\n\n\n\n\n\n
\n\n", + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/pkg/op/form_post.html.tmpl b/pkg/op/form_post.html.tmpl new file mode 100644 index 0000000..7bc9ab3 --- /dev/null +++ b/pkg/op/form_post.html.tmpl @@ -0,0 +1,14 @@ + + + + +
+{{with .Params.state}}{{end}} +{{with .Params.code}}{{end}} +{{with .Params.id_token}}{{end}} +{{with .Params.access_token}}{{end}} +{{with .Params.token_type}}{{end}} +{{with .Params.expires_in}}{{end}} +
+ + \ No newline at end of file