func mockServer(cis []client.LoadableClient) (*server.Server, error) { dbMap := db.NewMemDB() k, err := key.GeneratePrivateKey() if err != nil { return nil, fmt.Errorf("Unable to generate private key: %v", err) } km := key.NewPrivateKeyManager() err = km.Set(key.NewPrivateKeySet([]*key.PrivateKey{k}, time.Now().Add(time.Minute))) if err != nil { return nil, err } clientRepo, clientManager, err := makeClientRepoAndManager(dbMap, cis) if err != nil { return nil, err } sm := manager.NewSessionManager(db.NewSessionRepo(dbMap), db.NewSessionKeyRepo(dbMap)) srv := &server.Server{ IssuerURL: url.URL{Scheme: "http", Host: "server.example.com"}, KeyManager: km, ClientRepo: clientRepo, ClientManager: clientManager, SessionManager: sm, } return srv, nil }
func (cfg *SingleServerConfig) Configure(srv *Server) error { k, err := key.GeneratePrivateKey() if err != nil { return err } ks := key.NewPrivateKeySet([]*key.PrivateKey{k}, time.Now().Add(24*time.Hour)) kRepo := key.NewPrivateKeySetRepo() if err = kRepo.Set(ks); err != nil { return err } cf, err := os.Open(cfg.ClientsFile) if err != nil { return fmt.Errorf("unable to read clients from file %s: %v", cfg.ClientsFile, err) } defer cf.Close() ciRepo, err := client.NewClientIdentityRepoFromReader(cf) if err != nil { return fmt.Errorf("unable to read client identities from file %s: %v", cfg.ClientsFile, err) } f, err := os.Open(cfg.ConnectorsFile) if err != nil { return fmt.Errorf("opening connectors file: %v", err) } defer f.Close() cfgs, err := connector.ReadConfigs(f) if err != nil { return fmt.Errorf("decoding connector configs: %v", err) } cfgRepo := connector.NewConnectorConfigRepoFromConfigs(cfgs) sRepo := session.NewSessionRepo() skRepo := session.NewSessionKeyRepo() sm := session.NewSessionManager(sRepo, skRepo) userRepo, err := user.NewUserRepoFromFile(cfg.UsersFile) if err != nil { return fmt.Errorf("unable to read users from file: %v", err) } pwiRepo := user.NewPasswordInfoRepo() refTokRepo := refresh.NewRefreshTokenRepo() txnFactory := repo.InMemTransactionFactory userManager := manager.NewUserManager(userRepo, pwiRepo, cfgRepo, txnFactory, manager.ManagerOptions{}) srv.ClientIdentityRepo = ciRepo srv.KeySetRepo = kRepo srv.ConnectorConfigRepo = cfgRepo srv.UserRepo = userRepo srv.UserManager = userManager srv.PasswordInfoRepo = pwiRepo srv.SessionManager = sm srv.RefreshTokenRepo = refTokRepo return nil }
func (m *privateKeySetModel) PrivateKeySet() (*key.PrivateKeySet, error) { keys := make([]*key.PrivateKey, len(m.Keys)) for i, pkm := range m.Keys { pk, err := pkm.PrivateKey() if err != nil { return nil, err } keys[i] = pk } return key.NewPrivateKeySet(keys, m.ExpiresAt), nil }
func makeUserToken(issuerURL url.URL, userID, clientID string, expires time.Duration, privKey *key.PrivateKey) string { signer := key.NewPrivateKeySet([]*key.PrivateKey{testPrivKey}, time.Now().Add(time.Minute)).Active().Signer() claims := oidc.NewClaims(issuerURL.String(), userID, clientID, time.Now(), time.Now().Add(expires)) jwt, err := jose.NewSignedJWT(claims, signer) if err != nil { panic(fmt.Sprintf("could not make token: %v", err)) } return jwt.Encode() }
func mockServer(cis []oidc.ClientIdentity) (*server.Server, error) { k, err := key.GeneratePrivateKey() if err != nil { return nil, fmt.Errorf("Unable to generate private key: %v", err) } km := key.NewPrivateKeyManager() err = km.Set(key.NewPrivateKeySet([]*key.PrivateKey{k}, time.Now().Add(time.Minute))) if err != nil { return nil, err } sm := session.NewSessionManager(session.NewSessionRepo(), session.NewSessionKeyRepo()) srv := &server.Server{ IssuerURL: url.URL{Scheme: "http", Host: "server.example.com"}, KeyManager: km, ClientIdentityRepo: client.NewClientIdentityRepo(cis), SessionManager: sm, } return srv, nil }
func mockServer(cis []client.Client) (*server.Server, error) { dbMap := db.NewMemDB() k, err := key.GeneratePrivateKey() if err != nil { return nil, fmt.Errorf("Unable to generate private key: %v", err) } km := key.NewPrivateKeyManager() err = km.Set(key.NewPrivateKeySet([]*key.PrivateKey{k}, time.Now().Add(time.Minute))) if err != nil { return nil, err } clientIDGenerator := func(hostport string) (string, error) { return hostport, nil } secGen := func() ([]byte, error) { return []byte("secret"), nil } clientRepo := db.NewClientRepo(dbMap) clientManager, err := clientmanager.NewClientManagerFromClients(clientRepo, db.TransactionFactory(dbMap), cis, clientmanager.ManagerOptions{ClientIDGenerator: clientIDGenerator, SecretGenerator: secGen}) if err != nil { return nil, err } sm := manager.NewSessionManager(db.NewSessionRepo(dbMap), db.NewSessionKeyRepo(dbMap)) srv := &server.Server{ IssuerURL: url.URL{Scheme: "http", Host: "server.example.com"}, KeyManager: km, ClientRepo: clientRepo, ClientManager: clientManager, SessionManager: sm, } return srv, nil }
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 TestDBPrivateKeySetRepoSetGet(t *testing.T) { s1 := []byte("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx") s2 := []byte("oooooooooooooooooooooooooooooooo") s3 := []byte("wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww") keys := []*key.PrivateKey{} for i := 0; i < 2; i++ { k, err := key.GeneratePrivateKey() if err != nil { t.Fatalf("Unable to generate RSA key: %v", err) } keys = append(keys, k) } ks := key.NewPrivateKeySet( []*key.PrivateKey{keys[0], keys[1]}, time.Now().Add(time.Minute)) tests := []struct { setSecrets [][]byte getSecrets [][]byte wantErr bool }{ { // same secrets used to encrypt, decrypt setSecrets: [][]byte{s1, s2}, getSecrets: [][]byte{s1, s2}, }, { // setSecrets got rotated, but getSecrets didn't yet. setSecrets: [][]byte{s2, s3}, getSecrets: [][]byte{s1, s2}, }, { // getSecrets doesn't have s3 setSecrets: [][]byte{s3}, getSecrets: [][]byte{s1, s2}, wantErr: true, }, } for i, tt := range tests { dbMap := connect(t) setRepo, err := db.NewPrivateKeySetRepo(dbMap, false, tt.setSecrets...) if err != nil { t.Fatalf(err.Error()) } getRepo, err := db.NewPrivateKeySetRepo(dbMap, false, tt.getSecrets...) if err != nil { t.Fatalf(err.Error()) } if err := setRepo.Set(ks); err != nil { t.Fatalf("case %d: Unexpected error: %v", i, err) } got, err := getRepo.Get() if tt.wantErr { if err == nil { t.Errorf("case %d: want err, got nil", i) } continue } if err != nil { t.Fatalf("case %d: Unexpected error: %v", i, err) } if diff := pretty.Compare(ks, got); diff != "" { t.Fatalf("case %d:Retrieved incorrect KeySet: Compare(want,got): %v", i, diff) } } }
func TestHTTPExchangeTokenRefreshToken(t *testing.T) { password, err := user.NewPasswordFromPlaintext("woof") if err != nil { t.Fatalf("unexpectd error: %q", err) } passwordInfo := user.PasswordInfo{ UserID: "elroy77", Password: password, } cfg := &connector.LocalConnectorConfig{ ID: "local", } validRedirURL := url.URL{ Scheme: "http", Host: "client.example.com", Path: "/callback", } ci := client.Client{ Credentials: oidc.ClientCredentials{ ID: validRedirURL.Host, Secret: base64.URLEncoding.EncodeToString([]byte("secret")), }, Metadata: oidc.ClientMetadata{ RedirectURIs: []url.URL{ validRedirURL, }, }, } dbMap := db.NewMemDB() clientRepo, clientManager, err := makeClientRepoAndManager(dbMap, []client.LoadableClient{{ Client: ci, }}) if err != nil { t.Fatalf("Failed to create client identity manager: " + err.Error()) } passwordInfoRepo, err := db.NewPasswordInfoRepoFromPasswordInfos(db.NewMemDB(), []user.PasswordInfo{passwordInfo}) if err != nil { t.Fatalf("Failed to create password info repo: %v", err) } issuerURL := url.URL{Scheme: "http", Host: "server.example.com"} sm := manager.NewSessionManager(db.NewSessionRepo(dbMap), db.NewSessionKeyRepo(dbMap)) k, err := key.GeneratePrivateKey() if err != nil { t.Fatalf("Unable to generate RSA key: %v", err) } km := key.NewPrivateKeyManager() err = km.Set(key.NewPrivateKeySet([]*key.PrivateKey{k}, time.Now().Add(time.Minute))) if err != nil { t.Fatalf("Unexpected error: %v", err) } usr := user.User{ ID: "ID-test", Email: "*****@*****.**", DisplayName: "displayname", } userRepo := db.NewUserRepo(db.NewMemDB()) if err := userRepo.Create(nil, usr); err != nil { t.Fatalf("Unexpected error: %v", err) } refreshTokenRepo := refreshtest.NewTestRefreshTokenRepo() srv := &server.Server{ IssuerURL: issuerURL, KeyManager: km, SessionManager: sm, ClientRepo: clientRepo, ClientManager: clientManager, Templates: template.New(connector.LoginPageTemplateName), Connectors: []connector.Connector{}, UserRepo: userRepo, PasswordInfoRepo: passwordInfoRepo, RefreshTokenRepo: refreshTokenRepo, } if err = srv.AddConnector(cfg); err != nil { t.Fatalf("Unexpected error: %v", err) } sClient := &phttp.HandlerClient{Handler: srv.HTTPHandler()} pcfg, err := oidc.FetchProviderConfig(sClient, issuerURL.String()) if err != nil { t.Fatalf("Failed to fetch provider config: %v", err) } ks := key.NewPublicKeySet([]jose.JWK{k.JWK()}, time.Now().Add(1*time.Hour)) ccfg := oidc.ClientConfig{ HTTPClient: sClient, ProviderConfig: pcfg, Credentials: ci.Credentials, RedirectURL: validRedirURL.String(), KeySet: *ks, } cl, err := oidc.NewClient(ccfg) if err != nil { t.Fatalf("Failed creating oidc.Client: %v", err) } m := http.NewServeMux() var claims jose.Claims var refresh string m.HandleFunc("/callback", handleCallbackFunc(cl, &claims, &refresh)) cClient := &phttp.HandlerClient{Handler: m} // this will actually happen due to some interaction between the // end-user and a remote identity provider sessionID, err := sm.NewSession("bogus_idpc", ci.Credentials.ID, "bogus", url.URL{}, "", false, []string{"openid", "offline_access", "email", "profile"}) if err != nil { t.Fatalf("Unexpected error: %v", err) } if _, err = sm.AttachRemoteIdentity(sessionID, passwordInfo.Identity()); err != nil { t.Fatalf("Unexpected error: %v", err) } if _, err = sm.AttachUser(sessionID, usr.ID); err != nil { t.Fatalf("Unexpected error: %v", err) } key, err := sm.NewSessionKey(sessionID) if err != nil { t.Fatalf("Unexpected error: %v", err) } req, err := http.NewRequest("GET", fmt.Sprintf("http://client.example.com/callback?code=%s", key), nil) if err != nil { t.Fatalf("Failed creating HTTP request: %v", err) } resp, err := cClient.Do(req) if err != nil { t.Fatalf("Failed resolving HTTP requests against /callback: %v", err) } if err := verifyUserClaims(claims, &ci, &usr, issuerURL); err != nil { t.Fatalf("Failed to verify claims: %v", err) } if resp.StatusCode != http.StatusOK { t.Fatalf("Received status code %d, want %d", resp.StatusCode, http.StatusOK) } if refresh == "" { t.Fatalf("No refresh token") } // Use refresh token to get a new ID token. token, err := cl.RefreshToken(refresh) if err != nil { t.Fatalf("Unexpected error: %v", err) } claims, err = token.Claims() if err != nil { t.Fatalf("Failed parsing claims from client token: %v", err) } if err := verifyUserClaims(claims, &ci, &usr, issuerURL); err != nil { t.Fatalf("Failed to verify claims: %v", err) } }
func (cfg *SingleServerConfig) Configure(srv *Server) error { k, err := key.GeneratePrivateKey() if err != nil { return err } dbMap := db.NewMemDB() ks := key.NewPrivateKeySet([]*key.PrivateKey{k}, time.Now().Add(24*time.Hour)) kRepo := key.NewPrivateKeySetRepo() if err = kRepo.Set(ks); err != nil { return err } clients, err := loadClients(cfg.ClientsFile) if err != nil { return fmt.Errorf("unable to read clients from file %s: %v", cfg.ClientsFile, err) } clientRepo, err := db.NewClientRepoFromClients(dbMap, clients) if err != nil { return err } f, err := os.Open(cfg.ConnectorsFile) if err != nil { return fmt.Errorf("opening connectors file: %v", err) } defer f.Close() cfgs, err := connector.ReadConfigs(f) if err != nil { return fmt.Errorf("decoding connector configs: %v", err) } cfgRepo := db.NewConnectorConfigRepo(dbMap) if err := cfgRepo.Set(cfgs); err != nil { return fmt.Errorf("failed to set connectors: %v", err) } sRepo := db.NewSessionRepo(dbMap) skRepo := db.NewSessionKeyRepo(dbMap) sm := sessionmanager.NewSessionManager(sRepo, skRepo) users, pwis, err := loadUsers(cfg.UsersFile) if err != nil { return fmt.Errorf("unable to read users from file: %v", err) } userRepo, err := db.NewUserRepoFromUsers(dbMap, users) if err != nil { return err } pwiRepo, err := db.NewPasswordInfoRepoFromPasswordInfos(dbMap, pwis) if err != nil { return err } refTokRepo := db.NewRefreshTokenRepo(dbMap) txnFactory := db.TransactionFactory(dbMap) userManager := usermanager.NewUserManager(userRepo, pwiRepo, cfgRepo, txnFactory, usermanager.ManagerOptions{}) clientManager := clientmanager.NewClientManager(clientRepo, db.TransactionFactory(dbMap), clientmanager.ManagerOptions{}) if err != nil { return fmt.Errorf("Failed to create client identity manager: %v", err) } srv.ClientRepo = clientRepo srv.ClientManager = clientManager srv.KeySetRepo = kRepo srv.ConnectorConfigRepo = cfgRepo srv.UserRepo = userRepo srv.UserManager = userManager srv.PasswordInfoRepo = pwiRepo srv.SessionManager = sm srv.RefreshTokenRepo = refTokRepo srv.HealthChecks = append(srv.HealthChecks, db.NewHealthChecker(dbMap)) srv.dbMap = dbMap return nil }
func makeTestFixtures() (*testFixtures, error) { userRepo := user.NewUserRepoFromUsers(testUsers) pwRepo := user.NewPasswordInfoRepoFromPasswordInfos(testPasswordInfos) manager := user.NewManager(userRepo, pwRepo, repo.InMemTransactionFactory, user.ManagerOptions{}) connConfigs := []connector.ConnectorConfig{ &connector.OIDCConnectorConfig{ ID: "oidc", IssuerURL: testIssuerURL.String(), ClientID: "12345", ClientSecret: "567789", }, &connector.OIDCConnectorConfig{ ID: "oidc-trusted", IssuerURL: testIssuerURL.String(), ClientID: "12345-trusted", ClientSecret: "567789-trusted", TrustedEmailProvider: true, }, &connector.LocalConnectorConfig{ ID: "local", }, } sessionManager := session.NewSessionManager(session.NewSessionRepo(), session.NewSessionKeyRepo()) sessionManager.GenerateCode = sequentialGenerateCodeFunc() emailer, err := email.NewTemplatizedEmailerFromGlobs( emailTemplatesLocation+"/*.txt", emailTemplatesLocation+"/*.html", &email.FakeEmailer{}) if err != nil { return nil, err } clientIdentityRepo := client.NewClientIdentityRepo([]oidc.ClientIdentity{ oidc.ClientIdentity{ Credentials: oidc.ClientCredentials{ ID: "XXX", Secret: testClientSecret, }, Metadata: oidc.ClientMetadata{ RedirectURLs: []url.URL{ testRedirectURL, }, }, }, }) km := key.NewPrivateKeyManager() err = km.Set(key.NewPrivateKeySet([]*key.PrivateKey{testPrivKey}, time.Now().Add(time.Minute))) if err != nil { return nil, err } tpl, err := getTemplates("dex", "https://coreos.com/assets/images/brand/coreos-mark-30px.png", templatesLocation) if err != nil { return nil, err } srv := &Server{ IssuerURL: testIssuerURL, SessionManager: sessionManager, ClientIdentityRepo: clientIdentityRepo, Templates: tpl, UserRepo: userRepo, PasswordInfoRepo: pwRepo, UserManager: manager, KeyManager: km, } err = setTemplates(srv, tpl) if err != nil { return nil, err } for _, config := range connConfigs { if err := srv.AddConnector(config); err != nil { return nil, err } } srv.UserEmailer = useremail.NewUserEmailer(srv.UserRepo, srv.PasswordInfoRepo, srv.KeyManager.Signer, srv.SessionManager.ValidityWindow, srv.IssuerURL, emailer, "*****@*****.**", srv.absURL(httpPathResetPassword), srv.absURL(httpPathEmailVerify)) return &testFixtures{ srv: srv, redirectURL: testRedirectURL, userRepo: userRepo, sessionManager: sessionManager, emailer: emailer, clientIdentityRepo: clientIdentityRepo, }, nil }
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 makeTestFixtures() (*testFixtures, error) { dbMap := db.NewMemDB() userRepo, err := db.NewUserRepoFromUsers(dbMap, testUsers) if err != nil { return nil, err } pwRepo, err := db.NewPasswordInfoRepoFromPasswordInfos(dbMap, testPasswordInfos) if err != nil { return nil, err } connConfigs := []connector.ConnectorConfig{ &connector.OIDCConnectorConfig{ ID: "oidc", IssuerURL: testIssuerURL.String(), ClientID: "12345", ClientSecret: "567789", }, &connector.OIDCConnectorConfig{ ID: "oidc-trusted", IssuerURL: testIssuerURL.String(), ClientID: "12345-trusted", ClientSecret: "567789-trusted", TrustedEmailProvider: true, }, &connector.LocalConnectorConfig{ ID: "local", }, } connCfgRepo := db.NewConnectorConfigRepo(dbMap) if err := connCfgRepo.Set(connConfigs); err != nil { return nil, err } userManager := usermanager.NewUserManager(userRepo, pwRepo, connCfgRepo, db.TransactionFactory(dbMap), usermanager.ManagerOptions{}) sessionManager := sessionmanager.NewSessionManager(db.NewSessionRepo(db.NewMemDB()), db.NewSessionKeyRepo(db.NewMemDB())) sessionManager.GenerateCode = sequentialGenerateCodeFunc() emailer, err := email.NewTemplatizedEmailerFromGlobs( emailTemplatesLocation+"/*.txt", emailTemplatesLocation+"/*.html", &email.FakeEmailer{}) if err != nil { return nil, err } clients := []client.Client{ client.Client{ Credentials: oidc.ClientCredentials{ ID: testClientID, Secret: base64.URLEncoding.EncodeToString([]byte("secret")), }, Metadata: oidc.ClientMetadata{ RedirectURIs: []url.URL{ testRedirectURL, }, }, }, } clientIDGenerator := func(hostport string) (string, error) { return hostport, nil } secGen := func() ([]byte, error) { return []byte("secret"), nil } clientRepo := db.NewClientRepo(dbMap) clientManager, err := clientmanager.NewClientManagerFromClients(clientRepo, db.TransactionFactory(dbMap), clients, clientmanager.ManagerOptions{ClientIDGenerator: clientIDGenerator, SecretGenerator: secGen}) if err != nil { return nil, err } km := key.NewPrivateKeyManager() err = km.Set(key.NewPrivateKeySet([]*key.PrivateKey{testPrivKey}, time.Now().Add(time.Minute))) if err != nil { return nil, err } tpl, err := getTemplates("dex", "https://coreos.com/assets/images/brand/coreos-mark-30px.png", true, templatesLocation) if err != nil { return nil, err } srv := &Server{ IssuerURL: testIssuerURL, SessionManager: sessionManager, ClientRepo: clientRepo, Templates: tpl, UserRepo: userRepo, PasswordInfoRepo: pwRepo, UserManager: userManager, ClientManager: clientManager, KeyManager: km, } err = setTemplates(srv, tpl) if err != nil { return nil, err } for _, config := range connConfigs { if err := srv.AddConnector(config); err != nil { return nil, err } } srv.UserEmailer = useremail.NewUserEmailer(srv.UserRepo, srv.PasswordInfoRepo, srv.KeyManager.Signer, srv.SessionManager.ValidityWindow, srv.IssuerURL, emailer, "*****@*****.**", srv.absURL(httpPathResetPassword), srv.absURL(httpPathEmailVerify), srv.absURL(httpPathAcceptInvitation), ) return &testFixtures{ srv: srv, redirectURL: testRedirectURL, userRepo: userRepo, sessionManager: sessionManager, emailer: emailer, clientRepo: clientRepo, clientManager: clientManager, }, nil }
func makeTestFixturesWithOptions(options testFixtureOptions) (*testFixtures, error) { dbMap := db.NewMemDB() userRepo, err := db.NewUserRepoFromUsers(dbMap, testUsers) if err != nil { return nil, err } pwRepo, err := db.NewPasswordInfoRepoFromPasswordInfos(dbMap, testPasswordInfos) if err != nil { return nil, err } connConfigs := []connector.ConnectorConfig{ &connector.OIDCConnectorConfig{ ID: testConnectorIDOpenID, IssuerURL: testIssuerURL.String(), ClientID: "12345", ClientSecret: "567789", }, &connector.OIDCConnectorConfig{ ID: testConnectorIDOpenIDTrusted, IssuerURL: testIssuerURL.String(), ClientID: "12345-trusted", ClientSecret: "567789-trusted", TrustedEmailProvider: true, }, &connector.OIDCConnectorConfig{ ID: testConnectorID1, IssuerURL: testIssuerURL.String(), ClientID: testConnectorID1 + "_client_id", ClientSecret: testConnectorID1 + "_client_secret", TrustedEmailProvider: true, }, &connector.LocalConnectorConfig{ ID: testConnectorLocalID, }, } connCfgRepo := db.NewConnectorConfigRepo(dbMap) if err := connCfgRepo.Set(connConfigs); err != nil { return nil, err } userManager := usermanager.NewUserManager(userRepo, pwRepo, connCfgRepo, db.TransactionFactory(dbMap), usermanager.ManagerOptions{}) sessionManager := sessionmanager.NewSessionManager(db.NewSessionRepo(db.NewMemDB()), db.NewSessionKeyRepo(db.NewMemDB())) sessionManager.GenerateCode = sequentialGenerateCodeFunc() refreshTokenRepo := refreshtest.NewTestRefreshTokenRepo() emailer, err := email.NewTemplatizedEmailerFromGlobs( emailTemplatesLocation+"/*.txt", emailTemplatesLocation+"/*.html", &email.FakeEmailer{}, "*****@*****.**") if err != nil { return nil, err } var clients []client.LoadableClient if options.clients == nil { clients = testClients } else { clients = options.clients } clientIDGenerator := func(hostport string) (string, error) { return hostport, nil } secGen := func() ([]byte, error) { return []byte("secret"), nil } clientRepo, err := db.NewClientRepoFromClients(dbMap, clients) if err != nil { return nil, err } clientManager := clientmanager.NewClientManager(clientRepo, db.TransactionFactory(dbMap), clientmanager.ManagerOptions{ClientIDGenerator: clientIDGenerator, SecretGenerator: secGen}) km := key.NewPrivateKeyManager() err = km.Set(key.NewPrivateKeySet([]*key.PrivateKey{testPrivKey}, time.Now().Add(time.Minute))) if err != nil { return nil, err } tpl, err := getTemplates("dex", "https://coreos.com", "https://coreos.com/assets/images/brand/coreos-mark-30px.png", true, templatesLocation) if err != nil { return nil, err } srv := &Server{ IssuerURL: testIssuerURL, SessionManager: sessionManager, ClientRepo: clientRepo, Templates: tpl, UserRepo: userRepo, PasswordInfoRepo: pwRepo, UserManager: userManager, ClientManager: clientManager, KeyManager: km, RefreshTokenRepo: refreshTokenRepo, } err = setTemplates(srv, tpl) if err != nil { return nil, err } for _, config := range connConfigs { if err := srv.AddConnector(config); err != nil { return nil, err } } srv.UserEmailer = useremail.NewUserEmailer(srv.UserRepo, srv.PasswordInfoRepo, srv.KeyManager.Signer, srv.SessionManager.ValidityWindow, srv.IssuerURL, emailer, srv.absURL(httpPathResetPassword), srv.absURL(httpPathEmailVerify), srv.absURL(httpPathAcceptInvitation), ) clientCreds := map[string]oidc.ClientCredentials{} for _, c := range clients { clientCreds[c.Client.Credentials.ID] = c.Client.Credentials } return &testFixtures{ srv: srv, redirectURL: testRedirectURL, userRepo: userRepo, sessionManager: sessionManager, emailer: emailer, clientRepo: clientRepo, clientManager: clientManager, clientCreds: clientCreds, }, nil }