func TestServerLoginUnrecognizedSessionKey(t *testing.T) { ciRepo := client.NewClientIdentityRepo([]oidc.ClientIdentity{ oidc.ClientIdentity{ Credentials: oidc.ClientCredentials{ ID: "XXX", Secret: "secrete", }, }, }) km := &StaticKeyManager{ signer: &StaticSigner{sig: nil, err: errors.New("fail")}, } sm := session.NewSessionManager(session.NewSessionRepo(), session.NewSessionKeyRepo()) srv := &Server{ IssuerURL: url.URL{Scheme: "http", Host: "server.example.com"}, KeyManager: km, SessionManager: sm, ClientIdentityRepo: ciRepo, } ident := oidc.Identity{ID: "YYY", Name: "elroy", Email: "*****@*****.**"} code, err := srv.Login(ident, "XXX") if err == nil { t.Fatalf("Expected non-nil error") } if code != "" { t.Fatalf("Expected empty code, got=%s", code) } }
func makeTestFixtures() (*UsersAPI, *testEmailer) { ur := user.NewUserRepoFromUsers([]user.UserWithRemoteIdentities{ { User: user.User{ ID: "ID-1", Email: "*****@*****.**", Admin: true, CreatedAt: clock.Now(), }, }, { User: user.User{ ID: "ID-2", Email: "*****@*****.**", 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, }, }, }) pwr := user.NewPasswordInfoRepoFromPasswordInfos([]user.PasswordInfo{ { UserID: "ID-1", Password: []byte("password-1"), }, { UserID: "ID-2", Password: []byte("password-2"), }, }) mgr := user.NewManager(ur, pwr, repo.InMemTransactionFactory, user.ManagerOptions{}) mgr.Clock = clock ci := oidc.ClientIdentity{ Credentials: oidc.ClientCredentials{ ID: "XXX", Secret: "secrete", }, Metadata: oidc.ClientMetadata{ RedirectURLs: []url.URL{ validRedirURL, }, }, } cir := client.NewClientIdentityRepo([]oidc.ClientIdentity{ci}) emailer := &testEmailer{} api := NewUsersAPI(mgr, cir, emailer, "local") return api, emailer }
func TestServerLogin(t *testing.T) { ci := oidc.ClientIdentity{ Credentials: oidc.ClientCredentials{ ID: "XXX", Secret: "secrete", }, Metadata: oidc.ClientMetadata{ RedirectURLs: []url.URL{ url.URL{ Scheme: "http", Host: "client.example.com", Path: "/callback", }, }, }, } ciRepo := client.NewClientIdentityRepo([]oidc.ClientIdentity{ci}) km := &StaticKeyManager{ signer: &StaticSigner{sig: []byte("beer"), err: nil}, } sm := session.NewSessionManager(session.NewSessionRepo(), session.NewSessionKeyRepo()) sm.GenerateCode = staticGenerateCodeFunc("fakecode") sessionID, err := sm.NewSession("test_connector_id", ci.Credentials.ID, "bogus", ci.Metadata.RedirectURLs[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, ClientIdentityRepo: ciRepo, 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 makeUserAPITestFixtures() *userAPITestFixtures { f := &userAPITestFixtures{} _, _, um := makeUserObjects(userUsers, userPasswords) cir := client.NewClientIdentityRepo([]oidc.ClientIdentity{ oidc.ClientIdentity{ Credentials: oidc.ClientCredentials{ ID: testClientID, Secret: testClientSecret, }, Metadata: oidc.ClientMetadata{ RedirectURLs: []url.URL{ testRedirectURL, }, }, }, oidc.ClientIdentity{ Credentials: oidc.ClientCredentials{ ID: userBadClientID, Secret: "secret", }, Metadata: oidc.ClientMetadata{ RedirectURLs: []url.URL{ testRedirectURL, }, }, }, }) cir.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) } f.emailer = &testEmailer{} api := api.NewUsersAPI(um, cir, f.emailer, "local") usrSrv := server.NewUserMgmtServer(api, jwtvFactory, um, cir) 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 TestCreate(t *testing.T) { repo := client.NewClientIdentityRepo(nil) res := &clientResource{repo: repo} 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 TestServerTokenUnrecognizedKey(t *testing.T) { ci := oidc.ClientIdentity{ Credentials: oidc.ClientCredentials{ ID: "XXX", Secret: "secrete", }, } ciRepo := client.NewClientIdentityRepo([]oidc.ClientIdentity{ci}) km := &StaticKeyManager{ signer: &StaticSigner{sig: []byte("beer"), err: nil}, } sm := session.NewSessionManager(session.NewSessionRepo(), session.NewSessionKeyRepo()) srv := &Server{ IssuerURL: url.URL{Scheme: "http", Host: "server.example.com"}, KeyManager: km, SessionManager: sm, ClientIdentityRepo: ciRepo, } 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 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 TestServerCodeToken(t *testing.T) { ci := oidc.ClientIdentity{ Credentials: oidc.ClientCredentials{ ID: "XXX", Secret: "secrete", }, } ciRepo := client.NewClientIdentityRepo([]oidc.ClientIdentity{ci}) km := &StaticKeyManager{ signer: &StaticSigner{sig: []byte("beer"), err: nil}, } sm := session.NewSessionManager(session.NewSessionRepo(), session.NewSessionKeyRepo()) userRepo, err := makeNewUserRepo() if err != nil { t.Fatalf("Unexpected error: %v", err) } refreshTokenRepo, err := refreshtest.NewTestRefreshTokenRepo() if err != nil { t.Fatalf("Unexpected error: %v", err) } srv := &Server{ IssuerURL: url.URL{Scheme: "http", Host: "server.example.com"}, KeyManager: km, SessionManager: sm, ClientIdentityRepo: ciRepo, 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. { scope: []string{"openid", "offline_access"}, refreshToken: fmt.Sprintf("0/%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 TestClientToken(t *testing.T) { now := time.Now() tomorrow := now.Add(24 * time.Hour) validClientID := "valid-client" ci := oidc.ClientIdentity{ Credentials: oidc.ClientCredentials{ ID: validClientID, }, } repo := client.NewClientIdentityRepo([]oidc.ClientIdentity{ci}) privKey, err := key.GeneratePrivateKey() if err != nil { t.Fatalf("Failed to generate private key, error=%v", err) } signer := privKey.Signer() pubKey := *key.NewPublicKey(privKey.JWK()) validIss := "https://example.com" makeToken := func(iss, sub, aud string, iat, exp time.Time) string { claims := oidc.NewClaims(iss, sub, aud, iat, exp) jwt, err := jose.NewSignedJWT(claims, signer) if err != nil { t.Fatalf("Failed to generate JWT, error=%v", err) } return jwt.Encode() } validJWT := makeToken(validIss, validClientID, validClientID, now, tomorrow) invalidJWT := makeToken("", "", "", now, tomorrow) tests := []struct { keys []key.PublicKey repo client.ClientIdentityRepo header string wantCode int }{ // valid token { keys: []key.PublicKey{pubKey}, repo: repo, header: fmt.Sprintf("BEARER %s", validJWT), wantCode: http.StatusOK, }, // invalid token { keys: []key.PublicKey{pubKey}, repo: repo, header: fmt.Sprintf("BEARER %s", invalidJWT), wantCode: http.StatusUnauthorized, }, // empty header { keys: []key.PublicKey{pubKey}, repo: repo, header: "", wantCode: http.StatusUnauthorized, }, // unparsable token { keys: []key.PublicKey{pubKey}, repo: repo, header: "BEARER xxx", wantCode: http.StatusUnauthorized, }, // no verification keys { keys: []key.PublicKey{}, repo: repo, header: fmt.Sprintf("BEARER %s", validJWT), wantCode: http.StatusUnauthorized, }, // nil repo { keys: []key.PublicKey{pubKey}, repo: nil, header: fmt.Sprintf("BEARER %s", validJWT), wantCode: http.StatusUnauthorized, }, // empty repo { keys: []key.PublicKey{pubKey}, repo: client.NewClientIdentityRepo(nil), header: fmt.Sprintf("BEARER %s", validJWT), wantCode: http.StatusUnauthorized, }, // client not in repo { keys: []key.PublicKey{pubKey}, repo: repo, header: fmt.Sprintf("BEARER %s", makeToken(validIss, "DOESNT-EXIST", "DOESNT-EXIST", now, tomorrow)), wantCode: http.StatusUnauthorized, }, } for i, tt := range tests { w := httptest.NewRecorder() mw := &clientTokenMiddleware{ issuerURL: validIss, ciRepo: tt.repo, keysFunc: func() ([]key.PublicKey, error) { return tt.keys, nil }, next: staticHandler{}, } req := &http.Request{ Header: http.Header{ "Authorization": []string{tt.header}, }, } mw.ServeHTTP(w, req) if tt.wantCode != w.Code { t.Errorf("case %d: invalid response code, want=%d, got=%d", i, tt.wantCode, w.Code) } } }
func TestServerLoginDisabledUser(t *testing.T) { ci := oidc.ClientIdentity{ Credentials: oidc.ClientCredentials{ ID: "XXX", Secret: "secrete", }, Metadata: oidc.ClientMetadata{ RedirectURLs: []url.URL{ url.URL{ Scheme: "http", Host: "client.example.com", Path: "/callback", }, }, }, } ciRepo := client.NewClientIdentityRepo([]oidc.ClientIdentity{ci}) km := &StaticKeyManager{ signer: &StaticSigner{sig: []byte("beer"), err: nil}, } sm := session.NewSessionManager(session.NewSessionRepo(), session.NewSessionKeyRepo()) sm.GenerateCode = staticGenerateCodeFunc("fakecode") sessionID, err := sm.NewSession("test_connector_id", ci.Credentials.ID, "bogus", ci.Metadata.RedirectURLs[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, ClientIdentityRepo: ciRepo, 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") } }
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 TestHandleAuthFuncResponsesMultipleRedirectURLs(t *testing.T) { idpcs := []connector.Connector{ &fakeConnector{loginURL: "http://fake.example.com"}, } srv := &Server{ IssuerURL: url.URL{Scheme: "http", Host: "server.example.com"}, SessionManager: session.NewSessionManager(session.NewSessionRepo(), session.NewSessionKeyRepo()), ClientIdentityRepo: client.NewClientIdentityRepo([]oidc.ClientIdentity{ oidc.ClientIdentity{ Credentials: oidc.ClientCredentials{ ID: "XXX", Secret: "secrete", }, Metadata: oidc.ClientMetadata{ RedirectURLs: []url.URL{ url.URL{Scheme: "http", Host: "foo.example.com", Path: "/callback"}, url.URL{Scheme: "http", Host: "bar.example.com", Path: "/callback"}, }, }, }, }), } tests := []struct { query url.Values wantCode int wantLocation string }{ // provided redirect_uri matches client's first { query: url.Values{ "response_type": []string{"code"}, "redirect_uri": []string{"http://foo.example.com/callback"}, "client_id": []string{"XXX"}, "connector_id": []string{"fake"}, "scope": []string{"openid"}, }, wantCode: http.StatusTemporaryRedirect, wantLocation: "http://fake.example.com", }, // provided redirect_uri matches client's second { query: url.Values{ "response_type": []string{"code"}, "redirect_uri": []string{"http://bar.example.com/callback"}, "client_id": []string{"XXX"}, "connector_id": []string{"fake"}, "scope": []string{"openid"}, }, wantCode: http.StatusTemporaryRedirect, wantLocation: "http://fake.example.com", }, // provided redirect_uri does not match either of client's { query: url.Values{ "response_type": []string{"code"}, "redirect_uri": []string{"http://unrecognized.example.com/callback"}, "client_id": []string{"XXX"}, "connector_id": []string{"fake"}, "scope": []string{"openid"}, }, wantCode: http.StatusBadRequest, }, // no redirect_uri provided { query: url.Values{ "response_type": []string{"code"}, "client_id": []string{"XXX"}, "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) t.Errorf("case %d: BODY: %v", i, w.Body.String()) t.Errorf("case %d: LOCO: %v", i, w.HeaderMap.Get("Location")) 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 TestCreateInvalidRequest(t *testing.T) { u := &url.URL{Scheme: "http", Host: "example.com", Path: "clients"} h := http.Header{"Content-Type": []string{"application/json"}} repo := client.NewClientIdentityRepo(nil) res := &clientResource{repo: repo} 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":"invalid redirect URL: scheme not http/https"}`, }, // 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":"invalid redirect URL: host empty"}`, }, } 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 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{ PasswordInfos: []user.PasswordInfo{passwordInfo}, } ci := oidc.ClientIdentity{ Credentials: oidc.ClientCredentials{ ID: "72de74a9", Secret: "XXX", }, } cir := client.NewClientIdentityRepo([]oidc.ClientIdentity{ci}) issuerURL := url.URL{Scheme: "http", Host: "server.example.com"} sm := session.NewSessionManager(session.NewSessionRepo(), session.NewSessionKeyRepo()) 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 := user.NewUserRepo() if err := userRepo.Create(nil, usr); err != nil { t.Fatalf("Unexpected error: %v", err) } passwordInfoRepo := user.NewPasswordInfoRepo() refreshTokenRepo, err := refreshtest.NewTestRefreshTokenRepo() if err != nil { t.Fatalf("Unexpected error: %v", err) } srv := &server.Server{ IssuerURL: issuerURL, KeyManager: km, SessionManager: sm, ClientIdentityRepo: cir, 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: "http://client.example.com", 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 TestServerTokenFail(t *testing.T) { issuerURL := url.URL{Scheme: "http", Host: "server.example.com"} keyFixture := "goodkey" ccFixture := oidc.ClientCredentials{ ID: "XXX", Secret: "secrete", } 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 { signer: signerFixture, argCC: ccFixture, argKey: keyFixture, scope: []string{"openid", "offline_access"}, refreshToken: fmt.Sprintf("0/%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 := session.NewSessionManager(session.NewSessionRepo(), session.NewSessionKeyRepo()) 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, } ciRepo := client.NewClientIdentityRepo([]oidc.ClientIdentity{ oidc.ClientIdentity{Credentials: ccFixture}, }) _, 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, err := refreshtest.NewTestRefreshTokenRepo() if err != nil { t.Fatalf("Unexpected error: %v", err) } srv := &Server{ IssuerURL: issuerURL, KeyManager: km, SessionManager: sm, ClientIdentityRepo: ciRepo, 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 makeTestClientIdentityRepoMem(clients []oidc.ClientIdentity) client.ClientIdentityRepo { return client.NewClientIdentityRepo(clients) }
func TestHandleAuthFuncResponsesSingleRedirectURL(t *testing.T) { idpcs := []connector.Connector{ &fakeConnector{loginURL: "http://fake.example.com"}, } srv := &Server{ IssuerURL: url.URL{Scheme: "http", Host: "server.example.com"}, SessionManager: session.NewSessionManager(session.NewSessionRepo(), session.NewSessionKeyRepo()), ClientIdentityRepo: client.NewClientIdentityRepo([]oidc.ClientIdentity{ oidc.ClientIdentity{ Credentials: oidc.ClientCredentials{ ID: "XXX", Secret: "secrete", }, Metadata: oidc.ClientMetadata{ RedirectURLs: []url.URL{ url.URL{Scheme: "http", Host: "client.example.com", Path: "/callback"}, }, }, }, }), } 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{"XXX"}, "connector_id": []string{"fake"}, "scope": []string{"openid"}, }, wantCode: http.StatusTemporaryRedirect, 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{"XXX"}, "connector_id": []string{"fake"}, "scope": []string{"openid"}, }, wantCode: http.StatusTemporaryRedirect, 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{"XXX"}, "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{"XXX"}, "connector_id": []string{"fake"}, "scope": []string{"openid"}, }, wantCode: http.StatusTemporaryRedirect, 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{"XXX"}, "connector_id": []string{"fake"}, }, wantCode: http.StatusBadRequest, }, } for i, tt := range tests { hdlr := handleAuthFunc(srv, idpcs, nil) 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 TestServerRefreshToken(t *testing.T) { issuerURL := url.URL{Scheme: "http", Host: "server.example.com"} credXXX := oidc.ClientCredentials{ ID: "XXX", Secret: "secret", } credYYY := oidc.ClientCredentials{ ID: "YYY", Secret: "secret", } signerFixture := &StaticSigner{sig: []byte("beer"), err: nil} 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("0/%s", base64.URLEncoding.EncodeToString([]byte("refresh-1"))), "XXX", credXXX, signerFixture, nil, }, // Invalid refresh token(malformatted). { "invalid-token", "XXX", credXXX, signerFixture, oauth2.NewError(oauth2.ErrorInvalidRequest), }, // Invalid refresh token(invalid payload content). { fmt.Sprintf("0/%s", base64.URLEncoding.EncodeToString([]byte("refresh-2"))), "XXX", credXXX, signerFixture, oauth2.NewError(oauth2.ErrorInvalidRequest), }, // Invalid refresh token(invalid ID content). { fmt.Sprintf("1/%s", base64.URLEncoding.EncodeToString([]byte("refresh-1"))), "XXX", credXXX, signerFixture, oauth2.NewError(oauth2.ErrorInvalidRequest), }, // Invalid client(client is not associated with the token). { fmt.Sprintf("0/%s", base64.URLEncoding.EncodeToString([]byte("refresh-1"))), "XXX", credYYY, signerFixture, oauth2.NewError(oauth2.ErrorInvalidClient), }, // Invalid client(no client ID). { fmt.Sprintf("0/%s", base64.URLEncoding.EncodeToString([]byte("refresh-1"))), "XXX", oidc.ClientCredentials{ID: "", Secret: "aaa"}, signerFixture, oauth2.NewError(oauth2.ErrorInvalidClient), }, // Invalid client(no such client). { fmt.Sprintf("0/%s", base64.URLEncoding.EncodeToString([]byte("refresh-1"))), "XXX", oidc.ClientCredentials{ID: "AAA", Secret: "aaa"}, signerFixture, oauth2.NewError(oauth2.ErrorInvalidClient), }, // Invalid client(no secrets). { fmt.Sprintf("0/%s", base64.URLEncoding.EncodeToString([]byte("refresh-1"))), "XXX", oidc.ClientCredentials{ID: "XXX"}, signerFixture, oauth2.NewError(oauth2.ErrorInvalidClient), }, // Invalid client(invalid secret). { fmt.Sprintf("0/%s", base64.URLEncoding.EncodeToString([]byte("refresh-1"))), "XXX", oidc.ClientCredentials{ID: "XXX", Secret: "bad-secret"}, signerFixture, oauth2.NewError(oauth2.ErrorInvalidClient), }, // Signing operation fails. { fmt.Sprintf("0/%s", base64.URLEncoding.EncodeToString([]byte("refresh-1"))), "XXX", credXXX, &StaticSigner{sig: nil, err: errors.New("fail")}, oauth2.NewError(oauth2.ErrorServerError), }, } for i, tt := range tests { km := &StaticKeyManager{ signer: tt.signer, } ciRepo := client.NewClientIdentityRepo([]oidc.ClientIdentity{ oidc.ClientIdentity{Credentials: credXXX}, oidc.ClientIdentity{Credentials: credYYY}, }) userRepo, err := makeNewUserRepo() if err != nil { t.Fatalf("Unexpected error: %v", err) } refreshTokenRepo, err := refreshtest.NewTestRefreshTokenRepo() if err != nil { t.Fatalf("Unexpected error: %v", err) } srv := &Server{ IssuerURL: issuerURL, KeyManager: km, ClientIdentityRepo: ciRepo, 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"] != "XXX" { 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, } ciRepo := client.NewClientIdentityRepo([]oidc.ClientIdentity{ oidc.ClientIdentity{Credentials: credXXX}, oidc.ClientIdentity{Credentials: credYYY}, }) 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, err := refreshtest.NewTestRefreshTokenRepo() if err != nil { t.Fatalf("Unexpected error: %v", err) } srv := &Server{ IssuerURL: issuerURL, KeyManager: km, ClientIdentityRepo: ciRepo, UserRepo: userRepo, RefreshTokenRepo: refreshTokenRepo, } if _, err := refreshTokenRepo.Create("testid-2", credXXX.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(credXXX, fmt.Sprintf("0/%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 TestList(t *testing.T) { tests := []struct { cs []oidc.ClientIdentity want []*schema.Client }{ // empty repo { cs: nil, want: nil, }, // single client { cs: []oidc.ClientIdentity{ oidc.ClientIdentity{ Credentials: oidc.ClientCredentials{ID: "foo", Secret: "bar"}, Metadata: oidc.ClientMetadata{ RedirectURLs: []url.URL{ url.URL{Scheme: "http", Host: "example.com"}, }, }, }, }, want: []*schema.Client{ &schema.Client{ Id: "foo", RedirectURIs: []string{"http://example.com"}, }, }, }, // multi client { cs: []oidc.ClientIdentity{ oidc.ClientIdentity{ Credentials: oidc.ClientCredentials{ID: "foo", Secret: "bar"}, Metadata: oidc.ClientMetadata{ RedirectURLs: []url.URL{ url.URL{Scheme: "http", Host: "example.com"}, }, }, }, oidc.ClientIdentity{ Credentials: oidc.ClientCredentials{ID: "biz", Secret: "bang"}, Metadata: oidc.ClientMetadata{ RedirectURLs: []url.URL{ url.URL{Scheme: "https", Host: "example.com", Path: "one/two/three"}, }, }, }, }, want: []*schema.Client{ &schema.Client{ Id: "biz", RedirectURIs: []string{"https://example.com/one/two/three"}, }, &schema.Client{ Id: "foo", RedirectURIs: []string{"http://example.com"}, }, }, }, } for i, tt := range tests { repo := client.NewClientIdentityRepo(tt.cs) res := &clientResource{repo: repo} 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) } if !reflect.DeepEqual(tt.want, resp.Clients) { t.Errorf("case %d: invalid response body, want=%#v, got=%#v", i, tt.want, resp.Clients) } } }