diff --git a/.circleci/.travis.yml b/.circleci/.travis.yml deleted file mode 100644 index 2c20f81972..0000000000 --- a/.circleci/.travis.yml +++ /dev/null @@ -1,56 +0,0 @@ -sudo: required - -language: go - -go: - - 1.9 - -go_import_path: github.com/ory/hydra - -services: - - docker - -env: - - DOCKER_BIND_LOCALHOST=true DATABASE_URL=memory DEP_VERSION="0.3.2" - -before_install: - - curl -L -s https://github.com/golang/dep/releases/download/v${DEP_VERSION}/dep-linux-amd64 -o $GOPATH/bin/dep - - chmod +x $GOPATH/bin/dep - - sudo apt-get install curl - -install: - - go get -u github.com/go-swagger/go-swagger/cmd/swagger github.com/bradfitz/goimports github.com/mattn/goveralls golang.org/x/tools/cmd/cover github.com/mitchellh/gox github.com/ory/go-acc - - git clone https://github.com/docker-library/official-images.git ~/official-images - - dep ensure - - go install github.com/ory/hydra - -script: - - ./scripts/test-format.sh - - go-acc -o coverage.txt ./... - - go test -race -short $(go list ./... | grep -v cmd) - - docker build -t hydra-travis-ci -f Dockerfile-without-telemetry . - - docker run -d hydra-travis-ci - - DATABASE_URL=memory hydra host --dangerous-auto-logon --dangerous-force-http --disable-telemetry & - - while ! echo exit | nc localhost 4444; do sleep 1; done - - ./scripts/test-e2e.sh - - ./scripts/run-genswag.sh - - goveralls -service=travis-ci -coverprofile=coverage.txt - -after_success: - - ./scripts/run-deploy.sh - -deploy: - - provider: npm - api_key: "$NPM_TOKEN" - email: "$NPM_EMAIL" - skip_cleanup: true - on: - tags: true - - provider: releases - file_glob: true - api_key: "$GITHUB_TOKEN" - file: "dist/*" - skip_cleanup: true - on: - tags: true - go: 1.9 diff --git a/.circleci/config.yml b/.circleci/config.yml index 3a9e3bf0ad..69f5c35532 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -35,6 +35,7 @@ jobs: - run: go get -u github.com/mattn/goveralls golang.org/x/tools/cmd/cover github.com/ory/go-acc - run: dep ensure -vendor-only - run: go install github.com/ory/hydra + - run: go install github.com/ory/hydra/test/mock-lcp - run: go-acc -o coverage.txt ./... - run: go test -race -short $(go list ./... | grep -v cmd) - run: ./scripts/test-e2e.sh diff --git a/.gitignore b/.gitignore index 6e21bd9aa0..cc267bead5 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,4 @@ coverage.* Dockerfile-plugin-* plugin-*.so hydra-docker-bin +cookies.txt \ No newline at end of file diff --git a/consent/helper.go b/consent/helper.go index 45b856090c..df39de2e22 100644 --- a/consent/helper.go +++ b/consent/helper.go @@ -23,6 +23,8 @@ package consent import ( "net/http" + "fmt" + "github.com/gorilla/sessions" "github.com/ory/fosite" "github.com/ory/go-convenience/mapx" @@ -63,6 +65,7 @@ func createCsrfSession(w http.ResponseWriter, r *http.Request, store sessions.St session.Values["csrf"] = csrf session.Options.HttpOnly = true session.Options.Secure = secure + fmt.Printf("\n\n\n\nCREATE COOKIE %+v\n\n\n\n", session.Values) if err := session.Save(r, w); err != nil { return errors.WithStack(err) @@ -75,6 +78,7 @@ func validateCsrfSession(r *http.Request, store sessions.Store, name, expectedCS if cookie, err := store.Get(r, name); err != nil { return errors.WithStack(fosite.ErrRequestForbidden.WithDebug("CSRF session cookie could not be decoded")) } else if csrf, err := mapx.GetString(cookie.Values, "csrf"); err != nil { + fmt.Printf("\n\n\n\nGOT COOKIE %+v\n\n\n\nHEADER: %+v", cookie.Values, r.Header) return errors.WithStack(fosite.ErrRequestForbidden.WithDebug("No CSRF value available in the session cookie")) } else if csrf != expectedCSRF { return errors.WithStack(fosite.ErrRequestForbidden.WithDebug("The CSRF value from the token does not match the CSRF value from the data store")) diff --git a/scripts/test-e2e.sh b/scripts/test-e2e.sh index 5d573a1c5f..19a8b3b5d5 100755 --- a/scripts/test-e2e.sh +++ b/scripts/test-e2e.sh @@ -4,14 +4,89 @@ set -euo pipefail cd "$( dirname "${BASH_SOURCE[0]}" )/.." -DATABASE_URL=memory hydra serve --dangerous-force-http --disable-telemetry & -while ! echo exit | nc 127.0.0.1 4444; do sleep 1; done +killall hydra || true +killall mock-lcp || true +killall mock-cb || true -export HYDRA_URL=http://localhost:4444/ +export HYDRA_URL=http://127.0.0.1:4444/ export OAUTH2_CLIENT_ID=foobar export OAUTH2_CLIENT_SECRET=bazbar +export OAUTH2_ISSUER_URL=http://127.0.0.1:4444/ +export LOG_LEVEL=debug +export REDIRECT_URL=http://127.0.0.1:4445/callback +export AUTH2_SCOPE=openid,offline + +go install . +go install ./test/mock-client +go install ./test/mock-lcp +go install ./test/mock-cb + +DATABASE_URL=memory \ + OAUTH2_CONSENT_URL=http://127.0.0.1:3000/consent \ + OAUTH2_LOGIN_URL=http://127.0.0.1:3000/login \ + OAUTH2_ERROR_URL=http://127.0.0.1:3000/error \ + OAUTH2_SHARE_ERROR_DEBUG=true \ + hydra serve --dangerous-force-http --disable-telemetry & + +PORT=3000 mock-lcp & + +PORT=4445 mock-cb & + +while ! echo exit | nc 127.0.0.1 4444; do sleep 1; done +while ! echo exit | nc 127.0.0.1 4445; do sleep 1; done +while ! echo exit | nc 127.0.0.1 3000; do sleep 1; done + + +hydra clients create \ + --endpoint http://127.0.0.1:4444 \ + --id $OAUTH2_CLIENT_ID \ + --secret $OAUTH2_CLIENT_SECRET \ + --response-types token,code,id_token \ + --grant-types refresh_token,authorization_code,client_credentials \ + --scope openid,offline \ + --callbacks http://127.0.0.1:4445/callback -hydra clients create --id $OAUTH2_CLIENT_ID --secret $OAUTH2_CLIENT_SECRET -g client_credentials token=$(hydra token client) -hydra token introspect $token -hydra clients delete foobar + +hydra token introspect "$token" + +## Authenticate but do not remember user +cookie=$(OAUTH2_EXTRA="&mockLogin=accept&mockConsent=accept" \ + mock-client) +export AUTH_COOKIE=$cookie + +## Must fail because prompt=none but no session was remembered +if OAUTH2_EXTRA="&mockLogin=accept&mockConsent=accept&prompt=none" \ + mock-client; then + echo "should have failed" + exit 1 +fi + +# Authenticate and remember login (but not consent) +cookie=$(OAUTH2_EXTRA="&mockLogin=accept&mockConsent=accept&rememberLogin=yes" \ + mock-client) +export AUTH_COOKIE=$cookie + +## Must fail because prompt=none but consent was not yet stored +if OAUTH2_EXTRA="&mockLogin=accept&mockConsent=accept&prompt=none" \ + mock-client; then + echo "should have failed" + exit 1 +fi + +# Remember consent +cookie=$(OAUTH2_EXTRA="&mockLogin=accept&mockConsent=accept&rememberConsent=yes" \ + mock-client) + +## Prompt none should work now because cookie was set +OAUTH2_EXTRA="&mockLogin=accept&mockConsent=accept&prompt=none" \ + mock-client + +hydra clients delete $OAUTH2_CLIENT_ID + +kill %1 +kill %2 +kill %3 +exit 0 + +sleep 5 diff --git a/test/mock-cb/main.go b/test/mock-cb/main.go new file mode 100644 index 0000000000..76cf02827c --- /dev/null +++ b/test/mock-cb/main.go @@ -0,0 +1,67 @@ +/* + * Copyright © 2015-2018 Aeneas Rekkas + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @author Aeneas Rekkas + * @Copyright 2017-2018 Aeneas Rekkas + * @license Apache-2.0 + */ + +package main + +import ( + "log" + "net/http" + "os" + + "context" + "strings" + + "golang.org/x/oauth2" +) + +func callback(rw http.ResponseWriter, r *http.Request) { + if r.URL.Query().Get("error") != "" { + http.Error(rw, "error happened in callback: "+r.URL.Query().Get("error")+" "+r.URL.Query().Get("error_description")+" "+r.URL.Query().Get("error_debug"), http.StatusInternalServerError) + return + } + + code := r.URL.Query().Get("code") + conf := oauth2.Config{ + ClientID: os.Getenv("OAUTH2_CLIENT_ID"), + ClientSecret: os.Getenv("OAUTH2_CLIENT_SECRET"), + Endpoint: oauth2.Endpoint{ + AuthURL: strings.TrimRight(os.Getenv("HYDRA_URL"), "/") + "/oauth2/auth", + TokenURL: strings.TrimRight(os.Getenv("HYDRA_URL"), "/") + "/oauth2/token", + }, + RedirectURL: os.Getenv("REDIRECT_URL"), + } + + token, err := conf.Exchange(context.Background(), code) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + + rw.Write([]byte(`access_token=` + token.AccessToken)) +} + +func main() { + http.HandleFunc("/callback", callback) + port := "4445" + if os.Getenv("PORT") != "" { + port = os.Getenv("PORT") + } + log.Fatal(http.ListenAndServe(":"+port, nil)) +} diff --git a/test/mock-client/main.go b/test/mock-client/main.go new file mode 100644 index 0000000000..f03e46f65a --- /dev/null +++ b/test/mock-client/main.go @@ -0,0 +1,90 @@ +/* + * Copyright © 2015-2018 Aeneas Rekkas + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @author Aeneas Rekkas + * @Copyright 2017-2018 Aeneas Rekkas + * @license Apache-2.0 + */ + +package main + +import ( + "log" + "net/http" + "os" + + "io/ioutil" + "net/http/cookiejar" + "strings" + + "fmt" + "net/url" + + "golang.org/x/oauth2" +) + +func main() { + conf := oauth2.Config{ + ClientID: os.Getenv("OAUTH2_CLIENT_ID"), + ClientSecret: os.Getenv("OAUTH2_CLIENT_SECRET"), + Endpoint: oauth2.Endpoint{ + AuthURL: strings.TrimRight(os.Getenv("HYDRA_URL"), "/") + "/oauth2/auth", + TokenURL: strings.TrimRight(os.Getenv("HYDRA_URL"), "/") + "/oauth2/token", + }, + Scopes: strings.Split(os.Getenv("OAUTH2_SCOPE"), ","), + RedirectURL: os.Getenv("REDIRECT_URL"), + } + au := conf.AuthCodeURL("some-stupid-state-foo") + os.Getenv("OAUTH2_EXTRA") + c, err := cookiejar.New(&cookiejar.Options{}) + if err != nil { + log.Fatalf("Unable to create cookie jar: %s", err) + } + + u, _ := url.Parse("http://127.0.0.1") + if os.Getenv("AUTH_COOKIE") != "" { + c.SetCookies(u, []*http.Cookie{{Name: "oauth2_authentication_session", Value: os.Getenv("AUTH_COOKIE")}}) + } + + resp, err := (&http.Client{ + Jar: c, + // Hack to fix cookie across domains + CheckRedirect: func(req *http.Request, via []*http.Request) error { + if len(via) > 0 && req.Header.Get("cookie") == "" { + req.Header.Set("Cookie", via[len(via)-1].Header.Get("Cookie")) + } + + return nil + }, + }).Get(au) + if err != nil { + log.Fatalf("Unable to make request: %s", err) + } + defer resp.Body.Close() + + out, err := ioutil.ReadAll(resp.Body) + if err != nil { + log.Fatalf("Unable to read body: %s", err) + } + + if resp.StatusCode != http.StatusOK { + log.Fatalf("Got status code %d and body %s", resp.StatusCode, out) + } + + for _, c := range c.Cookies(u) { + if c.Name == "oauth2_authentication_session" { + fmt.Print(c.Value) + } + } +} diff --git a/test/mock-lcp/main.go b/test/mock-lcp/main.go new file mode 100644 index 0000000000..f1e453b53f --- /dev/null +++ b/test/mock-lcp/main.go @@ -0,0 +1,112 @@ +/* + * Copyright © 2015-2018 Aeneas Rekkas + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @author Aeneas Rekkas + * @Copyright 2017-2018 Aeneas Rekkas + * @license Apache-2.0 + */ + +package main + +import ( + "log" + "net/http" + "os" + + "strings" + + "github.com/ory/hydra/sdk/go/hydra/swagger" +) + +var client = swagger.NewOAuth2ApiWithBasePath(os.Getenv("HYDRA_URL")) + +func login(rw http.ResponseWriter, r *http.Request) { + challenge := r.URL.Query().Get("login_challenge") + lr, resp, err := client.GetLoginRequest(challenge) + if err != nil { + log.Fatalf("Unable to fetch clogin request: %s", err) + } else if resp.StatusCode != http.StatusOK { + log.Fatalf("Unable to fetch login request, got status code %d", resp.StatusCode) + } + + var v *swagger.CompletedRequest + if strings.Contains(lr.RequestUrl, "mockLogin=accept") { + remember := false + if strings.Contains(lr.RequestUrl, "rememberLogin=yes") { + remember = true + } + v, resp, err = client.AcceptLoginRequest(challenge, swagger.AcceptLoginRequest{ + Subject: "foobar", + Remember: remember, + }) + } else { + v, resp, err = client.RejectLoginRequest(challenge, swagger.RejectRequest{ + Error_: "invalid_request", + }) + } + if err != nil { + log.Fatalf("Unable to accept/reject login request: %s", err) + } else if resp.StatusCode != http.StatusOK { + log.Fatalf("Unable to accept/reject login request, got status code %d", resp.StatusCode) + } + http.Redirect(rw, r, v.RedirectTo, http.StatusFound) +} + +func consent(rw http.ResponseWriter, r *http.Request) { + challenge := r.URL.Query().Get("consent_challenge") + o, resp, err := client.GetConsentRequest(challenge) + if err != nil { + log.Fatalf("Unable to fetch consent request: %s", err) + } else if resp.StatusCode != http.StatusOK { + log.Fatalf("Unable to fetch consent request, got status code %d", resp.StatusCode) + } + + var v *swagger.CompletedRequest + if strings.Contains(o.RequestUrl, "mockConsent=accept") { + remember := false + if strings.Contains(o.RequestUrl, "rememberConsent=yes") { + remember = true + } + v, resp, err = client.AcceptConsentRequest(challenge, swagger.AcceptConsentRequest{ + GrantScope: o.RequestedScope, + Remember: remember, + }) + } else { + v, resp, err = client.RejectConsentRequest(challenge, swagger.RejectRequest{ + Error_: "invalid_request", + }) + } + if err != nil { + log.Fatalf("Unable to accept/reject consent request: %s", err) + } else if resp.StatusCode != http.StatusOK { + log.Fatalf("Unable to accept/reject consent request, got status code %d", resp.StatusCode) + } + http.Redirect(rw, r, v.RedirectTo, http.StatusFound) +} + +func errh(rw http.ResponseWriter, r *http.Request) { + http.Error(rw, r.URL.Query().Get("error")+" "+r.URL.Query().Get("error_debug"), http.StatusInternalServerError) +} + +func main() { + http.HandleFunc("/login", login) + http.HandleFunc("/consent", consent) + http.HandleFunc("/error", errh) + port := "3000" + if os.Getenv("PORT") != "" { + port = os.Getenv("PORT") + } + log.Fatal(http.ListenAndServe(":"+port, nil)) +}