package server import ( "errors" "io" "net/http" "net/http/httptest" "net/url" "strings" "testing" "github.com/kylelemons/godebug/pretty" "github.com/coreos/dex/pkg/html" "github.com/coreos/dex/user" "github.com/coreos/go-oidc/oidc" ) type testTemplate struct { tpl Template data registerTemplateData } func (t *testTemplate) Execute(w io.Writer, data interface{}) error { dataMap, ok := data.(registerTemplateData) if !ok { return errors.New("could not cast to registerTemplateData") } t.data = dataMap return t.tpl.Execute(w, data) } func TestHandleRegister(t *testing.T) { testIssuerAuth := testIssuerURL testIssuerAuth.Path = "/auth" str := func(s string) []string { return []string{s} } tests := []struct { // inputs query url.Values connID string attachRemote bool remoteIdentityEmail string remoteAlreadyExists bool // want wantStatus int wantFormValues url.Values wantUserExists bool wantRedirectURL url.URL wantRegisterTemplateData *registerTemplateData }{ { // User comes in with a valid code, redirected from the connector, // and is shown the form. query: url.Values{ "code": []string{"code-2"}, }, connID: "local", wantStatus: http.StatusOK, wantFormValues: url.Values{ "code": str("code-3"), "email": str(""), "password": str(""), "validate": str("1"), }, }, { // User comes in with a valid code, redirected from the connector, // user is created with a verified email, because it's a trusted // email provider. query: url.Values{ "code": []string{"code-3"}, }, connID: "oidc-trusted", remoteIdentityEmail: "test@example.com", attachRemote: true, wantStatus: http.StatusSeeOther, wantUserExists: true, }, { // User comes in with a valid code, redirected from the connector. // User is redirected to dex page with msg_code "login-maybe", // because the remote identity already exists. query: url.Values{ "code": []string{"code-3"}, }, connID: "oidc-trusted", remoteIdentityEmail: "test@example.com", attachRemote: true, remoteAlreadyExists: true, wantStatus: http.StatusOK, wantUserExists: true, wantRegisterTemplateData: ®isterTemplateData{ RemoteExists: &remoteExistsData{ Login: newURLWithParams(testRedirectURL, url.Values{ "code": []string{"code-7"}, "state": []string{""}, }).String(), Register: newURLWithParams(testIssuerAuth, url.Values{ "client_id": []string{testClientID}, "redirect_uri": []string{testRedirectURL.String()}, "register": []string{"1"}, "scope": []string{"openid"}, "state": []string{""}, }).String(), }, }, }, { // User comes in with a valid code, redirected from the connector, // user is created with a verified email, because it's a trusted // email provider. In addition, the email provided on the URL is // ignored, and instead comes from the remote identity. query: url.Values{ "code": []string{"code-3"}, "email": []string{"sneaky@example.com"}, }, connID: "oidc-trusted", remoteIdentityEmail: "test@example.com", attachRemote: true, wantStatus: http.StatusSeeOther, wantUserExists: true, }, { // User comes in with a valid code, redirected from the connector, // it's a trusted provider, but no email so no user created, and the // form comes back with the code. query: url.Values{ "code": []string{"code-3"}, }, connID: "oidc-trusted", remoteIdentityEmail: "", attachRemote: true, wantStatus: http.StatusOK, wantUserExists: false, wantFormValues: url.Values{ "code": str("code-4"), "email": str(""), "validate": str("1"), }, }, { // User comes in with a valid code, redirected from the connector, // it's a trusted provider, but the email is invalid, so no user // created, and the form comes back with the code. query: url.Values{ "code": []string{"code-3"}, }, connID: "oidc-trusted", remoteIdentityEmail: "notanemail", attachRemote: true, wantStatus: http.StatusOK, wantUserExists: false, wantFormValues: url.Values{ "code": str("code-4"), "email": str(""), "validate": str("1"), }, }, { // User comes in with a valid code, having submitted the form, but // has a invalid email. query: url.Values{ "code": []string{"code-2"}, "validate": []string{"1"}, "email": str(""), "password": str("password"), }, connID: "local", wantStatus: http.StatusBadRequest, wantFormValues: url.Values{ "code": str("code-3"), "email": str(""), "password": str("password"), "validate": str("1"), }, }, { // User comes in with a valid code, having submitted the form. A new // user is created. query: url.Values{ "code": []string{"code-2"}, "validate": []string{"1"}, "email": str("test@example.com"), "password": str("password"), }, connID: "local", wantStatus: http.StatusSeeOther, wantUserExists: true, }, { // User comes in with spaces in their email, having submitted the // form. The email is trimmed and the user is created. query: url.Values{ "code": []string{"code-2"}, "validate": []string{"1"}, "email": str("\t\ntest@example.com "), "password": str("password"), }, connID: "local", wantStatus: http.StatusSeeOther, wantUserExists: true, }, { // User comes in with an invalid email, having submitted the form. // The email is rejected and the user is not created. query: url.Values{ "code": []string{"code-2"}, "validate": []string{"1"}, "email": str("aninvalidemail"), "password": str("password"), }, connID: "local", wantStatus: http.StatusBadRequest, wantFormValues: url.Values{ "code": str("code-3"), "email": str("aninvalidemail"), "password": str("password"), "validate": str("1"), }, }, { // User comes in with a valid code, having submitted the form, but // there's no password. query: url.Values{ "code": []string{"code-2"}, "validate": []string{"1"}, "email": str("test@example.com"), }, connID: "local", wantStatus: http.StatusBadRequest, wantUserExists: false, wantFormValues: url.Values{ "code": str("code-3"), "email": str("test@example.com"), "password": str(""), "validate": str("1"), }, }, { // User comes in with a valid code, having submitted the form, but // there's no password, but they don't need one because connector ID // is oidc. query: url.Values{ "code": []string{"code-3"}, "validate": []string{"1"}, "email": str("test@example.com"), }, connID: "oidc", attachRemote: true, wantStatus: http.StatusSeeOther, wantUserExists: true, }, { // Same as before, but missing a code. query: url.Values{ "validate": []string{"1"}, "email": str("test@example.com"), }, connID: "oidc", attachRemote: true, wantStatus: http.StatusUnauthorized, wantUserExists: false, }, } for i, tt := range tests { f, err := makeTestFixtures() if err != nil { t.Fatalf("case %d: could not make test fixtures: %v", i, err) } if tt.remoteAlreadyExists { f.userRepo.Create(nil, user.User{ ID: "register-test-new-user", Email: tt.remoteIdentityEmail, EmailVerified: true, }) f.userRepo.AddRemoteIdentity(nil, "register-test-new-user", user.RemoteIdentity{ ID: "remoteID", ConnectorID: tt.connID, }) } key, err := f.srv.NewSession(tt.connID, "XXX", "", f.redirectURL, "", true, []string{"openid"}) t.Logf("case %d: key for NewSession: %v", i, key) if tt.attachRemote { sesID, err := f.sessionManager.ExchangeKey(key) if err != nil { t.Fatalf("case %d: expected non-nil error: %v", i, err) } ses, err := f.sessionManager.Get(sesID) if err != nil { t.Fatalf("case %d: expected non-nil error: %v", i, err) } _, err = f.sessionManager.AttachRemoteIdentity(ses.ID, oidc.Identity{ ID: "remoteID", Email: tt.remoteIdentityEmail, }) key, err := f.sessionManager.NewSessionKey(sesID) if err != nil { t.Fatalf("case %d: expected non-nil error: %v", i, err) } t.Logf("case %d: key for NewSession: %v", i, key) } tpl := &testTemplate{tpl: f.srv.RegisterTemplate} hdlr := handleRegisterFunc(f.srv, tpl) w := httptest.NewRecorder() u := "http://server.example.com" req, err := http.NewRequest("POST", u, strings.NewReader(tt.query.Encode())) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") if err != nil { t.Errorf("case %d: unable to form HTTP request: %v", i, err) } hdlr.ServeHTTP(w, req) if tt.wantRedirectURL.String() != "" { locationHdr := w.HeaderMap.Get("Location") redirURL, err := url.Parse(locationHdr) if err != nil { t.Errorf("case %d: unexpected error parsing url %q: %q", i, locationHdr, err) } else { if diff := pretty.Compare(*redirURL, tt.wantRedirectURL); diff != "" { t.Errorf("case %d: Compare(redirURL, tt.wantRedirectURL) = %v", i, diff) } } } if tt.wantStatus != w.Code { t.Errorf("case %d: wantStatus=%v, got=%v", i, tt.wantStatus, w.Code) } _, err = f.userRepo.GetByEmail(nil, "test@example.com") if tt.wantUserExists { if err != nil { t.Errorf("case %d: user not created: %v", i, err) } } else if err != user.ErrorNotFound { t.Errorf("case %d: unexpected error looking up user: want=%v, got=%v ", i, user.ErrorNotFound, err) } values, err := html.FormValues("#registerForm", w.Body) if err != nil { t.Errorf("case %d: could not parse form: %v", i, err) } if diff := pretty.Compare(tt.wantFormValues, values); diff != "" { t.Errorf("case %d: Compare(want, got) = %v", i, diff) } if tt.wantRegisterTemplateData != nil { if diff := pretty.Compare(*tt.wantRegisterTemplateData, tpl.data); diff != "" { t.Errorf("case %d: Compare(tt.wantRegisterTemplateData, tpl.data) = %v", i, diff) } } } } func newURLWithParams(u url.URL, values url.Values) *url.URL { newU := u newU.RawQuery = values.Encode() return &newU }