Example #1
0
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
}
Example #2
0
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())
				}
			}

		}
	}
}
Example #3
0
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())
			}
		}
	}
}
Example #4
0
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())
		}
	}
}