From 6a401464de8ab429888a08a427f7bfdecae60cbe Mon Sep 17 00:00:00 2001 From: Filip Burlacu Date: Wed, 29 Jun 2022 12:28:33 -0400 Subject: [PATCH] feat: GNAP flow closes popup window before wallet redirect Signed-off-by: Filip Burlacu --- Makefile | 1 + cmd/auth-rest/startcmd/start.go | 3 +- cmd/auth-rest/static/gnapRedirect.html | 16 ++++++ images/auth-rest/Dockerfile | 3 +- pkg/restapi/gnap/operations.go | 24 +++++++-- pkg/restapi/gnap/operations_test.go | 75 ++++++++++++++++++++++++-- test/bdd/pkg/gnap/steps.go | 14 +++++ 7 files changed, 125 insertions(+), 11 deletions(-) create mode 100644 cmd/auth-rest/static/gnapRedirect.html diff --git a/Makefile b/Makefile index f7c531e..7abbacd 100644 --- a/Makefile +++ b/Makefile @@ -34,6 +34,7 @@ unit-test: generate-unit-test-key auth-rest: @echo "Building auth-rest" @mkdir -p ./.build/bin + @cp -r ${AUTH_REST_PATH}/static ./.build/bin/ @cd ${AUTH_REST_PATH} && go build -o ../../.build/bin/auth-rest main.go .PHONY: auth-vue diff --git a/cmd/auth-rest/startcmd/start.go b/cmd/auth-rest/startcmd/start.go index 27a9078..56e43e2 100644 --- a/cmd/auth-rest/startcmd/start.go +++ b/cmd/auth-rest/startcmd/start.go @@ -519,6 +519,7 @@ func startAuthService(parameters *authRestParameters, srv server) error { AccessPolicyConfig: gnapAPConfig, InteractionHandler: interact, UIEndpoint: uiEndpoint, + ClosePopupHTML: parameters.staticFiles + "/gnapRedirect.html", StartupTimeout: parameters.startupTimeout, OIDC: &oidcmodel.Config{ CallbackURL: parameters.oidcParams.callbackURL, @@ -544,7 +545,7 @@ Database prefix: %s`, parameters.hostURL, parameters.databaseType, parameters.da router.PathPrefix(uiEndpoint). Subrouter(). Methods(http.MethodGet). - HandlerFunc(uiHandler(parameters.staticFiles, http.ServeFile)) + HandlerFunc(uiHandler(parameters.staticFiles+"/auth-vue", http.ServeFile)) return srv.ListenAndServe( parameters.hostURL, diff --git a/cmd/auth-rest/static/gnapRedirect.html b/cmd/auth-rest/static/gnapRedirect.html new file mode 100644 index 0000000..2d88efa --- /dev/null +++ b/cmd/auth-rest/static/gnapRedirect.html @@ -0,0 +1,16 @@ + + + + + Redirecting... + + + + + + + + diff --git a/images/auth-rest/Dockerfile b/images/auth-rest/Dockerfile index ff7ffe8..f2992f0 100644 --- a/images/auth-rest/Dockerfile +++ b/images/auth-rest/Dockerfile @@ -27,8 +27,9 @@ RUN make auth-rest FROM alpine:${ALPINE_VER} as base COPY --from=auth /go/src/github.com/trustbloc/auth/.build/bin/auth-rest /usr/local/bin +COPY --from=auth /go/src/github.com/trustbloc/auth/.build/bin/static /usr/local/static COPY ./.build/bin/auth-vue /usr/local/static/auth-vue -ENV AUTH_REST_STATIC_FILES=/usr/local/static/auth-vue +ENV AUTH_REST_STATIC_FILES=/usr/local/static # set up nsswitch.conf for Go's "netgo" implementation # - https://github.com/golang/go/blob/go1.9.1/src/net/conf.go#L194-L275 diff --git a/pkg/restapi/gnap/operations.go b/pkg/restapi/gnap/operations.go index 2af223a..7479adf 100644 --- a/pkg/restapi/gnap/operations.go +++ b/pkg/restapi/gnap/operations.go @@ -12,6 +12,7 @@ import ( "crypto/tls" "encoding/json" "fmt" + "html/template" "io/ioutil" "net/http" "net/url" @@ -76,6 +77,7 @@ type Operation struct { authHandler *authhandler.AuthHandler interactionHandler api.InteractionHandler uiEndpoint string + closePopupHTML string authProviders []authProvider oidcProvidersConfig map[string]*oidcmodel.ProviderConfig cachedOIDCProviders map[string]oidcProvider @@ -91,6 +93,7 @@ type Config struct { StoreProvider storage.Provider AccessPolicyConfig *accesspolicy.Config BaseURL string + ClosePopupHTML string InteractionHandler api.InteractionHandler UIEndpoint string OIDC *oidcmodel.Config @@ -139,6 +142,7 @@ func New(config *Config) (*Operation, error) { transientStore: transientStore, tlsConfig: config.TLSConfig, interactionHandler: config.InteractionHandler, + closePopupHTML: config.ClosePopupHTML, }, nil } @@ -397,8 +401,7 @@ func (o *Operation) oidcCallbackHandler(w http.ResponseWriter, r *http.Request) return } - // TODO: redirect to auth frontend that saves the redirect URI (with interactRef - // and responseHash) in vueX then closes the popup + // TODO: validate clientURI for security q := clientURI.Query() @@ -407,9 +410,20 @@ func (o *Operation) oidcCallbackHandler(w http.ResponseWriter, r *http.Request) clientURI.RawQuery = q.Encode() - // redirect to consumer url - http.Redirect(w, r, clientURI.String(), http.StatusFound) - logger.Debugf("redirected to: %s", clientURI.String()) + redirect := clientURI.String() + + t, err := template.ParseFiles(o.closePopupHTML) + if err != nil { + o.writeErrorResponse(w, http.StatusInternalServerError, "failed to parse template : %s", err.Error()) + + return + } + + if err := t.Execute(w, map[string]interface{}{ + "RedirectURI": redirect, + }); err != nil { + logger.Errorf(fmt.Sprintf("failed execute html template: %s", err.Error())) + } } func (o *Operation) authContinueHandler(w http.ResponseWriter, req *http.Request) { diff --git a/pkg/restapi/gnap/operations_test.go b/pkg/restapi/gnap/operations_test.go index 1d7962d..fcfa4c7 100644 --- a/pkg/restapi/gnap/operations_test.go +++ b/pkg/restapi/gnap/operations_test.go @@ -18,6 +18,9 @@ import ( "net/http" "net/http/httptest" "net/url" + "os" + "regexp" + "strings" "testing" "github.com/google/uuid" @@ -442,6 +445,11 @@ func TestOIDCCallbackHandler(t *testing.T) { code := uuid.New().String() config := config(t) + templatePath, deleteTmp := tmpStaticHTML(t) + defer deleteTmp() + + config.ClosePopupHTML = templatePath + o, err := New(config) require.NoError(t, err) @@ -503,7 +511,7 @@ func TestOIDCCallbackHandler(t *testing.T) { result := httptest.NewRecorder() o.oidcCallbackHandler(result, newOIDCCallback(state, code)) - require.Equal(t, http.StatusFound, result.Code) + require.Equal(t, http.StatusOK, result.Code) // TODO validate redirect url }) @@ -844,7 +852,14 @@ func TestOIDCCallbackHandler(t *testing.T) { } func Test_Full_Flow(t *testing.T) { - o, err := New(config(t)) + conf := config(t) + + templatePath, deleteTmp := tmpStaticHTML(t) + defer deleteTmp() + + conf.ClosePopupHTML = templatePath + + o, err := New(conf) require.NoError(t, err) authResp := &gnap.AuthResponse{} @@ -950,11 +965,23 @@ func Test_Full_Flow(t *testing.T) { o.oidcCallbackHandler(rw, newOIDCCallback(state, code)) - require.Equal(t, http.StatusFound, rw.Code) + require.Equal(t, http.StatusOK, rw.Code) - redirectURL, err := url.Parse(rw.Header().Get("location")) + body := rw.Body.Bytes() + + rx := regexp.MustCompile("window.opener.location.href = '(.*)';") + res := rx.FindStringSubmatch(string(body)) + + u := res[1] + + u = strings.ReplaceAll(u, "\\u0026", "\u0026") + u = strings.ReplaceAll(u, "\\/", "/") + + redirectURL, err := url.Parse(u) require.NoError(t, err) + require.Contains(t, redirectURL.String(), "interact_ref") + interactRef = redirectURL.Query().Get("interact_ref") require.NotEqual(t, "", interactRef) } @@ -1188,6 +1215,30 @@ func clientKey(t *testing.T) (*jwk.JWK, *gnap.ClientKey) { return &privJWK, &ck } +func tmpStaticHTML(t *testing.T) (string, func()) { + t.Helper() + + f, err := os.CreateTemp("", "tmpfile-*.html") + require.NoError(t, err) + + defer func() { + e := f.Close() + if e != nil { + fmt.Printf("failed to close tmpfile: %s", e.Error()) + } + }() + + _, err = f.Write([]byte(staticHTML)) + require.NoError(t, err) + + return f.Name(), func() { + e := os.Remove(f.Name()) + if e != nil { + fmt.Printf("failed to delete tmpfile: %s", e.Error()) + } + } +} + const ( accessPolicyConf = `{ "access-types": [{ @@ -1211,4 +1262,20 @@ const ( } ] }` + staticHTML = ` + + + +Redirecting... + + + + + + + +` ) diff --git a/test/bdd/pkg/gnap/steps.go b/test/bdd/pkg/gnap/steps.go index c815ef4..55d6d20 100644 --- a/test/bdd/pkg/gnap/steps.go +++ b/test/bdd/pkg/gnap/steps.go @@ -11,9 +11,11 @@ import ( "crypto/elliptic" "crypto/rand" "fmt" + "io/ioutil" "net/http" "net/http/cookiejar" "net/url" + "regexp" "strings" "github.com/cucumber/godog" @@ -297,6 +299,18 @@ func (s *Steps) interactRedirect() error { clientRedirect := result.Header.Get("Location") + body, err := ioutil.ReadAll(result.Body) + if err != nil { + return fmt.Errorf("failed to read result body: %w", err) + } + + rx := regexp.MustCompile("window.opener.location.href = '(.*)';") + res := rx.FindStringSubmatch(string(body)) + + clientRedirect = res[1] + clientRedirect = strings.Replace(clientRedirect, "\\u0026", "\u0026", -1) + clientRedirect = strings.Replace(clientRedirect, "\\/", "/", -1) + // TODO validate the client finishURL if !strings.HasPrefix(clientRedirect, mockClientFinishURI) { return fmt.Errorf(