func makeTestFixtures() *testFixtures { f := &testFixtures{} dbMap := db.NewMemDB() f.ur = func() user.UserRepo { repo, err := db.NewUserRepoFromUsers(dbMap, []user.UserWithRemoteIdentities{ { User: user.User{ ID: "ID-1", Email: "*****@*****.**", DisplayName: "Name-1", }, }, { User: user.User{ ID: "ID-2", Email: "*****@*****.**", DisplayName: "Name-2", }, }, }) if err != nil { panic("Failed to create user repo: " + err.Error()) } return repo }() f.pwr = func() user.PasswordInfoRepo { repo, err := db.NewPasswordInfoRepoFromPasswordInfos(dbMap, []user.PasswordInfo{ { UserID: "ID-1", Password: []byte("hi."), }, }) if err != nil { panic("Failed to create user repo: " + err.Error()) } return repo }() f.ccr = func() connector.ConnectorConfigRepo { c := []connector.ConnectorConfig{&connector.LocalConnectorConfig{ID: "local"}} repo := db.NewConnectorConfigRepo(dbMap) if err := repo.Set(c); err != nil { panic(err) } return repo }() f.mgr = manager.NewUserManager(f.ur, f.pwr, f.ccr, db.TransactionFactory(dbMap), manager.ManagerOptions{}) f.cm = clientmanager.NewClientManager(f.cr, db.TransactionFactory(dbMap), clientmanager.ManagerOptions{}) f.adAPI = NewAdminAPI(f.ur, f.pwr, f.cr, f.ccr, f.mgr, f.cm, "local") return f }
func makeTestFixtures() *testFixtures { f := &testFixtures{} dbMap := db.NewMemDB() clients := []client.Client{ { Credentials: oidc.ClientCredentials{ ID: "client.example.com", Secret: goodSecret, }, Metadata: oidc.ClientMetadata{ RedirectURIs: []url.URL{ {Scheme: "http", Host: "client.example.com", Path: "/"}, }, }, Admin: true, }, } clientIDGenerator := func(hostport string) (string, error) { return hostport, nil } secGen := func() ([]byte, error) { return []byte("secret"), nil } f.clientRepo = db.NewClientRepo(dbMap) clientManager, err := NewClientManagerFromClients(f.clientRepo, db.TransactionFactory(dbMap), clients, ManagerOptions{ClientIDGenerator: clientIDGenerator, SecretGenerator: secGen}) if err != nil { panic("Failed to create client manager: " + err.Error()) } f.mgr = clientManager return f }
func TestServerTokenUnrecognizedKey(t *testing.T) { ci := client.Client{ Credentials: oidc.ClientCredentials{ ID: testClientID, Secret: clientTestSecret, }, Metadata: oidc.ClientMetadata{ RedirectURIs: []url.URL{ validRedirURL, }, }, } clients := []client.Client{ci} dbm := db.NewMemDB() clientIDGenerator := func(hostport string) (string, error) { return hostport, nil } secGen := func() ([]byte, error) { return []byte("secret"), nil } clientRepo := db.NewClientRepo(dbm) clientManager, err := clientmanager.NewClientManagerFromClients(clientRepo, db.TransactionFactory(dbm), clients, clientmanager.ManagerOptions{ClientIDGenerator: clientIDGenerator, SecretGenerator: secGen}) if err != nil { t.Fatalf("Failed to create client identity manager: %v", err) } km := &StaticKeyManager{ signer: &StaticSigner{sig: []byte("beer"), err: nil}, } sm := manager.NewSessionManager(db.NewSessionRepo(db.NewMemDB()), db.NewSessionKeyRepo(db.NewMemDB())) srv := &Server{ IssuerURL: url.URL{Scheme: "http", Host: "server.example.com"}, KeyManager: km, SessionManager: sm, ClientRepo: clientRepo, ClientManager: clientManager, } sessionID, err := sm.NewSession("connector_id", ci.Credentials.ID, "bogus", url.URL{}, "", false, []string{"openid", "offline_access"}) if err != nil { t.Fatalf("Unexpected error: %v", err) } _, err = sm.AttachRemoteIdentity(sessionID, oidc.Identity{}) if err != nil { t.Fatalf("Unexpected error: %v", err) } jwt, token, err := srv.CodeToken(ci.Credentials, "foo") if err == nil { t.Fatalf("Expected non-nil error") } if jwt != nil { t.Fatalf("Expected nil jwt") } if token != "" { t.Fatalf("Expected empty refresh token") } }
func makeUserObjects(users []user.UserWithRemoteIdentities, passwords []user.PasswordInfo) (*gorp.DbMap, user.UserRepo, user.PasswordInfoRepo, *manager.UserManager) { dbMap := db.NewMemDB() ur := func() user.UserRepo { repo, err := db.NewUserRepoFromUsers(dbMap, users) if err != nil { panic("Failed to create user repo: " + err.Error()) } return repo }() pwr := func() user.PasswordInfoRepo { repo, err := db.NewPasswordInfoRepoFromPasswordInfos(dbMap, passwords) if err != nil { panic("Failed to create password info repo: " + err.Error()) } return repo }() ccr := func() connector.ConnectorConfigRepo { repo := db.NewConnectorConfigRepo(dbMap) c := []connector.ConnectorConfig{&connector.LocalConnectorConfig{ID: "local"}} if err := repo.Set(c); err != nil { panic(err) } return repo }() um := manager.NewUserManager(ur, pwr, ccr, db.TransactionFactory(dbMap), manager.ManagerOptions{}) um.Clock = clock return dbMap, ur, pwr, um }
func makeAdminAPITestFixtures() *adminAPITestFixtures { f := &adminAPITestFixtures{} dbMap, ur, pwr, um := makeUserObjects(adminUsers, adminPasswords) var cliCount int secGen := func() ([]byte, error) { id := []byte(fmt.Sprintf("client_%v", cliCount)) cliCount++ return id, nil } cr := db.NewClientRepo(dbMap) clientIDGenerator := func(hostport string) (string, error) { return fmt.Sprintf("client_%v", hostport), nil } cm := manager.NewClientManager(cr, db.TransactionFactory(dbMap), manager.ManagerOptions{SecretGenerator: secGen, ClientIDGenerator: clientIDGenerator}) ccr := db.NewConnectorConfigRepo(dbMap) f.cr = cr f.ur = ur f.pwr = pwr f.adAPI = admin.NewAdminAPI(ur, pwr, cr, ccr, um, cm, "local") f.adSrv = server.NewAdminServer(f.adAPI, nil, adminAPITestSecret) f.hSrv = httptest.NewServer(f.adSrv.HTTPHandler()) f.hc = &http.Client{ Transport: &adminAPITransport{ secret: adminAPITestSecret, }, } f.adClient, _ = adminschema.NewWithBasePath(f.hc, f.hSrv.URL) return f }
// TestClientSample makes sure that the clients.json.sample file is valid and can be loaded properly. func TestClientSample(t *testing.T) { f, err := os.Open(clientsFile) if err != nil { t.Fatalf("could not open file %q: %v", clientsFile, err) } defer f.Close() clients, err := client.ClientsFromReader(f) if err != nil { t.Fatalf("Error loading Clients: %v", err) } memDB := db.NewMemDB() repo, err := db.NewClientRepoFromClients(memDB, clients) if err != nil { t.Fatalf("Error creating Clients: %v", err) } mgr := manager.NewClientManager(repo, db.TransactionFactory(memDB), manager.ManagerOptions{}) for i, c := range clients { ok, err := mgr.Authenticate(c.Client.Credentials) if !ok { t.Errorf("case %d: couldn't authenticate", i) } if err != nil { t.Errorf("case %d: error authenticating: %v", i, err) } } }
func (cfg *MultiServerConfig) Configure(srv *Server) error { if len(cfg.KeySecrets) == 0 { return errors.New("missing key secret") } if cfg.DatabaseConfig.DSN == "" { return errors.New("missing database connection string") } dbc, err := db.NewConnection(cfg.DatabaseConfig) if err != nil { return fmt.Errorf("unable to initialize database connection: %v", err) } if _, ok := dbc.Dialect.(gorp.PostgresDialect); !ok { return errors.New("only postgres backend supported for multi server configurations") } kRepo, err := db.NewPrivateKeySetRepo(dbc, cfg.UseOldFormat, cfg.KeySecrets...) if err != nil { return fmt.Errorf("unable to create PrivateKeySetRepo: %v", err) } ciRepo := db.NewClientRepo(dbc) sRepo := db.NewSessionRepo(dbc) skRepo := db.NewSessionKeyRepo(dbc) cfgRepo := db.NewConnectorConfigRepo(dbc) userRepo := db.NewUserRepo(dbc) pwiRepo := db.NewPasswordInfoRepo(dbc) userManager := usermanager.NewUserManager(userRepo, pwiRepo, cfgRepo, db.TransactionFactory(dbc), usermanager.ManagerOptions{}) clientManager := clientmanager.NewClientManager(ciRepo, db.TransactionFactory(dbc), clientmanager.ManagerOptions{}) refreshTokenRepo := db.NewRefreshTokenRepo(dbc) sm := sessionmanager.NewSessionManager(sRepo, skRepo) srv.ClientRepo = ciRepo srv.ClientManager = clientManager srv.KeySetRepo = kRepo srv.ConnectorConfigRepo = cfgRepo srv.UserRepo = userRepo srv.UserManager = userManager srv.PasswordInfoRepo = pwiRepo srv.SessionManager = sm srv.RefreshTokenRepo = refreshTokenRepo srv.HealthChecks = append(srv.HealthChecks, db.NewHealthChecker(dbc)) srv.dbMap = dbc return nil }
func TestDBClientRepoAuthenticate(t *testing.T) { c := connect(t) r := db.NewClientRepo(c) clientIDGenerator := func(hostport string) (string, error) { return hostport, nil } secGen := func() ([]byte, error) { return []byte("secret"), nil } m := manager.NewClientManager(r, db.TransactionFactory(c), manager.ManagerOptions{ClientIDGenerator: clientIDGenerator, SecretGenerator: secGen}) cm := oidc.ClientMetadata{ RedirectURIs: []url.URL{ url.URL{Scheme: "http", Host: "127.0.0.1:5556", Path: "/cb"}, }, } cli := client.Client{ Metadata: cm, } cc, err := m.New(cli, nil) if err != nil { t.Fatalf(err.Error()) } if cc.ID != "127.0.0.1:5556" { t.Fatalf("Returned ClientCredentials has incorrect ID: want=baz got=%s", cc.ID) } ok, err := m.Authenticate(*cc) if err != nil { t.Fatalf("Unexpected error: %v", err) } else if !ok { t.Fatalf("Authentication failed for good creds") } creds := []oidc.ClientCredentials{ // completely made up oidc.ClientCredentials{ID: "foo", Secret: "bar"}, // good client ID, bad secret oidc.ClientCredentials{ID: cc.ID, Secret: "bar"}, // bad client ID, good secret oidc.ClientCredentials{ID: "foo", Secret: cc.Secret}, // good client ID, secret with some fluff on the end oidc.ClientCredentials{ID: cc.ID, Secret: fmt.Sprintf("%sfluff", cc.Secret)}, } for i, c := range creds { ok, err := m.Authenticate(c) if err != nil { t.Errorf("case %d: unexpected error: %v", i, err) } else if ok { t.Errorf("case %d: authentication succeeded for bad creds", i) } } }
func TestCreate(t *testing.T) { dbm := db.NewMemDB() repo := db.NewClientRepo(dbm) manager := manager.NewClientManager(repo, db.TransactionFactory(dbm), manager.ManagerOptions{}) res := &clientResource{manager: manager} tests := [][]string{ []string{"http://example.com"}, []string{"https://example.com"}, []string{"http://example.com/foo"}, []string{"http://example.com/bar", "http://example.com/foo"}, } endpoint := "http://example.com/clients" for i, tt := range tests { body := strings.NewReader(fmt.Sprintf(`{"redirectURIs":["%s"]}`, strings.Join(tt, `","`))) r, err := http.NewRequest("POST", endpoint, body) if err != nil { t.Fatalf("Failed creating http.Request: %v", err) } r.Header.Set("content-type", "application/json") w := httptest.NewRecorder() res.ServeHTTP(w, r) if w.Code != http.StatusCreated { t.Errorf("case %d: invalid response code, want=%d, got=%d", i, http.StatusCreated, w.Code) } var client schema.ClientWithSecret if err := json.Unmarshal(w.Body.Bytes(), &client); err != nil { t.Errorf("case %d: unexpected error=%v", i, err) } if len(client.RedirectURIs) != len(tt) { t.Errorf("case %d: unexpected number of redirect URIs, want=%d, got=%d", i, len(tt), len(client.RedirectURIs)) } if !reflect.DeepEqual(tt, client.RedirectURIs) { t.Errorf("case %d: unexpected client redirect URIs: want=%v got=%v", i, tt, client.RedirectURIs) } if client.Id == "" { t.Errorf("case %d: empty client ID in response", i) } if client.Secret == "" { t.Errorf("case %d: empty client secret in response", i) } wantLoc := fmt.Sprintf("%s/%s", endpoint, client.Id) gotLoc := w.Header().Get("Location") if gotLoc != wantLoc { t.Errorf("case %d: invalid location header, want=%v, got=%v", i, wantLoc, gotLoc) } } }
func newDBConnector(dsn string) (*dbConnector, error) { dbc, err := db.NewConnection(db.Config{DSN: dsn}) if err != nil { return nil, err } dConn := &dbConnector{ cfgRepo: db.NewConnectorConfigRepo(dbc), ciManager: manager.NewClientManager(db.NewClientRepo(dbc), db.TransactionFactory(dbc), manager.ManagerOptions{}), } return dConn, nil }
func TestDBClientRepoMetadataNoExist(t *testing.T) { c := connect(t) r := db.NewClientRepo(c) m := manager.NewClientManager(r, db.TransactionFactory(c), manager.ManagerOptions{}) got, err := m.Metadata("noexist") if err != client.ErrorNotFound { t.Errorf("want==%q, got==%q", client.ErrorNotFound, err) } if got != nil { t.Fatalf("Retrieved incorrect ClientMetadata: want=nil got=%#v", got) } }
func makeClientRepoAndManager(dbMap *gorp.DbMap, clients []client.LoadableClient) (client.ClientRepo, *clientmanager.ClientManager, error) { 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, nil, err } clientManager := clientmanager.NewClientManager(clientRepo, db.TransactionFactory(dbMap), clientmanager.ManagerOptions{ClientIDGenerator: clientIDGenerator, SecretGenerator: secGen}) return clientRepo, clientManager, nil }
func newRefreshRepo(t *testing.T, users []user.UserWithRemoteIdentities, clients []client.Client) refresh.RefreshTokenRepo { var dbMap *gorp.DbMap if dsn := os.Getenv("DEX_TEST_DSN"); dsn == "" { dbMap = db.NewMemDB() } else { dbMap = connect(t) } if _, err := db.NewUserRepoFromUsers(dbMap, users); err != nil { t.Fatalf("Unable to add users: %v", err) } if _, err := manager.NewClientManagerFromClients(db.NewClientRepo(dbMap), db.TransactionFactory(dbMap), clients, manager.ManagerOptions{}); err != nil { t.Fatalf("Unable to add clients: %v", err) } return db.NewRefreshTokenRepo(dbMap) }
func TestServerLoginUnrecognizedSessionKey(t *testing.T) { clients := []client.Client{ client.Client{ Credentials: oidc.ClientCredentials{ ID: testClientID, Secret: clientTestSecret, }, Metadata: oidc.ClientMetadata{ RedirectURIs: []url.URL{ validRedirURL, }, }, }, } dbm := db.NewMemDB() clientIDGenerator := func(hostport string) (string, error) { return hostport, nil } secGen := func() ([]byte, error) { return []byte("secret"), nil } clientRepo := db.NewClientRepo(dbm) clientManager, err := clientmanager.NewClientManagerFromClients(clientRepo, db.TransactionFactory(dbm), clients, clientmanager.ManagerOptions{ClientIDGenerator: clientIDGenerator, SecretGenerator: secGen}) if err != nil { t.Fatalf("Failed to create client identity manager: %v", err) } km := &StaticKeyManager{ signer: &StaticSigner{sig: nil, err: errors.New("fail")}, } sm := manager.NewSessionManager(db.NewSessionRepo(db.NewMemDB()), db.NewSessionKeyRepo(db.NewMemDB())) srv := &Server{ IssuerURL: url.URL{Scheme: "http", Host: "server.example.com"}, KeyManager: km, SessionManager: sm, ClientRepo: clientRepo, ClientManager: clientManager, } ident := oidc.Identity{ID: "YYY", Name: "elroy", Email: "*****@*****.**"} code, err := srv.Login(ident, testClientID) if err == nil { t.Fatalf("Expected non-nil error") } if code != "" { t.Fatalf("Expected empty code, got=%s", code) } }
func (cfg *MultiServerConfig) Configure(srv *Server) error { if len(cfg.KeySecrets) == 0 { return errors.New("missing key secret") } if cfg.DatabaseConfig.DSN == "" { return errors.New("missing database connection string") } dbc, err := db.NewConnection(cfg.DatabaseConfig) if err != nil { return fmt.Errorf("unable to initialize database connection: %v", err) } kRepo, err := db.NewPrivateKeySetRepo(dbc, cfg.UseOldFormat, cfg.KeySecrets...) if err != nil { return fmt.Errorf("unable to create PrivateKeySetRepo: %v", err) } ciRepo := db.NewClientIdentityRepo(dbc) sRepo := db.NewSessionRepo(dbc) skRepo := db.NewSessionKeyRepo(dbc) cfgRepo := db.NewConnectorConfigRepo(dbc) userRepo := db.NewUserRepo(dbc) pwiRepo := db.NewPasswordInfoRepo(dbc) userManager := user.NewManager(userRepo, pwiRepo, db.TransactionFactory(dbc), user.ManagerOptions{}) refreshTokenRepo := db.NewRefreshTokenRepo(dbc) sm := session.NewSessionManager(sRepo, skRepo) srv.ClientIdentityRepo = ciRepo srv.KeySetRepo = kRepo srv.ConnectorConfigRepo = cfgRepo srv.UserRepo = userRepo srv.UserManager = userManager srv.PasswordInfoRepo = pwiRepo srv.SessionManager = sm srv.RefreshTokenRepo = refreshTokenRepo return 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 makeUserAPITestFixtures() *userAPITestFixtures { f := &userAPITestFixtures{} dbMap, _, _, um := makeUserObjects(userUsers, userPasswords) clients := []client.Client{ client.Client{ Credentials: oidc.ClientCredentials{ ID: testClientID, Secret: testClientSecret, }, Metadata: oidc.ClientMetadata{ RedirectURIs: []url.URL{ testRedirectURL, }, }, }, client.Client{ Credentials: oidc.ClientCredentials{ ID: userBadClientID, Secret: base64.URLEncoding.EncodeToString([]byte("secret")), }, Metadata: oidc.ClientMetadata{ RedirectURIs: []url.URL{ testBadRedirectURL, }, }, }, } clientIDGenerator := func(hostport string) (string, error) { return hostport, nil } secGen := func() ([]byte, error) { return []byte(testClientSecret), nil } clientRepo := db.NewClientRepo(dbMap) clientManager, err := manager.NewClientManagerFromClients(clientRepo, db.TransactionFactory(dbMap), clients, manager.ManagerOptions{ClientIDGenerator: clientIDGenerator, SecretGenerator: secGen}) if err != nil { panic("Failed to create client identity manager: " + err.Error()) } clientManager.SetDexAdmin(testClientID, true) noop := func() error { return nil } keysFunc := func() []key.PublicKey { return []key.PublicKey{*key.NewPublicKey(testPrivKey.JWK())} } jwtvFactory := func(clientID string) oidc.JWTVerifier { return oidc.NewJWTVerifier(testIssuerURL.String(), clientID, noop, keysFunc) } refreshRepo := db.NewRefreshTokenRepo(dbMap) for _, user := range userUsers { if _, err := refreshRepo.Create(user.User.ID, testClientID); err != nil { panic("Failed to create refresh token: " + err.Error()) } } f.emailer = &testEmailer{} um.Clock = clock api := api.NewUsersAPI(um, clientManager, refreshRepo, f.emailer, "local") usrSrv := server.NewUserMgmtServer(api, jwtvFactory, um, clientManager) f.hSrv = httptest.NewServer(usrSrv.HTTPHandler()) f.trans = &tokenHandlerTransport{ Handler: usrSrv.HTTPHandler(), Token: userGoodToken, } hc := &http.Client{ Transport: f.trans, } f.client, _ = schema.NewWithBasePath(hc, f.hSrv.URL) return f }
func TestCreateInvalidRequest(t *testing.T) { u := &url.URL{Scheme: "http", Host: "example.com", Path: "clients"} h := http.Header{"Content-Type": []string{"application/json"}} dbm := db.NewMemDB() repo := db.NewClientRepo(dbm) manager := manager.NewClientManager(repo, db.TransactionFactory(dbm), manager.ManagerOptions{}) res := &clientResource{manager: manager} tests := []struct { req *http.Request wantCode int wantBody string }{ // invalid content-type { req: &http.Request{Method: "POST", URL: u, Header: http.Header{"Content-Type": []string{"application/xml"}}}, wantCode: http.StatusBadRequest, wantBody: `{"error":"invalid_request","error_description":"unsupported content-type"}`, }, // invalid method { req: &http.Request{Method: "DELETE", URL: u, Header: h}, wantCode: http.StatusMethodNotAllowed, wantBody: `{"error":"invalid_request","error_description":"HTTP DELETE method not supported for this resource"}`, }, // invalid method { req: &http.Request{Method: "PUT", URL: u, Header: h}, wantCode: http.StatusMethodNotAllowed, wantBody: `{"error":"invalid_request","error_description":"HTTP PUT method not supported for this resource"}`, }, // invalid method { req: &http.Request{Method: "HEAD", URL: u, Header: h}, wantCode: http.StatusMethodNotAllowed, wantBody: `{"error":"invalid_request","error_description":"HTTP HEAD method not supported for this resource"}`, }, // unserializable body { req: &http.Request{Method: "POST", URL: u, Header: h, Body: makeBody("asdf")}, wantCode: http.StatusBadRequest, wantBody: `{"error":"invalid_request","error_description":"unable to decode request body"}`, }, // empty body { req: &http.Request{Method: "POST", URL: u, Header: h, Body: makeBody("")}, wantCode: http.StatusBadRequest, wantBody: `{"error":"invalid_request","error_description":"unable to decode request body"}`, }, // missing url field { req: &http.Request{Method: "POST", URL: u, Header: h, Body: makeBody(`{"id":"foo"}`)}, wantCode: http.StatusBadRequest, wantBody: `{"error":"invalid_client_metadata","error_description":"zero redirect URLs"}`, }, // empty url array { req: &http.Request{Method: "POST", URL: u, Header: h, Body: makeBody(`{"redirectURIs":[]}`)}, wantCode: http.StatusBadRequest, wantBody: `{"error":"invalid_client_metadata","error_description":"zero redirect URLs"}`, }, // array with empty string { req: &http.Request{Method: "POST", URL: u, Header: h, Body: makeBody(`{"redirectURIs":[""]}`)}, wantCode: http.StatusBadRequest, wantBody: `{"error":"invalid_client_metadata","error_description":"missing or invalid field: redirectURIs"}`, }, // uri with unusable scheme { req: &http.Request{Method: "POST", URL: u, Header: h, Body: makeBody(`{"redirectURIs":["asdf.com"]}`)}, wantCode: http.StatusBadRequest, wantBody: `{"error":"invalid_client_metadata","error_description":"no host for uri field redirect_uris"}`, }, // uri missing host { req: &http.Request{Method: "POST", URL: u, Header: h, Body: makeBody(`{"redirectURIs":["http://"]}`)}, wantCode: http.StatusBadRequest, wantBody: `{"error":"invalid_client_metadata","error_description":"no host for uri field redirect_uris"}`, }, } for i, tt := range tests { w := httptest.NewRecorder() res.ServeHTTP(w, tt.req) if w.Code != tt.wantCode { t.Errorf("case %d: invalid response code, want=%d, got=%d", i, tt.wantCode, w.Code) } gotBody := w.Body.String() if gotBody != tt.wantBody { t.Errorf("case %d: invalid response body, want=%s, got=%s", i, tt.wantBody, gotBody) } } }
func TestList(t *testing.T) { b64Encode := func(s string) string { return base64.URLEncoding.EncodeToString([]byte(s)) } tests := []struct { cs []client.Client want []*schema.Client }{ // empty repo { cs: nil, want: nil, }, // single client { cs: []client.Client{ client.Client{ Credentials: oidc.ClientCredentials{ID: "example.com", Secret: b64Encode("secret")}, Metadata: oidc.ClientMetadata{ RedirectURIs: []url.URL{ url.URL{Scheme: "http", Host: "example.com"}, }, }, }, }, want: []*schema.Client{ &schema.Client{ Id: "example.com", RedirectURIs: []string{"http://example.com"}, }, }, }, // multi client { cs: []client.Client{ client.Client{ Credentials: oidc.ClientCredentials{ID: "example.com", Secret: b64Encode("secret")}, Metadata: oidc.ClientMetadata{ RedirectURIs: []url.URL{ url.URL{Scheme: "http", Host: "example.com"}, }, }, }, client.Client{ Credentials: oidc.ClientCredentials{ID: "example2.com", Secret: b64Encode("secret")}, Metadata: oidc.ClientMetadata{ RedirectURIs: []url.URL{ url.URL{Scheme: "https", Host: "example2.com", Path: "one/two/three"}, }, }, }, }, want: []*schema.Client{ &schema.Client{ Id: "example2.com", RedirectURIs: []string{"https://example2.com/one/two/three"}, }, &schema.Client{ Id: "example.com", RedirectURIs: []string{"http://example.com"}, }, }, }, } for i, tt := range tests { dbm := db.NewMemDB() clientIDGenerator := func(hostport string) (string, error) { return hostport, nil } secGen := func() ([]byte, error) { return []byte("secret"), nil } clientRepo := db.NewClientRepo(dbm) clientManager, err := manager.NewClientManagerFromClients(clientRepo, db.TransactionFactory(dbm), tt.cs, manager.ManagerOptions{ClientIDGenerator: clientIDGenerator, SecretGenerator: secGen}) if err != nil { t.Fatalf("Failed to create client identity manager: %v", err) continue } res := &clientResource{manager: clientManager} r, err := http.NewRequest("GET", "http://example.com/clients", nil) if err != nil { t.Fatalf("Failed creating http.Request: %v", err) } w := httptest.NewRecorder() res.ServeHTTP(w, r) if w.Code != http.StatusOK { t.Errorf("case %d: invalid response code, want=%d, got=%d", i, http.StatusOK, w.Code) } var resp schema.ClientPage if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { t.Errorf("case %d: unexpected error=%v", i, err) } sort.Sort(byClientId(tt.want)) sort.Sort(byClientId(resp.Clients)) if diff := pretty.Compare(tt.want, resp.Clients); diff != "" { t.Errorf("case %d: invalid response body: %s", i, diff) } } }
func makeTestFixtures(clientCredsFlag bool) (*UsersAPI, *testEmailer) { dbMap := db.NewMemDB() ur := func() user.UserRepo { repo, err := db.NewUserRepoFromUsers(dbMap, []user.UserWithRemoteIdentities{ { User: user.User{ ID: "ID-1", Email: "*****@*****.**", Admin: true, CreatedAt: clock.Now(), }, }, { User: user.User{ ID: "ID-2", Email: "*****@*****.**", EmailVerified: true, CreatedAt: clock.Now(), }, }, { User: user.User{ ID: "ID-3", Email: "*****@*****.**", CreatedAt: clock.Now(), }, }, { User: user.User{ ID: "ID-4", Email: "*****@*****.**", CreatedAt: clock.Now(), Disabled: true, }, }, }) if err != nil { panic("Failed to create user repo: " + err.Error()) } return repo }() pwr := func() user.PasswordInfoRepo { repo, err := db.NewPasswordInfoRepoFromPasswordInfos(dbMap, []user.PasswordInfo{ { UserID: "ID-1", Password: []byte("password-1"), }, { UserID: "ID-2", Password: []byte("password-2"), }, }) if err != nil { panic("Failed to create user repo: " + err.Error()) } return repo }() ccr := func() connector.ConnectorConfigRepo { repo := db.NewConnectorConfigRepo(dbMap) c := []connector.ConnectorConfig{ &connector.LocalConnectorConfig{ID: "local"}, } if err := repo.Set(c); err != nil { panic(err) } return repo }() mgr := manager.NewUserManager(ur, pwr, ccr, db.TransactionFactory(dbMap), manager.ManagerOptions{}) mgr.Clock = clock ci := client.Client{ Credentials: oidc.ClientCredentials{ ID: goodClientID, Secret: base64.URLEncoding.EncodeToString([]byte("secret")), }, Metadata: oidc.ClientMetadata{ RedirectURIs: []url.URL{ validRedirURL, }, }, } ci2 := client.Client{ Credentials: oidc.ClientCredentials{ ID: nonAdminClientID, Secret: base64.URLEncoding.EncodeToString([]byte("anothersecret")), }, Metadata: oidc.ClientMetadata{ RedirectURIs: []url.URL{ validRedirURL2, }, }, } clientIDGenerator := func(hostport string) (string, error) { return hostport, nil } secGen := func() ([]byte, error) { return []byte("secret"), nil } clientRepo, err := db.NewClientRepoFromClients(dbMap, []client.LoadableClient{{Client: ci}, {Client: ci2}}) if err != nil { panic("Failed to create client manager: " + err.Error()) } clientManager := clientmanager.NewClientManager(clientRepo, db.TransactionFactory(dbMap), clientmanager.ManagerOptions{ClientIDGenerator: clientIDGenerator, SecretGenerator: secGen}) // Used in TestRevokeRefreshToken test. refreshTokens := []struct { clientID string userID string }{ {goodClientID, "ID-1"}, {goodClientID, "ID-2"}, } refreshRepo := db.NewRefreshTokenRepo(dbMap) for _, token := range refreshTokens { if _, err := refreshRepo.Create(token.userID, token.clientID, "local", []string{"openid"}); err != nil { panic("Failed to create refresh token: " + err.Error()) } } emailer := &testEmailer{} api := NewUsersAPI(mgr, clientManager, refreshRepo, emailer, "local", clientCredsFlag) return api, emailer }
func TestServerLogin(t *testing.T) { ci := client.Client{ Credentials: oidc.ClientCredentials{ ID: testClientID, Secret: clientTestSecret, }, Metadata: oidc.ClientMetadata{ RedirectURIs: []url.URL{ url.URL{ Scheme: "http", Host: "client.example.com", Path: "/callback", }, }, }, } dbm := db.NewMemDB() clientRepo := db.NewClientRepo(dbm) clientManager, err := clientmanager.NewClientManagerFromClients(clientRepo, db.TransactionFactory(dbm), []client.Client{ci}, clientmanager.ManagerOptions{}) if err != nil { t.Fatalf("Failed to create client identity manager: %v", err) } km := &StaticKeyManager{ signer: &StaticSigner{sig: []byte("beer"), err: nil}, } sm := manager.NewSessionManager(db.NewSessionRepo(db.NewMemDB()), db.NewSessionKeyRepo(db.NewMemDB())) sm.GenerateCode = staticGenerateCodeFunc("fakecode") sessionID, err := sm.NewSession("test_connector_id", ci.Credentials.ID, "bogus", ci.Metadata.RedirectURIs[0], "", false, []string{"openid"}) if err != nil { t.Fatalf("Unexpected error: %v", err) } userRepo, err := makeNewUserRepo() if err != nil { t.Fatalf("Unexpected error: %v", err) } srv := &Server{ IssuerURL: url.URL{Scheme: "http", Host: "server.example.com"}, KeyManager: km, SessionManager: sm, ClientRepo: clientRepo, ClientManager: clientManager, UserRepo: userRepo, } ident := oidc.Identity{ID: "YYY", Name: "elroy", Email: "*****@*****.**"} key, err := sm.NewSessionKey(sessionID) if err != nil { t.Fatalf("Unexpected error: %v", err) } redirectURL, err := srv.Login(ident, key) if err != nil { t.Fatalf("Unexpected err from Server.Login: %v", err) } wantRedirectURL := "http://client.example.com/callback?code=fakecode&state=bogus" if wantRedirectURL != redirectURL { t.Fatalf("Unexpected redirectURL: want=%q, got=%q", wantRedirectURL, redirectURL) } }
func TestHandleAuthFuncResponsesSingleRedirectURL(t *testing.T) { idpcs := []connector.Connector{ &fakeConnector{loginURL: "http://fake.example.com"}, } dbm := db.NewMemDB() clients := []client.Client{ client.Client{ Credentials: oidc.ClientCredentials{ ID: "client.example.com", Secret: base64.URLEncoding.EncodeToString([]byte("secret")), }, Metadata: oidc.ClientMetadata{ RedirectURIs: []url.URL{ url.URL{Scheme: "http", Host: "client.example.com", Path: "/callback"}, }, }, }, } clientIDGenerator := func(hostport string) (string, error) { return hostport, nil } secGen := func() ([]byte, error) { return []byte("secret"), nil } clientRepo := db.NewClientRepo(dbm) clientManager, err := clientmanager.NewClientManagerFromClients(clientRepo, db.TransactionFactory(dbm), clients, clientmanager.ManagerOptions{ClientIDGenerator: clientIDGenerator, SecretGenerator: secGen}) if err != nil { t.Fatalf("Failed to create client identity manager: %v", err) } srv := &Server{ IssuerURL: url.URL{Scheme: "http", Host: "server.example.com"}, SessionManager: manager.NewSessionManager(db.NewSessionRepo(db.NewMemDB()), db.NewSessionKeyRepo(db.NewMemDB())), ClientRepo: clientRepo, ClientManager: clientManager, } tests := []struct { query url.Values wantCode int wantLocation string }{ // no redirect_uri provided, but client only has one, so it's usable { query: url.Values{ "response_type": []string{"code"}, "client_id": []string{"client.example.com"}, "connector_id": []string{"fake"}, "scope": []string{"openid"}, }, wantCode: http.StatusFound, wantLocation: "http://fake.example.com", }, // provided redirect_uri matches client { query: url.Values{ "response_type": []string{"code"}, "redirect_uri": []string{"http://client.example.com/callback"}, "client_id": []string{"client.example.com"}, "connector_id": []string{"fake"}, "scope": []string{"openid"}, }, wantCode: http.StatusFound, wantLocation: "http://fake.example.com", }, // provided redirect_uri does not match client { query: url.Values{ "response_type": []string{"code"}, "redirect_uri": []string{"http://unrecognized.example.com/callback"}, "client_id": []string{"client.example.com"}, "connector_id": []string{"fake"}, "scope": []string{"openid"}, }, wantCode: http.StatusBadRequest, }, // nonexistant client_id { query: url.Values{ "response_type": []string{"code"}, "redirect_uri": []string{"http://client.example.com/callback"}, "client_id": []string{"YYY"}, "connector_id": []string{"fake"}, "scope": []string{"openid"}, }, wantCode: http.StatusBadRequest, }, // unsupported response type, redirects back to client { query: url.Values{ "response_type": []string{"token"}, "client_id": []string{"client.example.com"}, "connector_id": []string{"fake"}, "scope": []string{"openid"}, }, wantCode: http.StatusFound, wantLocation: "http://client.example.com/callback?error=unsupported_response_type&state=", }, // no 'openid' in scope { query: url.Values{ "response_type": []string{"code"}, "redirect_uri": []string{"http://client.example.com/callback"}, "client_id": []string{"client.example.com"}, "connector_id": []string{"fake"}, }, wantCode: http.StatusBadRequest, }, // empty response_type { query: url.Values{ "redirect_uri": []string{"http://client.example.com/callback"}, "client_id": []string{"client.example.com"}, "connector_id": []string{"fake"}, "scope": []string{"openid"}, }, wantCode: http.StatusFound, wantLocation: "http://client.example.com/callback?error=unsupported_response_type&state=", }, // empty client_id { query: url.Values{ "response_type": []string{"code"}, "redirect_uri": []string{"http://unrecognized.example.com/callback"}, "connector_id": []string{"fake"}, "scope": []string{"openid"}, }, wantCode: http.StatusBadRequest, }, } for i, tt := range tests { hdlr := handleAuthFunc(srv, idpcs, nil, true) w := httptest.NewRecorder() u := fmt.Sprintf("http://server.example.com?%s", tt.query.Encode()) req, err := http.NewRequest("GET", u, nil) if err != nil { t.Errorf("case %d: unable to form HTTP request: %v", i, err) continue } hdlr.ServeHTTP(w, req) if tt.wantCode != w.Code { t.Errorf("case %d: HTTP code mismatch: want=%d got=%d", i, tt.wantCode, w.Code) continue } gotLocation := w.Header().Get("Location") if tt.wantLocation != gotLocation { t.Errorf("case %d: HTTP Location header mismatch: want=%s got=%s", i, tt.wantLocation, gotLocation) } } }
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, }, }, } clientIDGenerator := func(hostport string) (string, error) { return hostport, nil } secGen := func() ([]byte, error) { return []byte("secret"), nil } dbMap := db.NewMemDB() clientRepo := db.NewClientRepo(dbMap) clientManager, err := clientmanager.NewClientManagerFromClients(clientRepo, db.TransactionFactory(dbMap), []client.Client{ci}, clientmanager.ManagerOptions{ClientIDGenerator: clientIDGenerator, SecretGenerator: secGen}) 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"}) 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 main() { fs := flag.NewFlagSet("dex-overlord", flag.ExitOnError) keySecrets := pflag.NewBase64List(32) fs.Var(keySecrets, "key-secrets", "A comma-separated list of base64 encoded 32 byte strings used as symmetric keys used to encrypt/decrypt signing key data in DB. The first key is considered the active key and used for encryption, while the others are used to decrypt.") useOldFormat := fs.Bool("use-deprecated-secret-format", false, "In prior releases, the database used AES-CBC to encrypt keys. New deployments should use the default AES-GCM encryption.") dbURL := fs.String("db-url", "", "DSN-formatted database connection string") dbMigrate := fs.Bool("db-migrate", true, "perform database migrations when starting up overlord. This includes the initial DB objects creation.") keyPeriod := fs.Duration("key-period", 24*time.Hour, "length of time for-which a given key will be valid") gcInterval := fs.Duration("gc-interval", time.Hour, "length of time between garbage collection runs") adminListen := fs.String("admin-listen", "http://127.0.0.1:5557", "scheme, host and port for listening for administrative operation requests ") adminAPISecret := pflag.NewBase64(server.AdminAPISecretLength) fs.Var(adminAPISecret, "admin-api-secret", fmt.Sprintf("A base64-encoded %d byte string which is used to protect the Admin API.", server.AdminAPISecretLength)) localConnectorID := fs.String("local-connector", "local", "ID of the local connector") logDebug := fs.Bool("log-debug", false, "log debug-level information") logTimestamps := fs.Bool("log-timestamps", false, "prefix log lines with timestamps") printVersion := fs.Bool("version", false, "Print the version and exit") if err := fs.Parse(os.Args[1:]); err != nil { fmt.Fprintln(os.Stderr, err.Error()) os.Exit(1) } if err := pflag.SetFlagsFromEnv(fs, "DEX_OVERLORD"); err != nil { fmt.Fprintln(os.Stderr, err.Error()) os.Exit(1) } if *printVersion { fmt.Printf("dex version %s\ngo version %s\n", strings.TrimPrefix(version, "v"), strings.TrimPrefix(runtime.Version(), "go")) os.Exit(0) } if *logDebug { log.EnableDebug() } if *logTimestamps { log.EnableTimestamps() } adminURL, err := url.Parse(*adminListen) if err != nil { log.Fatalf("Unable to use --admin-listen flag: %v", err) } if len(keySecrets.BytesSlice()) == 0 { log.Fatalf("Must specify at least one key secret") } dbCfg := db.Config{ DSN: *dbURL, MaxIdleConnections: 1, MaxOpenConnections: 1, } dbc, err := db.NewConnection(dbCfg) if err != nil { log.Fatalf(err.Error()) } if *dbMigrate { var sleep time.Duration for { var err error var migrations int if migrations, err = db.MigrateToLatest(dbc); err == nil { log.Infof("Performed %d db migrations", migrations) break } sleep = ptime.ExpBackoff(sleep, time.Minute) log.Errorf("Unable to migrate database, retrying in %v: %v", sleep, err) time.Sleep(sleep) } } userRepo := db.NewUserRepo(dbc) pwiRepo := db.NewPasswordInfoRepo(dbc) connCfgRepo := db.NewConnectorConfigRepo(dbc) userManager := manager.NewUserManager(userRepo, pwiRepo, connCfgRepo, db.TransactionFactory(dbc), manager.ManagerOptions{}) adminAPI := admin.NewAdminAPI(userManager, userRepo, pwiRepo, *localConnectorID) kRepo, err := db.NewPrivateKeySetRepo(dbc, *useOldFormat, keySecrets.BytesSlice()...) if err != nil { log.Fatalf(err.Error()) } var sleep time.Duration for { var done bool _, err := kRepo.Get() switch err { case nil: done = true case key.ErrorNoKeys: done = true case db.ErrorCannotDecryptKeys: log.Fatalf("Cannot decrypt keys using any of the given key secrets. The key secrets must be changed to include one that can decrypt the existing keys, or the existing keys must be deleted.") } if done { break } sleep = ptime.ExpBackoff(sleep, time.Minute) log.Errorf("Unable to get keys from repository, retrying in %v: %v", sleep, err) time.Sleep(sleep) } krot := key.NewPrivateKeyRotator(kRepo, *keyPeriod) s := server.NewAdminServer(adminAPI, krot, adminAPISecret.String()) h := s.HTTPHandler() httpsrv := &http.Server{ Addr: adminURL.Host, Handler: h, } gc := db.NewGarbageCollector(dbc, *gcInterval) log.Infof("Binding to %s...", httpsrv.Addr) go func() { log.Fatal(httpsrv.ListenAndServe()) }() gc.Run() <-krot.Run() }
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 TestServerRefreshToken(t *testing.T) { issuerURL := url.URL{Scheme: "http", Host: "server.example.com"} clientA := client.Client{ Credentials: oidc.ClientCredentials{ ID: testClientID, Secret: clientTestSecret, }, Metadata: oidc.ClientMetadata{ RedirectURIs: []url.URL{ url.URL{Scheme: "https", Host: "client.example.com", Path: "one/two/three"}, }, }, } clientB := client.Client{ Credentials: oidc.ClientCredentials{ ID: "example2.com", Secret: clientTestSecret, }, Metadata: oidc.ClientMetadata{ RedirectURIs: []url.URL{ url.URL{Scheme: "https", Host: "example2.com", Path: "one/two/three"}, }, }, } signerFixture := &StaticSigner{sig: []byte("beer"), err: nil} // NOTE(ericchiang): These tests assume that the database ID of the first // refresh token will be "1". tests := []struct { token string clientID string // The client that associates with the token. creds oidc.ClientCredentials signer jose.Signer err error }{ // Everything is good. { fmt.Sprintf("1/%s", base64.URLEncoding.EncodeToString([]byte("refresh-1"))), clientA.Credentials.ID, clientA.Credentials, signerFixture, nil, }, // Invalid refresh token(malformatted). { "invalid-token", clientA.Credentials.ID, clientA.Credentials, signerFixture, oauth2.NewError(oauth2.ErrorInvalidRequest), }, // Invalid refresh token(invalid payload content). { fmt.Sprintf("1/%s", base64.URLEncoding.EncodeToString([]byte("refresh-2"))), clientA.Credentials.ID, clientA.Credentials, signerFixture, oauth2.NewError(oauth2.ErrorInvalidRequest), }, // Invalid refresh token(invalid ID content). { fmt.Sprintf("0/%s", base64.URLEncoding.EncodeToString([]byte("refresh-1"))), clientA.Credentials.ID, clientA.Credentials, signerFixture, oauth2.NewError(oauth2.ErrorInvalidRequest), }, // Invalid client(client is not associated with the token). { fmt.Sprintf("1/%s", base64.URLEncoding.EncodeToString([]byte("refresh-1"))), clientA.Credentials.ID, clientB.Credentials, signerFixture, oauth2.NewError(oauth2.ErrorInvalidClient), }, // Invalid client(no client ID). { fmt.Sprintf("1/%s", base64.URLEncoding.EncodeToString([]byte("refresh-1"))), clientA.Credentials.ID, oidc.ClientCredentials{ID: "", Secret: "aaa"}, signerFixture, oauth2.NewError(oauth2.ErrorInvalidClient), }, // Invalid client(no such client). { fmt.Sprintf("1/%s", base64.URLEncoding.EncodeToString([]byte("refresh-1"))), clientA.Credentials.ID, oidc.ClientCredentials{ID: "AAA", Secret: "aaa"}, signerFixture, oauth2.NewError(oauth2.ErrorInvalidClient), }, // Invalid client(no secrets). { fmt.Sprintf("1/%s", base64.URLEncoding.EncodeToString([]byte("refresh-1"))), clientA.Credentials.ID, oidc.ClientCredentials{ID: testClientID}, signerFixture, oauth2.NewError(oauth2.ErrorInvalidClient), }, // Invalid client(invalid secret). { fmt.Sprintf("1/%s", base64.URLEncoding.EncodeToString([]byte("refresh-1"))), clientA.Credentials.ID, oidc.ClientCredentials{ID: "bad-id", Secret: "bad-secret"}, signerFixture, oauth2.NewError(oauth2.ErrorInvalidClient), }, // Signing operation fails. { fmt.Sprintf("1/%s", base64.URLEncoding.EncodeToString([]byte("refresh-1"))), clientA.Credentials.ID, clientA.Credentials, &StaticSigner{sig: nil, err: errors.New("fail")}, oauth2.NewError(oauth2.ErrorServerError), }, } for i, tt := range tests { km := &StaticKeyManager{ signer: tt.signer, } clients := []client.Client{ clientA, clientB, } clientIDGenerator := func(hostport string) (string, error) { return hostport, nil } secGen := func() ([]byte, error) { return []byte("secret"), nil } dbm := db.NewMemDB() clientRepo := db.NewClientRepo(dbm) clientManager, err := clientmanager.NewClientManagerFromClients(clientRepo, db.TransactionFactory(dbm), clients, clientmanager.ManagerOptions{ClientIDGenerator: clientIDGenerator, SecretGenerator: secGen}) if err != nil { t.Fatalf("Failed to create client identity manager: %v", err) } userRepo, err := makeNewUserRepo() if err != nil { t.Fatalf("Unexpected error: %v", err) } refreshTokenRepo := refreshtest.NewTestRefreshTokenRepo() srv := &Server{ IssuerURL: issuerURL, KeyManager: km, ClientRepo: clientRepo, ClientManager: clientManager, UserRepo: userRepo, RefreshTokenRepo: refreshTokenRepo, } if _, err := refreshTokenRepo.Create("testid-1", tt.clientID); err != nil { t.Fatalf("Unexpected error: %v", err) } jwt, err := srv.RefreshToken(tt.creds, tt.token) if !reflect.DeepEqual(err, tt.err) { t.Errorf("Case %d: expect: %v, got: %v", i, tt.err, err) } if jwt != nil { if string(jwt.Signature) != "beer" { t.Errorf("Case %d: expect signature: beer, got signature: %v", i, jwt.Signature) } claims, err := jwt.Claims() if err != nil { t.Errorf("Case %d: unexpected error: %v", i, err) } if claims["iss"] != issuerURL.String() || claims["sub"] != "testid-1" || claims["aud"] != testClientID { t.Errorf("Case %d: invalid claims: %v", i, claims) } } } // Test that we should return error when user cannot be found after // verifying the token. km := &StaticKeyManager{ signer: signerFixture, } clients := []client.Client{ clientA, clientB, } clientIDGenerator := func(hostport string) (string, error) { return hostport, nil } secGen := func() ([]byte, error) { return []byte("secret"), nil } dbm := db.NewMemDB() clientRepo := db.NewClientRepo(dbm) clientManager, err := clientmanager.NewClientManagerFromClients(clientRepo, db.TransactionFactory(dbm), clients, clientmanager.ManagerOptions{ClientIDGenerator: clientIDGenerator, SecretGenerator: secGen}) if err != nil { t.Fatalf("Failed to create client identity manager: %v", err) } userRepo, err := makeNewUserRepo() if err != nil { t.Fatalf("Unexpected error: %v", err) } // Create a user that will be removed later. if err := userRepo.Create(nil, user.User{ ID: "testid-2", Email: "*****@*****.**", }); err != nil { t.Fatalf("Unexpected error: %v", err) } refreshTokenRepo := refreshtest.NewTestRefreshTokenRepo() srv := &Server{ IssuerURL: issuerURL, KeyManager: km, ClientRepo: clientRepo, ClientManager: clientManager, UserRepo: userRepo, RefreshTokenRepo: refreshTokenRepo, } if _, err := refreshTokenRepo.Create("testid-2", clientA.Credentials.ID); err != nil { t.Fatalf("Unexpected error: %v", err) } // Recreate the user repo to remove the user we created. userRepo, err = makeNewUserRepo() if err != nil { t.Fatalf("Unexpected error: %v", err) } srv.UserRepo = userRepo _, err = srv.RefreshToken(clientA.Credentials, fmt.Sprintf("1/%s", base64.URLEncoding.EncodeToString([]byte("refresh-1")))) if !reflect.DeepEqual(err, oauth2.NewError(oauth2.ErrorServerError)) { t.Errorf("Expect: %v, got: %v", oauth2.NewError(oauth2.ErrorServerError), err) } }
func TestServerTokenFail(t *testing.T) { issuerURL := url.URL{Scheme: "http", Host: "server.example.com"} keyFixture := "goodkey" ccFixture := oidc.ClientCredentials{ ID: testClientID, Secret: clientTestSecret, } signerFixture := &StaticSigner{sig: []byte("beer"), err: nil} tests := []struct { signer jose.Signer argCC oidc.ClientCredentials argKey string err error scope []string refreshToken string }{ // control test case to make sure fixtures check out { // NOTE(ericchiang): This test assumes that the database ID of the first // refresh token will be "1". signer: signerFixture, argCC: ccFixture, argKey: keyFixture, scope: []string{"openid", "offline_access"}, refreshToken: fmt.Sprintf("1/%s", base64.URLEncoding.EncodeToString([]byte("refresh-1"))), }, // no 'offline_access' in 'scope', should get empty refresh token { signer: signerFixture, argCC: ccFixture, argKey: keyFixture, scope: []string{"openid"}, }, // unrecognized key { signer: signerFixture, argCC: ccFixture, argKey: "foo", err: oauth2.NewError(oauth2.ErrorInvalidGrant), scope: []string{"openid", "offline_access"}, }, // unrecognized client { signer: signerFixture, argCC: oidc.ClientCredentials{ID: "YYY"}, argKey: keyFixture, err: oauth2.NewError(oauth2.ErrorInvalidClient), scope: []string{"openid", "offline_access"}, }, // signing operation fails { signer: &StaticSigner{sig: nil, err: errors.New("fail")}, argCC: ccFixture, argKey: keyFixture, err: oauth2.NewError(oauth2.ErrorServerError), scope: []string{"openid", "offline_access"}, }, } for i, tt := range tests { sm := manager.NewSessionManager(db.NewSessionRepo(db.NewMemDB()), db.NewSessionKeyRepo(db.NewMemDB())) sm.GenerateCode = func() (string, error) { return keyFixture, nil } sessionID, err := sm.NewSession("connector_id", ccFixture.ID, "bogus", url.URL{}, "", false, tt.scope) if err != nil { t.Fatalf("Unexpected error: %v", err) } _, err = sm.AttachRemoteIdentity(sessionID, oidc.Identity{}) if err != nil { t.Errorf("case %d: unexpected error: %v", i, err) continue } km := &StaticKeyManager{ signer: tt.signer, } clients := []client.Client{ client.Client{ Credentials: ccFixture, Metadata: oidc.ClientMetadata{ RedirectURIs: []url.URL{ validRedirURL, }, }, }, } dbm := db.NewMemDB() clientIDGenerator := func(hostport string) (string, error) { return hostport, nil } secGen := func() ([]byte, error) { return []byte("secret"), nil } clientRepo := db.NewClientRepo(dbm) clientManager, err := clientmanager.NewClientManagerFromClients(clientRepo, db.TransactionFactory(dbm), clients, clientmanager.ManagerOptions{ClientIDGenerator: clientIDGenerator, SecretGenerator: secGen}) if err != nil { t.Fatalf("Failed to create client identity manager: %v", err) } _, err = sm.AttachUser(sessionID, "testid-1") if err != nil { t.Fatalf("case %d: unexpected error: %v", i, err) } userRepo, err := makeNewUserRepo() if err != nil { t.Fatalf("Unexpected error: %v", err) } refreshTokenRepo := refreshtest.NewTestRefreshTokenRepo() srv := &Server{ IssuerURL: issuerURL, KeyManager: km, SessionManager: sm, ClientRepo: clientRepo, ClientManager: clientManager, UserRepo: userRepo, RefreshTokenRepo: refreshTokenRepo, } _, err = sm.NewSessionKey(sessionID) if err != nil { t.Fatalf("Unexpected error: %v", err) } jwt, token, err := srv.CodeToken(tt.argCC, tt.argKey) if token != tt.refreshToken { fmt.Printf("case %d: expect refresh token %q, got %q\n", i, tt.refreshToken, token) t.Fatalf("case %d: expect refresh token %q, got %q", i, tt.refreshToken, token) panic("") } if !reflect.DeepEqual(err, tt.err) { t.Errorf("case %d: expect %v, got %v", i, tt.err, err) } if err == nil && jwt == nil { t.Errorf("case %d: got nil JWT", i) } if err != nil && jwt != nil { t.Errorf("case %d: got non-nil JWT %v", i, jwt) } } }
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 } manager := 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 } clientIdentityRepo, err := db.NewClientIdentityRepoFromClients(db.NewMemDB(), []oidc.ClientIdentity{ oidc.ClientIdentity{ Credentials: oidc.ClientCredentials{ ID: "XXX", Secret: base64.URLEncoding.EncodeToString([]byte("secrete")), }, Metadata: oidc.ClientMetadata{ RedirectURIs: []url.URL{ testRedirectURL, }, }, }, }) 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, 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), srv.absURL(httpPathAcceptInvitation), ) return &testFixtures{ srv: srv, redirectURL: testRedirectURL, userRepo: userRepo, sessionManager: sessionManager, emailer: emailer, clientIdentityRepo: clientIdentityRepo, }, nil }
func TestServerCodeToken(t *testing.T) { ci := client.Client{ Credentials: oidc.ClientCredentials{ ID: testClientID, Secret: clientTestSecret, }, Metadata: oidc.ClientMetadata{ RedirectURIs: []url.URL{ validRedirURL, }, }, } clients := []client.Client{ci} dbm := db.NewMemDB() clientIDGenerator := func(hostport string) (string, error) { return hostport, nil } secGen := func() ([]byte, error) { return []byte("secret"), nil } clientRepo := db.NewClientRepo(dbm) clientManager, err := clientmanager.NewClientManagerFromClients(clientRepo, db.TransactionFactory(dbm), clients, clientmanager.ManagerOptions{ClientIDGenerator: clientIDGenerator, SecretGenerator: secGen}) if err != nil { t.Fatalf("Failed to create client identity manager: %v", err) } km := &StaticKeyManager{ signer: &StaticSigner{sig: []byte("beer"), err: nil}, } sm := manager.NewSessionManager(db.NewSessionRepo(db.NewMemDB()), db.NewSessionKeyRepo(db.NewMemDB())) userRepo, err := makeNewUserRepo() if err != nil { t.Fatalf("Unexpected error: %v", err) } refreshTokenRepo := refreshtest.NewTestRefreshTokenRepo() srv := &Server{ IssuerURL: url.URL{Scheme: "http", Host: "server.example.com"}, KeyManager: km, SessionManager: sm, ClientRepo: clientRepo, ClientManager: clientManager, UserRepo: userRepo, RefreshTokenRepo: refreshTokenRepo, } tests := []struct { scope []string refreshToken string }{ // No 'offline_access' in scope, should get empty refresh token. { scope: []string{"openid"}, refreshToken: "", }, // Have 'offline_access' in scope, should get non-empty refresh token. { // NOTE(ericchiang): This test assumes that the database ID of the first // refresh token will be "1". scope: []string{"openid", "offline_access"}, refreshToken: fmt.Sprintf("1/%s", base64.URLEncoding.EncodeToString([]byte("refresh-1"))), }, } for i, tt := range tests { sessionID, err := sm.NewSession("bogus_idpc", ci.Credentials.ID, "bogus", url.URL{}, "", false, tt.scope) if err != nil { t.Fatalf("case %d: unexpected error: %v", i, err) } _, err = sm.AttachRemoteIdentity(sessionID, oidc.Identity{}) if err != nil { t.Fatalf("case %d: unexpected error: %v", i, err) } _, err = sm.AttachUser(sessionID, "testid-1") if err != nil { t.Fatalf("case %d: unexpected error: %v", i, err) } key, err := sm.NewSessionKey(sessionID) if err != nil { t.Fatalf("case %d: unexpected error: %v", i, err) } jwt, token, err := srv.CodeToken(ci.Credentials, key) if err != nil { t.Fatalf("case %d: unexpected error: %v", i, err) } if jwt == nil { t.Fatalf("case %d: expect non-nil jwt", i) } if token != tt.refreshToken { t.Fatalf("case %d: expect refresh token %q, got %q", i, tt.refreshToken, token) } } }
func TestServerLoginDisabledUser(t *testing.T) { ci := client.Client{ Credentials: oidc.ClientCredentials{ ID: testClientID, Secret: clientTestSecret, }, Metadata: oidc.ClientMetadata{ RedirectURIs: []url.URL{ validRedirURL, }, }, } clients := []client.Client{ci} dbm := db.NewMemDB() clientIDGenerator := func(hostport string) (string, error) { return hostport, nil } secGen := func() ([]byte, error) { return []byte("secret"), nil } clientRepo := db.NewClientRepo(dbm) clientManager, err := clientmanager.NewClientManagerFromClients(clientRepo, db.TransactionFactory(dbm), clients, clientmanager.ManagerOptions{ClientIDGenerator: clientIDGenerator, SecretGenerator: secGen}) if err != nil { t.Fatalf("Failed to create client identity manager: %v", err) } km := &StaticKeyManager{ signer: &StaticSigner{sig: []byte("beer"), err: nil}, } sm := manager.NewSessionManager(db.NewSessionRepo(db.NewMemDB()), db.NewSessionKeyRepo(db.NewMemDB())) sm.GenerateCode = staticGenerateCodeFunc("fakecode") sessionID, err := sm.NewSession("test_connector_id", ci.Credentials.ID, "bogus", ci.Metadata.RedirectURIs[0], "", false, []string{"openid"}) if err != nil { t.Fatalf("Unexpected error: %v", err) } userRepo, err := makeNewUserRepo() if err != nil { t.Fatalf("Unexpected error: %v", err) } err = userRepo.Create(nil, user.User{ ID: "disabled-1", Email: "*****@*****.**", Disabled: true, }) if err != nil { t.Fatalf("Unexpected error: %v", err) } err = userRepo.AddRemoteIdentity(nil, "disabled-1", user.RemoteIdentity{ ConnectorID: "test_connector_id", ID: "disabled-connector-id", }) srv := &Server{ IssuerURL: url.URL{Scheme: "http", Host: "server.example.com"}, KeyManager: km, SessionManager: sm, ClientRepo: clientRepo, ClientManager: clientManager, UserRepo: userRepo, } ident := oidc.Identity{ID: "disabled-connector-id", Name: "elroy", Email: "*****@*****.**"} key, err := sm.NewSessionKey(sessionID) if err != nil { t.Fatalf("Unexpected error: %v", err) } _, err = srv.Login(ident, key) if err == nil { t.Errorf("disabled user was allowed to log in") } }