func TestResetPasswordHandler(t *testing.T) { makeToken := func(userID, password, clientID string, callback url.URL, expires time.Duration, signer jose.Signer) string { pr := user.NewPasswordReset("ID-1", user.Password(password), testIssuerURL, clientID, callback, expires) jwt, err := jose.NewSignedJWT(pr.Claims, signer) if err != nil { t.Fatalf("couldn't make token: %q", err) } token := jwt.Encode() return token } goodSigner := key.NewPrivateKeySet([]*key.PrivateKey{testPrivKey}, time.Now().Add(time.Minute)).Active().Signer() badKey, err := key.GeneratePrivateKey() if err != nil { t.Fatalf("couldn't make new key: %q", err) } badSigner := key.NewPrivateKeySet([]*key.PrivateKey{badKey}, time.Now().Add(time.Minute)).Active().Signer() str := func(s string) []string { return []string{s} } user.PasswordHasher = func(s string) ([]byte, error) { return []byte(strings.ToUpper(s)), nil } defer func() { user.PasswordHasher = user.DefaultPasswordHasher }() tokenForCase := map[int]string{ 0: makeToken("ID-1", "password", testClientID, testRedirectURL, time.Hour*1, goodSigner), 2: makeToken("ID-1", "password", testClientID, url.URL{}, time.Hour*1, goodSigner), 5: makeToken("ID-1", "password", testClientID, url.URL{}, time.Hour*1, goodSigner), } tests := []struct { query url.Values method string wantFormValues *url.Values wantCode int wantPassword string }{ // Scenario 1: Happy Path { // Case 0 // Step 1.1 - User clicks link in email, has valid token. query: url.Values{ "token": str(tokenForCase[0]), }, method: "GET", wantCode: http.StatusOK, wantFormValues: &url.Values{ "password": str(""), "token": str(tokenForCase[0]), }, wantPassword: "******", }, { // Case 1 // Step 1.2 - User enters in new valid password, password is changed, user is redirected. query: url.Values{ "token": str(makeToken("ID-1", "password", testClientID, testRedirectURL, time.Hour*1, goodSigner)), "password": str("new_password"), }, method: "POST", wantCode: http.StatusSeeOther, wantFormValues: &url.Values{}, wantPassword: "******", }, // Scenario 2: Happy Path, but without redirect. { // Case 2 // Step 2.1 - User clicks link in email, has valid token. query: url.Values{ "token": str(tokenForCase[2]), }, method: "GET", wantCode: http.StatusOK, wantFormValues: &url.Values{ "password": str(""), "token": str(tokenForCase[2]), }, wantPassword: "******", }, { // Case 3 // Step 2.2 - User enters in new valid password, password is changed, user is redirected. query: url.Values{ "token": str(makeToken("ID-1", "password", testClientID, url.URL{}, time.Hour*1, goodSigner)), "password": str("new_password"), }, method: "POST", // no redirect wantCode: http.StatusOK, wantFormValues: &url.Values{}, wantPassword: "******", }, // Errors { // Case 4 // Step 1.1.1 - User clicks link in email, has invalid token. query: url.Values{ "token": str(makeToken("ID-1", "password", testClientID, testRedirectURL, time.Hour*1, badSigner)), }, method: "GET", wantCode: http.StatusBadRequest, wantFormValues: &url.Values{}, wantPassword: "******", }, { // Case 5 // Step 2.2.1 - User enters in new valid password, password is changed, no redirect query: url.Values{ "token": str(tokenForCase[5]), "password": str("shrt"), }, method: "POST", // no redirect wantCode: http.StatusBadRequest, wantFormValues: &url.Values{ "password": str(""), "token": str(tokenForCase[5]), }, wantPassword: "******", }, { // Case 6 // Step 2.2.2 - User enters in new valid password, with suspicious token. query: url.Values{ "token": str(makeToken("ID-1", "password", testClientID, url.URL{}, time.Hour*1, badSigner)), "password": str("shrt"), }, method: "POST", // no redirect wantCode: http.StatusBadRequest, wantFormValues: &url.Values{}, wantPassword: "******", }, { // Case 7 // Token lacking client id query: url.Values{ "token": str(makeToken("ID-1", "password", "", url.URL{}, time.Hour*1, goodSigner)), "password": str("shrt"), }, method: "GET", wantCode: http.StatusBadRequest, wantPassword: "******", }, { // Case 8 // Token lacking client id query: url.Values{ "token": str(makeToken("ID-1", "password", "", url.URL{}, time.Hour*1, goodSigner)), "password": str("shrt"), }, method: "POST", wantCode: http.StatusBadRequest, wantPassword: "******", }, } for i, tt := range tests { f, err := makeTestFixtures() if err != nil { t.Fatalf("case %d: could not make test fixtures: %v", i, err) } hdlr := ResetPasswordHandler{ tpl: f.srv.ResetPasswordTemplate, issuerURL: testIssuerURL, um: f.srv.UserManager, keysFunc: f.srv.KeyManager.PublicKeys, } w := httptest.NewRecorder() var req *http.Request u := testIssuerURL u.Path = httpPathResetPassword if tt.method == "POST" { req, err = http.NewRequest(tt.method, u.String(), strings.NewReader(tt.query.Encode())) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") } else { u.RawQuery = tt.query.Encode() req, err = http.NewRequest(tt.method, u.String(), nil) } if err != nil { t.Errorf("case %d: unable to form HTTP request: %v", i, err) } hdlr.ServeHTTP(w, req) if tt.wantCode != w.Code { t.Errorf("case %d: wantCode=%v, got=%v", i, tt.wantCode, w.Code) continue } values, err := html.FormValues("#resetPasswordForm", bytes.NewReader(w.Body.Bytes())) if err != nil { t.Errorf("case %d: could not parse form: %v", i, err) } if tt.wantFormValues != nil { if diff := pretty.Compare(*tt.wantFormValues, values); diff != "" { t.Errorf("case %d: Compare(wantFormValues, got) = %v", i, diff) } } pwi, err := f.srv.PasswordInfoRepo.Get(nil, "ID-1") if err != nil { t.Errorf("case %d: Error getting Password info: %v", i, err) } if tt.wantPassword != string(pwi.Password) { t.Errorf("case %d: wantPassword=%v, got=%v", i, tt.wantPassword, string(pwi.Password)) } } }
func TestHandleRegister(t *testing.T) { str := func(s string) []string { return []string{s} } tests := []struct { // inputs query url.Values connID string attachRemote bool remoteIdentityEmail string // want wantStatus int wantFormValues url.Values wantUserCreated bool }{ { // 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: "*****@*****.**", attachRemote: true, wantStatus: http.StatusSeeOther, wantUserCreated: true, }, { // 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{"*****@*****.**"}, }, connID: "oidc-trusted", remoteIdentityEmail: "*****@*****.**", attachRemote: true, wantStatus: http.StatusSeeOther, wantUserCreated: 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, wantUserCreated: 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, wantUserCreated: 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("*****@*****.**"), "password": str("password"), }, connID: "local", wantStatus: http.StatusSeeOther, wantUserCreated: true, }, { // 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("*****@*****.**"), }, connID: "local", wantStatus: http.StatusBadRequest, wantUserCreated: false, wantFormValues: url.Values{ "code": str("code-3"), "email": str("*****@*****.**"), "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("*****@*****.**"), }, connID: "oidc", attachRemote: true, wantStatus: http.StatusSeeOther, wantUserCreated: true, }, { // Same as before, but missing a code. query: url.Values{ "validate": []string{"1"}, "email": str("*****@*****.**"), }, connID: "oidc", attachRemote: true, wantStatus: http.StatusUnauthorized, wantUserCreated: false, }, } for i, tt := range tests { f, err := makeTestFixtures() if err != nil { t.Fatalf("case %d: could not make test fixtures: %v", i, err) } 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) } hdlr := handleRegisterFunc(f.srv) 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.wantStatus != w.Code { t.Errorf("case %d: wantStatus=%v, got=%v", i, tt.wantStatus, w.Code) } _, err = f.userRepo.GetByEmail(nil, "*****@*****.**") if tt.wantUserCreated { 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) } } }
func TestSendResetPasswordEmailHandler(t *testing.T) { str := func(s string) []string { return []string{s} } textTemplateString := `{{define "password-reset.txt"}}{{.link}}{{end}}` textTemplates := template.New("text") _, err := textTemplates.Parse(textTemplateString) if err != nil { t.Fatalf("error parsing text templates: %v", err) } htmlTemplates := htmltemplate.New("html") tests := []struct { query url.Values method string wantFormValues *url.Values wantCode int wantRedirectURL *url.URL wantEmailer *testEmailer wantPRRedirect *url.URL wantPRUserID string wantPRPassword string }{ // First we'll test all the requests for happy path #1: { // Case 0 // STEP 1.1 - User clicks on link from local-login page and has a // session_key, which will prompt a redirect to page which has // instead a client_id and redirect_uri. query: url.Values{ "session_key": str("code-2"), }, method: "GET", wantCode: http.StatusSeeOther, wantRedirectURL: &url.URL{ Scheme: testIssuerURL.Scheme, Host: testIssuerURL.Host, Path: httpPathSendResetPassword, RawQuery: url.Values{ "client_id": str(testClientID), "redirect_uri": str(testRedirectURL.String()), }.Encode(), }, }, { // Case 1 // STEP 1.2 - This is the request that happens as a result of the // redirect. The client_id and redirect_uri should be in the form on // the page. query: url.Values{ "client_id": str(testClientID), "redirect_uri": str(testRedirectURL.String()), }, method: "GET", wantCode: http.StatusOK, wantFormValues: &url.Values{ "client_id": str(testClientID), "redirect_uri": str(testRedirectURL.String()), "email": str(""), }, }, { // Case 2 // STEP 1.3 - User enters a valid email, gets success page. The // values from the GET redirect are resent in the form POST along // with the email. query: url.Values{ "client_id": str(testClientID), "redirect_uri": str(testRedirectURL.String()), "email": str("*****@*****.**"), }, method: "POST", wantCode: http.StatusOK, wantEmailer: &testEmailer{ to: str("*****@*****.**"), from: "*****@*****.**", subject: "Reset Your Password", }, wantPRUserID: "ID-1", wantPRRedirect: &testRedirectURL, wantPRPassword: "******", }, // Happy Path #2 - no email or redirect { // Case 3 // STEP 2.1 - user somehow ends up on reset page with nothing but a client id query: url.Values{ "client_id": str(testClientID), }, method: "GET", wantCode: http.StatusOK, wantFormValues: &url.Values{ "client_id": str(testClientID), "redirect_uri": str(""), "email": str(""), }, }, { // Case 4 // STEP 2.3 - There is no STEP 2 because we don't have the redirect. query: url.Values{ "email": str("*****@*****.**"), "client_id": str(testClientID), }, method: "POST", wantCode: http.StatusOK, wantEmailer: &testEmailer{ to: str("*****@*****.**"), from: "*****@*****.**", subject: "Reset Your Password", }, wantPRPassword: "******", wantPRUserID: "ID-1", }, // Some error conditions: { // Case 5 // STEP 1.3.1 - User enters an invalid email, gets form again. query: url.Values{ "client_id": str(testClientID), "redirect_uri": str(testRedirectURL.String()), "email": str("NOT EMAIL"), }, method: "POST", wantCode: http.StatusBadRequest, wantFormValues: &url.Values{ "client_id": str(testClientID), "redirect_uri": str(testRedirectURL.String()), "email": str(""), }, }, { // Case 6 // STEP 1.3.2 - User enters a valid email but for a user not in the // system. They still get the success page, but no email is sent. query: url.Values{ "client_id": str(testClientID), "redirect_uri": str(testRedirectURL.String()), "email": str("*****@*****.**"), }, method: "POST", wantCode: http.StatusOK, }, { // Case 7 // STEP 1.1.1 - User clicks on link from local-login page and has a // session_key, but it is not-recognized. query: url.Values{ "session_key": str("code-UNKNOWN"), }, method: "GET", wantCode: http.StatusBadRequest, }, { // Case 8 // STEP 1.2.1 - Someone trying to replace a valid redirect_url with // an invalid one. query: url.Values{ "client_id": str(testClientID), "redirect_uri": str("http://evilhackers.example.com"), }, method: "GET", wantCode: http.StatusBadRequest, }, { // Case 9 // STEP 1.3.4 - User enters a valid email for a user in the system, // but with an invalid redirect_uri. query: url.Values{ "client_id": str(testClientID), "redirect_uri": str("http://evilhackers.example.com"), "email": str("*****@*****.**"), }, method: "POST", wantCode: http.StatusBadRequest, }, { // Case 10 // User hits the page with a valid email but no client id query: url.Values{ "email": str("*****@*****.**"), }, method: "GET", wantCode: http.StatusBadRequest, }, { // Case 10 // Don't send an email without a client id query: url.Values{ "email": str("*****@*****.**"), }, method: "POST", wantCode: http.StatusBadRequest, }, { // Case 11 // Empty requests lack a client id query: url.Values{}, method: "GET", wantCode: http.StatusBadRequest, }, { // Case 12 // Empty requests lack a client id query: url.Values{}, method: "POST", wantCode: http.StatusBadRequest, }, } for i, tt := range tests { t.Logf("CASE: %d", i) f, err := makeTestFixtures() if err != nil { t.Fatalf("case %d: could not make test fixtures: %v", i, err) } _, err = f.srv.NewSession("local", "XXX", "", f.redirectURL, "", true, []string{"openid"}) if err != nil { t.Fatalf("case %d: could not create new session: %v", i, err) } emailer := &testEmailer{ sent: make(chan struct{}), } templatizer := email.NewTemplatizedEmailerFromTemplates(textTemplates, htmlTemplates, emailer) f.srv.UserEmailer.SetEmailer(templatizer) hdlr := SendResetPasswordEmailHandler{ tpl: f.srv.SendResetPasswordEmailTemplate, emailer: f.srv.UserEmailer, sm: f.sessionManager, cr: f.clientIdentityRepo, } w := httptest.NewRecorder() var req *http.Request u := testIssuerURL u.Path = httpPathSendResetPassword if tt.method == "POST" { req, err = http.NewRequest(tt.method, u.String(), strings.NewReader(tt.query.Encode())) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") } else { u.RawQuery = tt.query.Encode() req, err = http.NewRequest(tt.method, u.String(), nil) } if err != nil { t.Errorf("case %d: unable to form HTTP request: %v", i, err) } hdlr.ServeHTTP(w, req) if tt.wantCode != w.Code { t.Errorf("case %d: wantCode=%v, got=%v", i, tt.wantCode, w.Code) t.Logf("case %d: Body: %v ", i, w.Body) continue } values, err := html.FormValues("#sendResetPasswordForm", w.Body) if err != nil { t.Errorf("case %d: could not parse form: %v", i, err) } if tt.wantFormValues != nil { if diff := pretty.Compare(*tt.wantFormValues, values); diff != "" { t.Errorf("case %d: Compare(wantFormValues, got) = %v", i, diff) } } if tt.wantRedirectURL != nil { location, err := url.Parse(w.Header().Get("location")) if err != nil { t.Errorf("case %d: could not parse Location header: %v", i, err) } if diff := pretty.Compare(*tt.wantRedirectURL, *location); diff != "" { t.Errorf("case %d: Compare(wantRedirectURL, got) = %v", i, diff) } } if tt.wantEmailer != nil { <-emailer.sent txt := emailer.text emailer.text = "" if diff := pretty.Compare(*tt.wantEmailer, *emailer); diff != "" { t.Errorf("case %d: Compare(wantEmailer, got) = %v", i, diff) } u, err := url.Parse(txt) if err != nil { t.Errorf("case %d: could not parse generated link: %v", i, err) } token := u.Query().Get("token") pubKeys, err := f.srv.KeyManager.PublicKeys() if err != nil { t.Errorf("case %d: could not parse generated link: %v", i, err) } pr, err := user.ParseAndVerifyPasswordResetToken(token, testIssuerURL, pubKeys) if err != nil { t.Errorf("case %d: could not parse reset token: %v", i, err) } if tt.wantPRPassword != string(pr.Password()) { t.Errorf("case %d: wantPRPassword=%v, got=%v", i, tt.wantPRPassword, string(pr.Password())) } if tt.wantPRRedirect == nil { if pr.Callback() != nil { t.Errorf("case %d: wantPRCallback=nil, got=%v", i, pr.Callback()) } } else { if *tt.wantPRRedirect != *pr.Callback() { t.Errorf("case %d: wantPRCallback=%v, got=%v", i, tt.wantPRRedirect, pr.Callback()) } } } } }
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: "*****@*****.**", 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: "*****@*****.**", 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{"*****@*****.**"}, }, connID: "oidc-trusted", remoteIdentityEmail: "*****@*****.**", 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("*****@*****.**"), "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\[email protected] "), "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("*****@*****.**"), }, connID: "local", wantStatus: http.StatusBadRequest, wantUserExists: false, wantFormValues: url.Values{ "code": str("code-3"), "email": str("*****@*****.**"), "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("*****@*****.**"), }, connID: "oidc", attachRemote: true, wantStatus: http.StatusSeeOther, wantUserExists: true, }, { // Same as before, but missing a code. query: url.Values{ "validate": []string{"1"}, "email": str("*****@*****.**"), }, 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, testClientID, "", 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, "*****@*****.**") 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) } } } }