func TestUpdatePasswordInfo(t *testing.T) { tests := []struct { pw user.PasswordInfo err error }{ { pw: user.PasswordInfo{ UserID: "ID-1", Password: user.Password("new_pass"), PasswordExpires: time.Now().Round(time.Second).UTC(), }, err: nil, }, { pw: user.PasswordInfo{ UserID: "ID-2", Password: user.Password("new_pass"), PasswordExpires: time.Now().Round(time.Second).UTC(), }, err: user.ErrorNotFound, }, { pw: user.PasswordInfo{ UserID: "ID-1", PasswordExpires: time.Now().Round(time.Second).UTC(), }, err: user.ErrorInvalidPassword, }, } for i, tt := range tests { repo := newPasswordInfoRepo(t) err := repo.Update(nil, tt.pw) if tt.err != nil { if err != tt.err { t.Errorf("case %d: want=%q, got=%q", i, tt.err, err) } } else { if err != nil { t.Errorf("case %d: want nil err, got %q", i, err) } gotPW, err := repo.Get(nil, tt.pw.UserID) if err != nil { t.Errorf("case %d: want nil err, got %q", i, err) } if diff := pretty.Compare(tt.pw, gotPW); diff != "" { t.Errorf("case %d: Compare(want, got) = %v", i, diff) } } } }
func (a *AdminAPI) CreateAdmin(admn adminschema.Admin) (string, error) { userID, err := a.userManager.CreateUser(user.User{ Email: admn.Email, Admin: true}, user.Password(admn.Password), a.localConnectorID) if err != nil { return "", mapError(err) } return userID, nil }
func (p *passwordInfoModel) passwordInfo() (user.PasswordInfo, error) { pw := user.PasswordInfo{ UserID: p.UserID, Password: user.Password(p.Password), } if p.PasswordExpires != 0 { pw.PasswordExpires = time.Unix(p.PasswordExpires, 0).UTC() } return pw, nil }
func (u *UsersAPI) CreateUser(creds Creds, usr schema.User, redirURL url.URL) (schema.UserCreateResponse, error) { log.Infof("userAPI: CreateUser") if !u.Authorize(creds) { return schema.UserCreateResponse{}, ErrorUnauthorized } hash, err := generateTempHash() if err != nil { return schema.UserCreateResponse{}, mapError(err) } metadata, err := u.clientIdentityRepo.Metadata(creds.ClientID) if err != nil { return schema.UserCreateResponse{}, mapError(err) } validRedirURL, err := client.ValidRedirectURL(&redirURL, metadata.RedirectURLs) if err != nil { return schema.UserCreateResponse{}, ErrorInvalidRedirectURL } id, err := u.manager.CreateUser(schemaUserToUser(usr), user.Password(hash), u.localConnectorID) if err != nil { return schema.UserCreateResponse{}, mapError(err) } userUser, err := u.manager.Get(id) if err != nil { return schema.UserCreateResponse{}, mapError(err) } usr = userToSchemaUser(userUser) url, err := u.emailer.SendInviteEmail(usr.Email, validRedirURL, creds.ClientID) // An email is sent only if we don't get a link and there's no error. emailSent := err == nil && url == nil var resetLink string if url != nil { resetLink = url.String() } return schema.UserCreateResponse{ User: &usr, EmailSent: emailSent, ResetPasswordLink: resetLink, }, nil }
func TestGetState(t *testing.T) { tests := []struct { addUsers []user.User want adminschema.State }{ { addUsers: []user.User{ user.User{ ID: "ID-3", Email: "*****@*****.**", DisplayName: "Admin", Admin: true, }, }, want: adminschema.State{ AdminUserCreated: true, }, }, { want: adminschema.State{ AdminUserCreated: false, }, }, } for i, tt := range tests { f := makeTestFixtures() for _, usr := range tt.addUsers { _, err := f.mgr.CreateUser(usr, user.Password("foopass"), f.adAPI.localConnectorID) if err != nil { t.Fatalf("case %d: err != nil: %q", i, err) } } got, err := f.adAPI.GetState() if err != nil { t.Errorf("case %d: err != nil: %q", i, err) } if diff := pretty.Compare(tt.want, got); diff != "" { t.Errorf("case %d: Compare(want, got) = %v", i, diff) } } }
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 TestCreateAdmin(t *testing.T) { tests := []struct { admn adminschema.Admin errCode int }{ { admn: adminschema.Admin{ Name: "foo", PasswordHash: user.Password([]byte("foopass")).EncodeBase64(), }, errCode: -1, }, { // duplicate Name admn: adminschema.Admin{ Name: "Name-1", PasswordHash: user.Password([]byte("foopass")).EncodeBase64(), }, errCode: http.StatusBadRequest, }, { // missing Name admn: adminschema.Admin{ PasswordHash: user.Password([]byte("foopass")).EncodeBase64(), }, errCode: http.StatusBadRequest, }, } for i, tt := range tests { f := makeTestFixtures() id, err := f.adAPI.CreateAdmin(tt.admn) if tt.errCode != -1 { if err == nil { t.Errorf("case %d: err was nil", i) continue } aErr, ok := err.(Error) if !ok { t.Errorf("case %d: not a admin.Error: %#v", i, err) continue } if aErr.Code != tt.errCode { t.Errorf("case %d: want=%d, got=%d", i, tt.errCode, aErr.Code) continue } } else { if err != nil { t.Errorf("case %d: err != nil: %q", i, err) } gotAdmn, err := f.adAPI.GetAdmin(id) if err != nil { t.Errorf("case %d: err != nil: %q", i, err) } tt.admn.Id = id if diff := pretty.Compare(tt.admn, gotAdmn); diff != "" { t.Errorf("case %d: Compare(want, got) = %v", i, diff) } } } }
func TestCreateUser(t *testing.T) { tests := []struct { usr user.User hashedPW user.Password localID string // defaults to "local" wantErr bool }{ { usr: user.User{ DisplayName: "Bob Exampleson", Email: "*****@*****.**", }, hashedPW: user.Password("I am a hash"), }, { usr: user.User{ DisplayName: "Al Adminson", Email: "*****@*****.**", Admin: true, }, hashedPW: user.Password("I am a hash"), }, { usr: user.User{ DisplayName: "Ed Emailless", }, hashedPW: user.Password("I am a hash"), wantErr: true, }, { usr: user.User{ DisplayName: "Eric Exampleson", Email: "*****@*****.**", }, hashedPW: user.Password("I am a hash"), localID: "abadlocalid", wantErr: true, }, } for i, tt := range tests { f := makeTestFixtures() localID := "local" if tt.localID != "" { localID = tt.localID } id, err := f.mgr.CreateUser(tt.usr, tt.hashedPW, localID) if tt.wantErr { if err == nil { t.Errorf("case %d: want non-nil err", i) } continue } if id == "" { t.Errorf("case %d: want non-empty id", i) } if err != nil { t.Errorf("case %d: unexpected err: %v", i, err) continue } gotUsr, err := f.ur.Get(nil, id) if err != nil { t.Errorf("case %d: unexpected err: %v", i, err) } tt.usr.ID = id tt.usr.CreatedAt = f.clock.Now() if diff := pretty.Compare(tt.usr, gotUsr); diff != "" { t.Errorf("case %d: Compare(want, got) = %v", i, diff) } pwi, err := f.pwr.Get(nil, id) if err != nil { t.Errorf("case %d: unexpected err: %v", i, err) } if string(pwi.Password) != string(tt.hashedPW) { t.Errorf("case %d: want=%q, got=%q", i, tt.hashedPW, pwi.Password) } ridUser, err := f.ur.GetByRemoteIdentity(nil, user.RemoteIdentity{ ID: id, ConnectorID: "local", }) if err != nil { t.Errorf("case %d: err != nil: %q", i, err) } if diff := pretty.Compare(gotUsr, ridUser); diff != "" { t.Errorf("case %d: Compare(want, got) = %v", i, diff) } } }
func (p *passwordChange) Password() user.Password { return user.Password(p.oldPassword) }
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 TestNewConnectorConfigFromMap(t *testing.T) { user.PasswordHasher = func(plaintext string) ([]byte, error) { return []byte(strings.ToUpper(plaintext)), nil } defer func() { user.PasswordHasher = user.DefaultPasswordHasher }() tests := []struct { m map[string]interface{} want ConnectorConfig }{ { m: map[string]interface{}{ "type": "local", "id": "foo", "passwordInfos": []map[string]string{ {"userId": "abc", "passwordHash": "UElORw=="}, // []byte is base64 encoded when using json.Marshasl {"userId": "271", "passwordPlaintext": "pong"}, }, }, want: &LocalConnectorConfig{ ID: "foo", PasswordInfos: []user.PasswordInfo{ user.PasswordInfo{ UserID: "abc", Password: user.Password("PING"), }, user.PasswordInfo{ UserID: "271", Password: user.Password("PONG"), }, }, }, }, { m: map[string]interface{}{ "type": "oidc", "id": "bar", "issuerURL": "http://example.com", "clientID": "client123", "clientSecret": "whaaaaa", }, want: &OIDCConnectorConfig{ ID: "bar", IssuerURL: "http://example.com", ClientID: "client123", ClientSecret: "whaaaaa", }, }, } for i, tt := range tests { got, err := newConnectorConfigFromMap(tt.m) if err != nil { t.Errorf("case %d: want nil error: %v", i, err) continue } if diff := pretty.Compare(tt.want, got); diff != "" { t.Errorf("case %d: Compare(want, got): %v", i, diff) } } }
func TestCreatePasswordInfo(t *testing.T) { tests := []struct { pw user.PasswordInfo err error }{ { pw: user.PasswordInfo{ UserID: "ID-2", Password: user.Password("*****@*****.**"), }, err: nil, }, { pw: user.PasswordInfo{ UserID: "ID-3", Password: user.Password("1234"), PasswordExpires: time.Now().Round(time.Second).UTC(), }, err: nil, }, { pw: user.PasswordInfo{ UserID: "ID-1", Password: user.Password("1234"), PasswordExpires: time.Now().Round(time.Second).UTC(), }, err: user.ErrorDuplicateID, }, { pw: user.PasswordInfo{ Password: user.Password("1234"), PasswordExpires: time.Now().Round(time.Second).UTC(), }, err: user.ErrorInvalidID, }, } for i, tt := range tests { repo := makeTestPasswordInfoRepo() err := repo.Create(nil, tt.pw) if tt.err != nil { if err != tt.err { t.Errorf("case %d: want=%v, got=%v", i, tt.err, err) } } else { if err != nil { t.Errorf("case %d: want nil err, got %v", i, err) } gotPW, err := repo.Get(nil, tt.pw.UserID) if err != nil { t.Errorf("case %d: want nil err, got %v", i, err) } if diff := pretty.Compare(tt.pw, gotPW); diff != "" { t.Errorf("case %d: Compare(want, got) = %v", i, diff) } } } }