func (r *resetPasswordRequest) parseAndVerifyToken() bool { keys, err := r.h.keysFunc() if err != nil { log.Errorf("problem getting keys: %v", err) r.data.Error = "There's been an error processing your request." r.data.Message = "Plesae try again later." execTemplateWithStatus(r.w, r.h.tpl, r.data, http.StatusInternalServerError) return false } token := r.r.FormValue("token") pwReset, err := user.ParseAndVerifyPasswordResetToken(token, r.h.issuerURL, keys) if err != nil { log.Errorf("Reset Password unverifiable token: %v", err) r.data.Error = "Bad Password Reset Token" r.data.Message = "That was not a verifiable token." r.data.DontShowForm = true execTemplateWithStatus(r.w, r.h.tpl, r.data, http.StatusBadRequest) return false } r.pwReset = pwReset r.data.Token = token return true }
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 TestInvitationHandler(t *testing.T) { invUserID := "ID-1" invVerifiedID := "ID-Verified" invGoodSigner := key.NewPrivateKeySet([]*key.PrivateKey{testPrivKey}, time.Now().Add(time.Minute)).Active().Signer() badKey, err := key.GeneratePrivateKey() if err != nil { panic(fmt.Sprintf("couldn't make new key: %q", err)) } invBadSigner := key.NewPrivateKeySet([]*key.PrivateKey{badKey}, time.Now().Add(time.Minute)).Active().Signer() makeInvitationToken := func(password, userID, clientID, email string, callback url.URL, expires time.Duration, signer jose.Signer) string { iv := user.NewInvitation( user.User{ID: userID, Email: email}, user.Password(password), testIssuerURL, clientID, callback, expires) jwt, err := jose.NewSignedJWT(iv.Claims, signer) if err != nil { t.Fatalf("couldn't make token: %q", err) } token := jwt.Encode() return token } tests := []struct { userID string query url.Values signer jose.Signer wantCode int wantCallback url.URL wantEmailVerified bool }{ { // Case 0 Happy Path userID: invUserID, query: url.Values{ "token": []string{makeInvitationToken("password", invUserID, testClientID, "*****@*****.**", testRedirectURL, time.Hour*1, invGoodSigner)}, }, signer: invGoodSigner, wantCode: http.StatusSeeOther, wantCallback: testRedirectURL, wantEmailVerified: true, }, { // Case 1 user already verified userID: invVerifiedID, query: url.Values{ "token": []string{makeInvitationToken("password", invVerifiedID, testClientID, "*****@*****.**", testRedirectURL, time.Hour*1, invGoodSigner)}, }, signer: invGoodSigner, wantCode: http.StatusSeeOther, wantCallback: testRedirectURL, wantEmailVerified: true, }, { // Case 2 bad email userID: invUserID, query: url.Values{ "token": []string{makeInvitationToken("password", invVerifiedID, testClientID, "*****@*****.**", testRedirectURL, time.Hour*1, invGoodSigner)}, }, signer: invGoodSigner, wantCode: http.StatusBadRequest, wantCallback: testRedirectURL, wantEmailVerified: false, }, { // Case 3 bad signer userID: invUserID, query: url.Values{ "token": []string{makeInvitationToken("password", invUserID, testClientID, "*****@*****.**", testRedirectURL, time.Hour*1, invBadSigner)}, }, signer: invGoodSigner, wantCode: http.StatusBadRequest, wantCallback: testRedirectURL, wantEmailVerified: false, }, } for i, tt := range tests { f, err := makeTestFixtures() if err != nil { t.Fatalf("case %d: could not make test fixtures: %v", i, err) } keys, err := f.srv.KeyManager.PublicKeys() if err != nil { t.Fatalf("case %d: test fixture key infrastructure is broken: %v", i, err) } tZero := clock.Now() handler := &InvitationHandler{ passwordResetURL: f.srv.absURL("RESETME"), issuerURL: testIssuerURL, um: f.srv.UserManager, keysFunc: f.srv.KeyManager.PublicKeys, signerFunc: func() (jose.Signer, error) { return tt.signer, nil }, redirectValidityWindow: 100 * time.Second, } w := httptest.NewRecorder() u := testIssuerURL u.RawQuery = tt.query.Encode() req, err := http.NewRequest("GET", u.String(), nil) if err != nil { t.Fatalf("case %d: impossible error: %v", i, err) } handler.ServeHTTP(w, req) if tt.wantCode != w.Code { t.Errorf("case %d: wantCode=%v, got=%v", i, tt.wantCode, w.Code) continue } usr, err := f.srv.UserManager.Get(tt.userID) if err != nil { t.Fatalf("case %d: unexpected error: %v", i, err) } if usr.EmailVerified != tt.wantEmailVerified { t.Errorf("case %d: wantEmailVerified=%v got=%v", i, tt.wantEmailVerified, usr.EmailVerified) } if w.Code == http.StatusSeeOther { locString := w.HeaderMap.Get("Location") loc, err := url.Parse(locString) if err != nil { t.Fatalf("case %d: redirect returned nonsense url: '%v', %v", i, locString, err) } pwrToken := loc.Query().Get("token") pwrReset, err := user.ParseAndVerifyPasswordResetToken(pwrToken, testIssuerURL, keys) if err != nil { t.Errorf("case %d: password token is invalid: %v", i, err) } expTime := pwrReset.Claims["exp"].(float64) if expTime > float64(tZero.Add(handler.redirectValidityWindow).Unix()) || expTime < float64(tZero.Unix()) { t.Errorf("case %d: funny expiration time detected: %d", i, pwrReset.Claims["exp"]) } if pwrReset.Claims["aud"] != testClientID { t.Errorf("case %d: wanted \"aud\"=%v got=%v", i, testClientID, pwrReset.Claims["aud"]) } if pwrReset.Claims["iss"] != testIssuerURL.String() { t.Errorf("case %d: wanted \"iss\"=%v got=%v", i, testIssuerURL, pwrReset.Claims["iss"]) } if pwrReset.UserID() != tt.userID { t.Errorf("case %d: wanted UserID=%v got=%v", i, tt.userID, pwrReset.UserID()) } if bytes.Compare(pwrReset.Password(), user.Password("password")) != 0 { t.Errorf("case %d: wanted Password=%v got=%v", i, user.Password("password"), pwrReset.Password()) } if *pwrReset.Callback() != testRedirectURL { t.Errorf("case %d: wanted callback=%v got=%v", i, testRedirectURL, pwrReset.Callback()) } } } }
func TestSendResetPasswordEmail(t *testing.T) { tests := []struct { email string hasEmailer bool wantUserID string wantPassword string wantURL bool wantEmail bool wantErr bool }{ { // typical case with an emailer. email: "*****@*****.**", hasEmailer: true, wantURL: false, wantUserID: "ID-1", wantPassword: "******", wantEmail: true, }, { // typical case without an emailer. email: "*****@*****.**", hasEmailer: false, wantURL: true, wantUserID: "ID-1", wantPassword: "******", wantEmail: false, }, { // no such user. email: "*****@*****.**", hasEmailer: false, wantErr: true, }, { // user with no local password. email: "*****@*****.**", hasEmailer: false, wantErr: true, }, } for i, tt := range tests { ue, emailer, pubKey := makeTestFixtures() if !tt.hasEmailer { ue.SetEmailer(nil) } resetLink, err := ue.SendResetPasswordEmail(tt.email, redirURL, clientID) if tt.wantErr { if err == nil { t.Errorf("case %d: want non-nil err.", i) } continue } if tt.wantURL { if resetLink == nil { t.Errorf("case %d: want non-nil resetLink", i) continue } } else if resetLink != nil { t.Errorf("case %d: want resetLink==nil, got==%v", i, resetLink.String()) continue } if tt.wantEmail { if !emailer.sent { t.Errorf("case %d: want emailer.sent", i) continue } // In this case the link is in the email. resetLink, err = url.Parse(emailer.text) if err != nil { t.Errorf("case %d: want non-nil err, got: %q", i, err) } if tt.email != emailer.to[0] { t.Errorf("case %d: want==%v, got==%v", i, tt.email, emailer.to[0]) } if fromAddress != emailer.from { t.Errorf("case %d: want==%v, got==%v", i, fromAddress, emailer.from) } } else if emailer.sent { t.Errorf("case %d: want !emailer.sent", i) } token := resetLink.Query().Get("token") pr, err := user.ParseAndVerifyPasswordResetToken(token, issuerURL, []key.PublicKey{*pubKey}) if diff := pretty.Compare(redirURL, pr.Callback()); diff != "" { t.Errorf("case %d: Compare(want, got) = %v", i, diff) } if tt.wantUserID != pr.UserID() { t.Errorf("case %d: want==%v, got==%v", i, tt.wantUserID, pr.UserID()) } } }