Example #1
0
// SendResetPasswordEmail sends a password reset email to the user specified by the email addresss, containing a link with a signed token which can be visitied to initiate the password change/reset process.
// This method DOES NOT check for client ID, redirect URL validity - it is expected that upstream users have already done so.
// A link that can be used to reset the given user's password is returned.
func (u *UserEmailer) SendResetPasswordEmail(email string, redirectURL url.URL, clientID string) (*url.URL, error) {
	usr, pwi, err := u.userPasswordInfo(email)
	if err != nil {
		return nil, err
	}

	passwordReset := user.NewPasswordReset(usr.ID, pwi.Password, u.issuerURL,
		clientID, redirectURL, u.tokenValidityWindow)

	token, err := u.signedClaimsToken(passwordReset.Claims)
	if err != nil {
		return nil, err
	}

	resetURL := u.passwordResetURL
	q := resetURL.Query()
	q.Set("token", token)
	resetURL.RawQuery = q.Encode()

	if u.emailer != nil {
		err = u.emailer.SendMail(u.fromAddress, "Reset Your Password", "password-reset",
			map[string]interface{}{
				"email": usr.Email,
				"link":  resetURL.String(),
			}, usr.Email)
		if err != nil {
			log.Errorf("error sending password reset email %v: ", err)
		}
		return nil, err
	}
	return &resetURL, nil
}
Example #2
0
File: email.go Project: philips/dex
// SendResetPasswordEmail sends a password reset email to the user specified by the email addresss, containing a link with a signed token which can be visitied to initiate the password change/reset process.
// This method DOES NOT check for client ID, redirect URL validity - it is expected that upstream users have already done so.
// If there is no emailer is configured, the URL of the aforementioned link is returned, otherwise nil is returned.
func (u *UserEmailer) SendResetPasswordEmail(email string, redirectURL url.URL, clientID string) (*url.URL, error) {
	usr, err := u.ur.GetByEmail(nil, email)
	if err == user.ErrorNotFound {
		log.Errorf("No Such user for email: %q", email)
		return nil, err
	}
	if err != nil {
		log.Errorf("Error getting user: %q", err)
		return nil, err
	}

	pwi, err := u.pwi.Get(nil, usr.ID)
	if err == user.ErrorNotFound {
		// TODO(bobbyrullo): In this case, maybe send a different email explaining that
		// they don't have a local password.
		log.Errorf("No Password for userID: %q", usr.ID)
		return nil, err
	}
	if err != nil {
		log.Errorf("Error getting password: %q", err)
		return nil, err
	}

	signer, err := u.signerFn()
	if err != nil || signer == nil {
		log.Errorf("error getting signer: %v (%v)", err, signer)
		return nil, err
	}

	passwordReset := user.NewPasswordReset(usr, pwi.Password, u.issuerURL,
		clientID, redirectURL, u.tokenValidityWindow)
	jwt, err := jose.NewSignedJWT(passwordReset.Claims, signer)
	if err != nil {
		log.Errorf("error constructing or signing PasswordReset JWT: %v", err)
		return nil, err
	}
	token := jwt.Encode()

	resetURL := u.passwordResetURL
	q := resetURL.Query()
	q.Set("token", token)
	resetURL.RawQuery = q.Encode()

	if u.emailer != nil {
		err = u.emailer.SendMail(u.fromAddress, "Reset your password.", "password-reset",
			map[string]interface{}{
				"email": usr.Email,
				"link":  resetURL.String(),
			}, usr.Email)
		if err != nil {
			log.Errorf("error sending password reset email %v: ", err)
		}
		return nil, err
	}
	return &resetURL, nil
}
Example #3
0
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))
		}

	}
}