X-Git-Url: https://git.arvados.org/arvados.git/blobdiff_plain/7407f41105f8000bb3908d41a31daaf3a30d9440..a84890bb3791f3df021afc79ccfaf15698696e93:/lib/controller/localdb/login_oidc_test.go diff --git a/lib/controller/localdb/login_oidc_test.go b/lib/controller/localdb/login_oidc_test.go index 2ccb1fce2a..9bc6f90ea9 100644 --- a/lib/controller/localdb/login_oidc_test.go +++ b/lib/controller/localdb/login_oidc_test.go @@ -7,9 +7,6 @@ package localdb import ( "bytes" "context" - "crypto/rand" - "crypto/rsa" - "encoding/base64" "encoding/json" "fmt" "net/http" @@ -27,7 +24,6 @@ import ( "git.arvados.org/arvados.git/sdk/go/auth" "git.arvados.org/arvados.git/sdk/go/ctxlog" check "gopkg.in/check.v1" - jose "gopkg.in/square/go-jose.v2" ) // Gocheck boilerplate @@ -38,22 +34,10 @@ func Test(t *testing.T) { var _ = check.Suite(&OIDCLoginSuite{}) type OIDCLoginSuite struct { - cluster *arvados.Cluster - localdb *Conn - railsSpy *arvadostest.Proxy - fakeIssuer *httptest.Server - fakePeopleAPI *httptest.Server - fakePeopleAPIResponse map[string]interface{} - issuerKey *rsa.PrivateKey - - // expected token request - validCode string - validClientID string - validClientSecret string - // desired response from token endpoint - authEmail string - authEmailVerified bool - authName string + cluster *arvados.Cluster + localdb *Conn + railsSpy *arvadostest.Proxy + fakeProvider *arvadostest.OIDCProvider } func (s *OIDCLoginSuite) TearDownSuite(c *check.C) { @@ -64,103 +48,12 @@ func (s *OIDCLoginSuite) TearDownSuite(c *check.C) { } func (s *OIDCLoginSuite) SetUpTest(c *check.C) { - var err error - s.issuerKey, err = rsa.GenerateKey(rand.Reader, 2048) - c.Assert(err, check.IsNil) - - s.authEmail = "active-user@arvados.local" - s.authEmailVerified = true - s.authName = "Fake User Name" - s.fakeIssuer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - req.ParseForm() - c.Logf("fakeIssuer: got req: %s %s %s", req.Method, req.URL, req.Form) - w.Header().Set("Content-Type", "application/json") - switch req.URL.Path { - case "/.well-known/openid-configuration": - json.NewEncoder(w).Encode(map[string]interface{}{ - "issuer": s.fakeIssuer.URL, - "authorization_endpoint": s.fakeIssuer.URL + "/auth", - "token_endpoint": s.fakeIssuer.URL + "/token", - "jwks_uri": s.fakeIssuer.URL + "/jwks", - "userinfo_endpoint": s.fakeIssuer.URL + "/userinfo", - }) - case "/token": - var clientID, clientSecret string - auth, _ := base64.StdEncoding.DecodeString(strings.TrimPrefix(req.Header.Get("Authorization"), "Basic ")) - authsplit := strings.Split(string(auth), ":") - if len(authsplit) == 2 { - clientID, _ = url.QueryUnescape(authsplit[0]) - clientSecret, _ = url.QueryUnescape(authsplit[1]) - } - if clientID != s.validClientID || clientSecret != s.validClientSecret { - c.Logf("fakeIssuer: expected (%q, %q) got (%q, %q)", s.validClientID, s.validClientSecret, clientID, clientSecret) - w.WriteHeader(http.StatusUnauthorized) - return - } - - if req.Form.Get("code") != s.validCode || s.validCode == "" { - w.WriteHeader(http.StatusUnauthorized) - return - } - idToken, _ := json.Marshal(map[string]interface{}{ - "iss": s.fakeIssuer.URL, - "aud": []string{clientID}, - "sub": "fake-user-id", - "exp": time.Now().UTC().Add(time.Minute).Unix(), - "iat": time.Now().UTC().Unix(), - "nonce": "fake-nonce", - "email": s.authEmail, - "email_verified": s.authEmailVerified, - "name": s.authName, - "alt_verified": true, // for custom claim tests - "alt_email": "alt_email@example.com", // for custom claim tests - "alt_username": "desired-username", // for custom claim tests - }) - json.NewEncoder(w).Encode(struct { - AccessToken string `json:"access_token"` - TokenType string `json:"token_type"` - RefreshToken string `json:"refresh_token"` - ExpiresIn int32 `json:"expires_in"` - IDToken string `json:"id_token"` - }{ - AccessToken: s.fakeToken(c, []byte("fake access token")), - TokenType: "Bearer", - RefreshToken: "test-refresh-token", - ExpiresIn: 30, - IDToken: s.fakeToken(c, idToken), - }) - case "/jwks": - json.NewEncoder(w).Encode(jose.JSONWebKeySet{ - Keys: []jose.JSONWebKey{ - {Key: s.issuerKey.Public(), Algorithm: string(jose.RS256), KeyID: ""}, - }, - }) - case "/auth": - w.WriteHeader(http.StatusInternalServerError) - case "/userinfo": - w.WriteHeader(http.StatusInternalServerError) - default: - w.WriteHeader(http.StatusNotFound) - } - })) - s.validCode = fmt.Sprintf("abcdefgh-%d", time.Now().Unix()) - - s.fakePeopleAPI = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - req.ParseForm() - c.Logf("fakePeopleAPI: got req: %s %s %s", req.Method, req.URL, req.Form) - w.Header().Set("Content-Type", "application/json") - switch req.URL.Path { - case "/v1/people/me": - if f := req.Form.Get("personFields"); f != "emailAddresses,names" { - w.WriteHeader(http.StatusBadRequest) - break - } - json.NewEncoder(w).Encode(s.fakePeopleAPIResponse) - default: - w.WriteHeader(http.StatusNotFound) - } - })) - s.fakePeopleAPIResponse = map[string]interface{}{} + s.fakeProvider = arvadostest.NewOIDCProvider(c) + s.fakeProvider.AuthEmail = "active-user@arvados.local" + s.fakeProvider.AuthEmailVerified = true + s.fakeProvider.AuthName = "Fake User Name" + s.fakeProvider.ValidCode = fmt.Sprintf("abcdefgh-%d", time.Now().Unix()) + s.fakeProvider.PeopleAPIResponse = map[string]interface{}{} cfg, err := config.NewLoader(nil, ctxlog.TestLogger(c)).Load() c.Assert(err, check.IsNil) @@ -171,13 +64,13 @@ func (s *OIDCLoginSuite) SetUpTest(c *check.C) { s.cluster.Login.Google.ClientID = "test%client$id" s.cluster.Login.Google.ClientSecret = "test#client/secret" s.cluster.Users.PreferDomainForUsername = "PreferDomainForUsername.example.com" - s.validClientID = "test%client$id" - s.validClientSecret = "test#client/secret" + s.fakeProvider.ValidClientID = "test%client$id" + s.fakeProvider.ValidClientSecret = "test#client/secret" s.localdb = NewConn(s.cluster) c.Assert(s.localdb.loginController, check.FitsTypeOf, (*oidcLoginController)(nil)) - s.localdb.loginController.(*oidcLoginController).Issuer = s.fakeIssuer.URL - s.localdb.loginController.(*oidcLoginController).peopleAPIBasePath = s.fakePeopleAPI.URL + s.localdb.loginController.(*oidcLoginController).Issuer = s.fakeProvider.Issuer.URL + s.localdb.loginController.(*oidcLoginController).peopleAPIBasePath = s.fakeProvider.PeopleAPI.URL s.railsSpy = arvadostest.NewProxy(c, s.cluster.Services.RailsAPI) *s.localdb.railsProxy = *rpc.NewConn(s.cluster.ClusterID, s.railsSpy.URL, true, rpc.PassthroughTokenProvider) @@ -206,7 +99,7 @@ func (s *OIDCLoginSuite) TestGoogleLogin_Start(c *check.C) { c.Check(err, check.IsNil) target, err := url.Parse(resp.RedirectLocation) c.Check(err, check.IsNil) - issuerURL, _ := url.Parse(s.fakeIssuer.URL) + issuerURL, _ := url.Parse(s.fakeProvider.Issuer.URL) c.Check(target.Host, check.Equals, issuerURL.Host) q := target.Query() c.Check(q.Get("client_id"), check.Equals, "test%client$id") @@ -232,7 +125,7 @@ func (s *OIDCLoginSuite) TestGoogleLogin_InvalidCode(c *check.C) { func (s *OIDCLoginSuite) TestGoogleLogin_InvalidState(c *check.C) { s.startLogin(c) resp, err := s.localdb.Login(context.Background(), arvados.LoginOptions{ - Code: s.validCode, + Code: s.fakeProvider.ValidCode, State: "bogus-state", }) c.Check(err, check.IsNil) @@ -241,20 +134,20 @@ func (s *OIDCLoginSuite) TestGoogleLogin_InvalidState(c *check.C) { } func (s *OIDCLoginSuite) setupPeopleAPIError(c *check.C) { - s.fakePeopleAPI = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + s.fakeProvider.PeopleAPI = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { w.WriteHeader(http.StatusForbidden) fmt.Fprintln(w, `Error 403: accessNotConfigured`) })) - s.localdb.loginController.(*oidcLoginController).peopleAPIBasePath = s.fakePeopleAPI.URL + s.localdb.loginController.(*oidcLoginController).peopleAPIBasePath = s.fakeProvider.PeopleAPI.URL } func (s *OIDCLoginSuite) TestGoogleLogin_PeopleAPIDisabled(c *check.C) { s.localdb.loginController.(*oidcLoginController).UseGooglePeopleAPI = false - s.authEmail = "joe.smith@primary.example.com" + s.fakeProvider.AuthEmail = "joe.smith@primary.example.com" s.setupPeopleAPIError(c) state := s.startLogin(c) _, err := s.localdb.Login(context.Background(), arvados.LoginOptions{ - Code: s.validCode, + Code: s.fakeProvider.ValidCode, State: state, }) c.Check(err, check.IsNil) @@ -294,7 +187,7 @@ func (s *OIDCLoginSuite) TestGoogleLogin_PeopleAPIError(c *check.C) { s.setupPeopleAPIError(c) state := s.startLogin(c) resp, err := s.localdb.Login(context.Background(), arvados.LoginOptions{ - Code: s.validCode, + Code: s.fakeProvider.ValidCode, State: state, }) c.Check(err, check.IsNil) @@ -304,11 +197,11 @@ func (s *OIDCLoginSuite) TestGoogleLogin_PeopleAPIError(c *check.C) { func (s *OIDCLoginSuite) TestGenericOIDCLogin(c *check.C) { s.cluster.Login.Google.Enable = false s.cluster.Login.OpenIDConnect.Enable = true - json.Unmarshal([]byte(fmt.Sprintf("%q", s.fakeIssuer.URL)), &s.cluster.Login.OpenIDConnect.Issuer) + json.Unmarshal([]byte(fmt.Sprintf("%q", s.fakeProvider.Issuer.URL)), &s.cluster.Login.OpenIDConnect.Issuer) s.cluster.Login.OpenIDConnect.ClientID = "oidc#client#id" s.cluster.Login.OpenIDConnect.ClientSecret = "oidc#client#secret" - s.validClientID = "oidc#client#id" - s.validClientSecret = "oidc#client#secret" + s.fakeProvider.ValidClientID = "oidc#client#id" + s.fakeProvider.ValidClientSecret = "oidc#client#secret" for _, trial := range []struct { expectEmail string // "" if failure expected setup func() @@ -317,8 +210,8 @@ func (s *OIDCLoginSuite) TestGenericOIDCLogin(c *check.C) { expectEmail: "user@oidc.example.com", setup: func() { c.Log("=== succeed because email_verified is false but not required") - s.authEmail = "user@oidc.example.com" - s.authEmailVerified = false + s.fakeProvider.AuthEmail = "user@oidc.example.com" + s.fakeProvider.AuthEmailVerified = false s.cluster.Login.OpenIDConnect.EmailClaim = "email" s.cluster.Login.OpenIDConnect.EmailVerifiedClaim = "" s.cluster.Login.OpenIDConnect.UsernameClaim = "" @@ -328,8 +221,8 @@ func (s *OIDCLoginSuite) TestGenericOIDCLogin(c *check.C) { expectEmail: "", setup: func() { c.Log("=== fail because email_verified is false and required") - s.authEmail = "user@oidc.example.com" - s.authEmailVerified = false + s.fakeProvider.AuthEmail = "user@oidc.example.com" + s.fakeProvider.AuthEmailVerified = false s.cluster.Login.OpenIDConnect.EmailClaim = "email" s.cluster.Login.OpenIDConnect.EmailVerifiedClaim = "email_verified" s.cluster.Login.OpenIDConnect.UsernameClaim = "" @@ -339,8 +232,8 @@ func (s *OIDCLoginSuite) TestGenericOIDCLogin(c *check.C) { expectEmail: "user@oidc.example.com", setup: func() { c.Log("=== succeed because email_verified is false but config uses custom 'verified' claim") - s.authEmail = "user@oidc.example.com" - s.authEmailVerified = false + s.fakeProvider.AuthEmail = "user@oidc.example.com" + s.fakeProvider.AuthEmailVerified = false s.cluster.Login.OpenIDConnect.EmailClaim = "email" s.cluster.Login.OpenIDConnect.EmailVerifiedClaim = "alt_verified" s.cluster.Login.OpenIDConnect.UsernameClaim = "" @@ -350,8 +243,8 @@ func (s *OIDCLoginSuite) TestGenericOIDCLogin(c *check.C) { expectEmail: "alt_email@example.com", setup: func() { c.Log("=== succeed with custom 'email' and 'email_verified' claims") - s.authEmail = "bad@wrong.example.com" - s.authEmailVerified = false + s.fakeProvider.AuthEmail = "bad@wrong.example.com" + s.fakeProvider.AuthEmailVerified = false s.cluster.Login.OpenIDConnect.EmailClaim = "alt_email" s.cluster.Login.OpenIDConnect.EmailVerifiedClaim = "alt_verified" s.cluster.Login.OpenIDConnect.UsernameClaim = "alt_username" @@ -368,7 +261,7 @@ func (s *OIDCLoginSuite) TestGenericOIDCLogin(c *check.C) { state := s.startLogin(c) resp, err := s.localdb.Login(context.Background(), arvados.LoginOptions{ - Code: s.validCode, + Code: s.fakeProvider.ValidCode, State: state, }) c.Assert(err, check.IsNil) @@ -399,7 +292,7 @@ func (s *OIDCLoginSuite) TestGenericOIDCLogin(c *check.C) { func (s *OIDCLoginSuite) TestGoogleLogin_Success(c *check.C) { state := s.startLogin(c) resp, err := s.localdb.Login(context.Background(), arvados.LoginOptions{ - Code: s.validCode, + Code: s.fakeProvider.ValidCode, State: state, }) c.Check(err, check.IsNil) @@ -436,8 +329,8 @@ func (s *OIDCLoginSuite) TestGoogleLogin_Success(c *check.C) { } func (s *OIDCLoginSuite) TestGoogleLogin_RealName(c *check.C) { - s.authEmail = "joe.smith@primary.example.com" - s.fakePeopleAPIResponse = map[string]interface{}{ + s.fakeProvider.AuthEmail = "joe.smith@primary.example.com" + s.fakeProvider.PeopleAPIResponse = map[string]interface{}{ "names": []map[string]interface{}{ { "metadata": map[string]interface{}{"primary": false}, @@ -453,7 +346,7 @@ func (s *OIDCLoginSuite) TestGoogleLogin_RealName(c *check.C) { } state := s.startLogin(c) s.localdb.Login(context.Background(), arvados.LoginOptions{ - Code: s.validCode, + Code: s.fakeProvider.ValidCode, State: state, }) @@ -463,11 +356,11 @@ func (s *OIDCLoginSuite) TestGoogleLogin_RealName(c *check.C) { } func (s *OIDCLoginSuite) TestGoogleLogin_OIDCRealName(c *check.C) { - s.authName = "Joe P. Smith" - s.authEmail = "joe.smith@primary.example.com" + s.fakeProvider.AuthName = "Joe P. Smith" + s.fakeProvider.AuthEmail = "joe.smith@primary.example.com" state := s.startLogin(c) s.localdb.Login(context.Background(), arvados.LoginOptions{ - Code: s.validCode, + Code: s.fakeProvider.ValidCode, State: state, }) @@ -478,8 +371,8 @@ func (s *OIDCLoginSuite) TestGoogleLogin_OIDCRealName(c *check.C) { // People API returns some additional email addresses. func (s *OIDCLoginSuite) TestGoogleLogin_AlternateEmailAddresses(c *check.C) { - s.authEmail = "joe.smith@primary.example.com" - s.fakePeopleAPIResponse = map[string]interface{}{ + s.fakeProvider.AuthEmail = "joe.smith@primary.example.com" + s.fakeProvider.PeopleAPIResponse = map[string]interface{}{ "emailAddresses": []map[string]interface{}{ { "metadata": map[string]interface{}{"verified": true}, @@ -496,7 +389,7 @@ func (s *OIDCLoginSuite) TestGoogleLogin_AlternateEmailAddresses(c *check.C) { } state := s.startLogin(c) s.localdb.Login(context.Background(), arvados.LoginOptions{ - Code: s.validCode, + Code: s.fakeProvider.ValidCode, State: state, }) @@ -507,8 +400,8 @@ func (s *OIDCLoginSuite) TestGoogleLogin_AlternateEmailAddresses(c *check.C) { // Primary address is not the one initially returned by oidc. func (s *OIDCLoginSuite) TestGoogleLogin_AlternateEmailAddresses_Primary(c *check.C) { - s.authEmail = "joe.smith@alternate.example.com" - s.fakePeopleAPIResponse = map[string]interface{}{ + s.fakeProvider.AuthEmail = "joe.smith@alternate.example.com" + s.fakeProvider.PeopleAPIResponse = map[string]interface{}{ "emailAddresses": []map[string]interface{}{ { "metadata": map[string]interface{}{"verified": true, "primary": true}, @@ -526,7 +419,7 @@ func (s *OIDCLoginSuite) TestGoogleLogin_AlternateEmailAddresses_Primary(c *chec } state := s.startLogin(c) s.localdb.Login(context.Background(), arvados.LoginOptions{ - Code: s.validCode, + Code: s.fakeProvider.ValidCode, State: state, }) authinfo := getCallbackAuthInfo(c, s.railsSpy) @@ -536,9 +429,9 @@ func (s *OIDCLoginSuite) TestGoogleLogin_AlternateEmailAddresses_Primary(c *chec } func (s *OIDCLoginSuite) TestGoogleLogin_NoPrimaryEmailAddress(c *check.C) { - s.authEmail = "joe.smith@unverified.example.com" - s.authEmailVerified = false - s.fakePeopleAPIResponse = map[string]interface{}{ + s.fakeProvider.AuthEmail = "joe.smith@unverified.example.com" + s.fakeProvider.AuthEmailVerified = false + s.fakeProvider.PeopleAPIResponse = map[string]interface{}{ "emailAddresses": []map[string]interface{}{ { "metadata": map[string]interface{}{"verified": true}, @@ -552,7 +445,7 @@ func (s *OIDCLoginSuite) TestGoogleLogin_NoPrimaryEmailAddress(c *check.C) { } state := s.startLogin(c) s.localdb.Login(context.Background(), arvados.LoginOptions{ - Code: s.validCode, + Code: s.fakeProvider.ValidCode, State: state, }) @@ -574,23 +467,6 @@ func (s *OIDCLoginSuite) startLogin(c *check.C) (state string) { return } -func (s *OIDCLoginSuite) fakeToken(c *check.C, payload []byte) string { - signer, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.RS256, Key: s.issuerKey}, nil) - if err != nil { - c.Error(err) - } - object, err := signer.Sign(payload) - if err != nil { - c.Error(err) - } - t, err := object.CompactSerialize() - if err != nil { - c.Error(err) - } - c.Logf("fakeToken(%q) == %q", payload, t) - return t -} - func getCallbackAuthInfo(c *check.C, railsSpy *arvadostest.Proxy) (authinfo rpc.UserSessionAuthInfo) { for _, dump := range railsSpy.RequestDumps { c.Logf("spied request: %q", dump)