diff --git a/connector/oidc/oidc.go b/connector/oidc/oidc.go index b38915e303..59ee3f90da 100644 --- a/connector/oidc/oidc.go +++ b/connector/oidc/oidc.go @@ -6,6 +6,7 @@ import ( "encoding/json" "errors" "fmt" + "io" "net/http" "net/url" "strings" @@ -35,6 +36,11 @@ type Config struct { Scopes []string `json:"scopes"` // defaults to "profile" and "email" + PKCE struct { + // Configurable key which controls if pkce challenge should be created or not + Enabled bool `json:"enabled"` // defaults to "false" + } `json:"pkce"` + // HostedDomains was an optional list of whitelisted domains when using the OIDC connector with Google. // Only users from a whitelisted domain were allowed to log in. // Support for this option was removed from the OIDC connector. @@ -161,6 +167,12 @@ func (c *Config) Open(id string, logger log.Logger) (conn connector.Connector, e c.PromptType = "consent" } + // pkce + pkceVerifier := "" + if c.PKCE.Enabled { + pkceVerifier = oauth2.GenerateVerifier() + } + clientID := c.ClientID return &oidcConnector{ provider: provider, @@ -175,6 +187,7 @@ func (c *Config) Open(id string, logger log.Logger) (conn connector.Connector, e verifier: provider.Verifier( &oidc.Config{ClientID: clientID}, ), + pkceVerifier: pkceVerifier, logger: logger, cancel: cancel, httpClient: httpClient, @@ -202,6 +215,7 @@ type oidcConnector struct { redirectURI string oauth2Config *oauth2.Config verifier *oidc.IDTokenVerifier + pkceVerifier string cancel context.CancelFunc logger log.Logger httpClient *http.Client @@ -238,6 +252,10 @@ func (c *oidcConnector) LoginURL(s connector.Scopes, callbackURL, state string) if s.OfflineAccess { opts = append(opts, oauth2.AccessTypeOffline, oauth2.SetAuthURLParam("prompt", c.promptType)) } + + if c.pkceVerifier != "" { + opts = append(opts, oauth2.S256ChallengeOption(c.pkceVerifier)) + } return c.oauth2Config.AuthCodeURL(state, opts...), nil } @@ -260,6 +278,50 @@ const ( refreshCaller ) +func (c *oidcConnector) getTokenViaClientCredentials() (token *oauth2.Token, err error) { + data := url.Values{ + "grant_type": {"client_credentials"}, + "client_id": {c.oauth2Config.ClientID}, + "client_secret": {c.oauth2Config.ClientSecret}, + "scope": {strings.Join(c.oauth2Config.Scopes, " ")}, + } + + resp, err := c.httpClient.PostForm(c.oauth2Config.Endpoint.TokenURL, data) + if err != nil { + return nil, fmt.Errorf("oidc: failed to get token: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("oidc: issuer returned an error: %v", resp.Status) + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("oidc: failed to get read token body: %v", err) + } + + type AccessTokenType struct { + AccessToken string `json:"access_token"` + TokenType string `json:"token_type"` + ExpiresIn int `json:"expires_in"` + } + response := AccessTokenType{} + if err = json.Unmarshal(body, &response); err != nil { + return nil, fmt.Errorf("oidc: unable to parse response: %v", err) + } + + token = &oauth2.Token{ + AccessToken: response.AccessToken, + Expiry: time.Now().Add(time.Second * time.Duration(response.ExpiresIn)), + } + raw := make(map[string]interface{}) + json.Unmarshal(body, &raw) // no error checks for optional fields + token = token.WithExtra(raw) + + return token, nil +} + func (c *oidcConnector) HandleCallback(s connector.Scopes, r *http.Request) (identity connector.Identity, err error) { q := r.URL.Query() if errType := q.Get("error"); errType != "" { @@ -267,10 +329,25 @@ func (c *oidcConnector) HandleCallback(s connector.Scopes, r *http.Request) (ide } ctx := context.WithValue(r.Context(), oauth2.HTTPClient, c.httpClient) + var token *oauth2.Token + if q.Has("code") { + // exchange code to token + var opts []oauth2.AuthCodeOption - token, err := c.oauth2Config.Exchange(ctx, q.Get("code")) - if err != nil { - return identity, fmt.Errorf("oidc: failed to get token: %v", err) + if c.pkceVerifier != "" { + opts = append(opts, oauth2.VerifierOption(c.pkceVerifier)) + } + + token, err = c.oauth2Config.Exchange(ctx, q.Get("code"), opts...) + if err != nil { + return identity, fmt.Errorf("oidc: failed to get token: %v", err) + } + } else { + // get token via client_credentials + token, err = c.getTokenViaClientCredentials() + if err != nil { + return identity, err + } } return c.createIdentity(ctx, identity, token, createCaller) } diff --git a/connector/oidc/oidc_test.go b/connector/oidc/oidc_test.go index d94af79de8..b476553582 100644 --- a/connector/oidc/oidc_test.go +++ b/connector/oidc/oidc_test.go @@ -61,6 +61,7 @@ func TestHandleCallback(t *testing.T) { expectPreferredUsername string expectedEmailField string token map[string]interface{} + pkce bool }{ { name: "simpleCase", @@ -287,6 +288,40 @@ func TestHandleCallback(t *testing.T) { "email_verified": true, }, }, + { + name: "withPKCE", + userIDKey: "", // not configured + userNameKey: "", // not configured + expectUserID: "subvalue", + expectUserName: "namevalue", + expectGroups: []string{"group1", "group2"}, + expectedEmailField: "emailvalue", + token: map[string]interface{}{ + "sub": "subvalue", + "name": "namevalue", + "groups": []string{"group1", "group2"}, + "email": "emailvalue", + "email_verified": true, + }, + pkce: true, + }, + { + name: "withoutPKCE", + userIDKey: "", // not configured + userNameKey: "", // not configured + expectUserID: "subvalue", + expectUserName: "namevalue", + expectGroups: []string{"group1", "group2"}, + expectedEmailField: "emailvalue", + token: map[string]interface{}{ + "sub": "subvalue", + "name": "namevalue", + "groups": []string{"group1", "group2"}, + "email": "emailvalue", + "email_verified": true, + }, + pkce: false, + }, } for _, tc := range tests { @@ -322,6 +357,7 @@ func TestHandleCallback(t *testing.T) { config.ClaimMapping.PreferredUsernameKey = tc.preferredUsernameKey config.ClaimMapping.EmailKey = tc.emailKey config.ClaimMapping.GroupsKey = tc.groupsKey + config.PKCE.Enabled = tc.pkce conn, err := newConnector(config) if err != nil { diff --git a/go.mod b/go.mod index df739978d1..cb72c44b0c 100644 --- a/go.mod +++ b/go.mod @@ -31,10 +31,10 @@ require ( github.com/stretchr/testify v1.8.4 go.etcd.io/etcd/client/pkg/v3 v3.5.9 go.etcd.io/etcd/client/v3 v3.5.9 - golang.org/x/crypto v0.10.0 + golang.org/x/crypto v0.14.0 golang.org/x/exp v0.0.0-20221004215720-b9f4876ce741 - golang.org/x/net v0.11.0 - golang.org/x/oauth2 v0.9.0 + golang.org/x/net v0.16.0 + golang.org/x/oauth2 v0.13.0 google.golang.org/api v0.129.0 google.golang.org/grpc v1.56.1 google.golang.org/protobuf v1.31.0 @@ -43,7 +43,7 @@ require ( require ( ariga.io/atlas v0.10.2-0.20230427182402-87a07dfb83bf // indirect - cloud.google.com/go/compute v1.19.3 // indirect + cloud.google.com/go/compute v1.20.1 // indirect cloud.google.com/go/compute/metadata v0.2.3 // indirect github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect github.com/Masterminds/goutils v1.1.1 // indirect @@ -89,8 +89,8 @@ require ( go.uber.org/multierr v1.6.0 // indirect go.uber.org/zap v1.17.0 // indirect golang.org/x/mod v0.10.0 // indirect - golang.org/x/sys v0.9.0 // indirect - golang.org/x/text v0.10.0 // indirect + golang.org/x/sys v0.13.0 // indirect + golang.org/x/text v0.13.0 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/genproto v0.0.0-20230530153820-e85fd2cbaebc // indirect google.golang.org/genproto/googleapis/api v0.0.0-20230530153820-e85fd2cbaebc // indirect diff --git a/go.sum b/go.sum index c65da13191..8b733596b9 100644 --- a/go.sum +++ b/go.sum @@ -2,8 +2,8 @@ ariga.io/atlas v0.10.2-0.20230427182402-87a07dfb83bf h1:Tq2DRB39ZHScIwWACjPKLv5o ariga.io/atlas v0.10.2-0.20230427182402-87a07dfb83bf/go.mod h1:+TR129FJZ5Lvzms6dvCeGWh1yR6hMvmXBhug4hrNIGk= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go/compute v1.19.3 h1:DcTwsFgGev/wV5+q8o2fzgcHOaac+DKGC91ZlvpsQds= -cloud.google.com/go/compute v1.19.3/go.mod h1:qxvISKp/gYnXkSAD1ppcSOveRAmzxicEv/JlizULFrI= +cloud.google.com/go/compute v1.20.1 h1:6aKEtlUiwEpJzM001l0yFkpXmUVXaN8W+fbkb2AZNbg= +cloud.google.com/go/compute v1.20.1/go.mod h1:4tCnrn48xsqlwSAiLf1HXMQk8CONslYbdiEZc9FEIbM= cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= entgo.io/ent v0.12.3 h1:N5lO2EOrHpCH5HYfiMOCHYbo+oh5M8GjT0/cx5x6xkk= @@ -251,8 +251,8 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.0.0-20220314234659-1baeb1ce4c0b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= -golang.org/x/crypto v0.10.0 h1:LKqV2xt9+kDzSTfOhx4FrkEBcMrAgHSYgzywV9zcGmM= -golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I= +golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= +golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20221004215720-b9f4876ce741 h1:fGZugkZk2UgYBxtpKmvub51Yno1LJDeEsRp2xGD+0gY= golang.org/x/exp v0.0.0-20221004215720-b9f4876ce741/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= @@ -284,12 +284,12 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= -golang.org/x/net v0.11.0 h1:Gi2tvZIJyBtO9SDr1q9h5hEQCp/4L2RQ+ar0qjx2oNU= -golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ= +golang.org/x/net v0.16.0 h1:7eBu7KsSvFDtSXUIDbh3aqlK4DPsZ1rByC8PFfBThos= +golang.org/x/net v0.16.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.9.0 h1:BPpt2kU7oMRq3kCHAA1tbSEshXRw1LpG2ztgDwrzuAs= -golang.org/x/oauth2 v0.9.0/go.mod h1:qYgFZaFiu6Wg24azG8bdV52QJXJGbZzIIsRCdVKzbLw= +golang.org/x/oauth2 v0.13.0 h1:jDDenyj+WgFtmV3zYVoi8aE2BwtXFLWOA67ZfNWftiY= +golang.org/x/oauth2 v0.13.0/go.mod h1:/JMhi4ZRXAf4HG9LiNmxvk+45+96RUlVThiH8FzNBn0= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -314,8 +314,8 @@ golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s= -golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= @@ -331,8 +331,8 @@ golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.10.0 h1:UpjohKhiEgNc0CSauXmwYftY1+LlaC75SJwh0SgCX58= -golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= diff --git a/server/deviceflowhandlers.go b/server/deviceflowhandlers.go index 95fed3b3c3..43762e2164 100644 --- a/server/deviceflowhandlers.go +++ b/server/deviceflowhandlers.go @@ -90,7 +90,7 @@ func (s *Server) handleDeviceCode(w http.ResponseWriter, r *http.Request) { // Make device code deviceCode := storage.NewDeviceCode() - // make user code + // Make user code userCode := storage.NewUserCode() // Generate the expire time @@ -432,7 +432,7 @@ func (s *Server) verifyUserCode(w http.ResponseWriter, r *http.Request) { q.Set("client_secret", deviceRequest.ClientSecret) q.Set("state", deviceRequest.UserCode) q.Set("response_type", "code") - q.Set("redirect_uri", "/device/callback") + q.Set("redirect_uri", fmt.Sprintf("%s/device/callback", s.issuerURL.Path)) q.Set("scope", strings.Join(deviceRequest.Scopes, " ")) u.RawQuery = q.Encode() diff --git a/server/handlers.go b/server/handlers.go index 08004c6d0e..67fc285472 100755 --- a/server/handlers.go +++ b/server/handlers.go @@ -837,6 +837,8 @@ func (s *Server) handleToken(w http.ResponseWriter, r *http.Request) { s.withClientFromStorage(w, r, s.handleAuthCode) case grantTypeRefreshToken: s.withClientFromStorage(w, r, s.handleRefreshToken) + case grantTypeClientCredentials: + s.withClientFromStorage(w, r, s.handleClientCredentials) case grantTypePassword: s.withClientFromStorage(w, r, s.handlePasswordGrant) default: diff --git a/server/oauth2.go b/server/oauth2.go index bb0058a74a..1d4b2be808 100644 --- a/server/oauth2.go +++ b/server/oauth2.go @@ -132,6 +132,7 @@ const ( grantTypeImplicit = "implicit" grantTypePassword = "password" grantTypeDeviceCode = "urn:ietf:params:oauth:grant-type:device_code" + grantTypeClientCredentials = "client_credentials" ) const ( diff --git a/server/refreshhandlers.go b/server/refreshhandlers.go index 11eaf2e702..bdcc5bf663 100644 --- a/server/refreshhandlers.go +++ b/server/refreshhandlers.go @@ -385,3 +385,64 @@ func (s *Server) handleRefreshToken(w http.ResponseWriter, r *http.Request, clie resp := s.toAccessTokenResponse(idToken, accessToken, rawNewToken, expiry) s.writeAccessToken(w, resp) } + +func (s *Server) handleClientCredentials(w http.ResponseWriter, r *http.Request, client storage.Client) { + // Parse the fields + if err := r.ParseForm(); err != nil { + s.tokenErrHelper(w, errInvalidRequest, "Couldn't parse data", http.StatusBadRequest) + return + } + q := r.Form + + scopes := strings.Fields(q.Get("scope")) + nonce := "" + connID := q.Get("connector_id") + + // Which connector + conn, err := s.getConnector(connID) + if err != nil { + s.tokenErrHelper(w, errInvalidRequest, "Requested connector does not exist.", http.StatusBadRequest) + return + } + + callbackConnector, ok := conn.Connector.(connector.CallbackConnector) + if !ok { + s.tokenErrHelper(w, errInvalidRequest, "Requested callback connector does not correct type.", http.StatusBadRequest) + return + } + + // Login + identity, err := callbackConnector.HandleCallback(parseScopes(scopes), r) + if err != nil { + s.logger.Errorf("Failed to login user: %v", err) + s.tokenErrHelper(w, errInvalidRequest, "Could not login user", http.StatusBadRequest) + return + } + + // Build the claims to send the id token + claims := storage.Claims{ + UserID: identity.UserID, + Username: identity.Username, + PreferredUsername: identity.PreferredUsername, + Email: identity.Email, + EmailVerified: identity.EmailVerified, + Groups: identity.Groups, + } + + accessToken, err := s.newAccessToken(client.ID, claims, scopes, nonce, connID) + if err != nil { + s.logger.Errorf("client grant failed to create new access token: %v", err) + s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError) + return + } + + idToken, expiry, err := s.newIDToken(client.ID, claims, scopes, nonce, accessToken, "", connID) + if err != nil { + s.logger.Errorf("client grant failed to create new ID token: %v", err) + s.tokenErrHelper(w, errServerError, "", http.StatusInternalServerError) + return + } + + resp := s.toAccessTokenResponse(idToken, accessToken, "", expiry) + s.writeAccessToken(w, resp) +} diff --git a/server/server.go b/server/server.go index f23eb54b7c..2d503bce90 100755 --- a/server/server.go +++ b/server/server.go @@ -213,7 +213,7 @@ func newServer(ctx context.Context, c Config, rotationStrategy rotationStrategy) c.SupportedResponseTypes = []string{responseTypeCode} } - supportedGrant := []string{grantTypeAuthorizationCode, grantTypeRefreshToken, grantTypeDeviceCode} // default + supportedGrant := []string{grantTypeAuthorizationCode, grantTypeRefreshToken, grantTypeDeviceCode, grantTypeClientCredentials} // default supportedRes := make(map[string]bool) for _, respType := range c.SupportedResponseTypes { diff --git a/server/server_test.go b/server/server_test.go index bedc336be3..040edfb930 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -1622,7 +1622,7 @@ func TestOAuth2DeviceFlow(t *testing.T) { // Add the Clients to the test server client := storage.Client{ ID: clientID, - RedirectURIs: []string{deviceCallbackURI}, + RedirectURIs: []string{"/non-root-path" + deviceCallbackURI}, Public: true, } if err := s.storage.CreateClient(client); err != nil { @@ -1756,17 +1756,17 @@ func TestServerSupportedGrants(t *testing.T) { { name: "Simple", config: func(c *Config) {}, - resGrants: []string{grantTypeAuthorizationCode, grantTypeRefreshToken, grantTypeDeviceCode}, + resGrants: []string{grantTypeAuthorizationCode, grantTypeClientCredentials, grantTypeRefreshToken, grantTypeDeviceCode}, }, { name: "With password connector", config: func(c *Config) { c.PasswordConnector = "local" }, - resGrants: []string{grantTypeAuthorizationCode, grantTypePassword, grantTypeRefreshToken, grantTypeDeviceCode}, + resGrants: []string{grantTypeAuthorizationCode, grantTypeClientCredentials, grantTypePassword, grantTypeRefreshToken, grantTypeDeviceCode}, }, { name: "With token response", config: func(c *Config) { c.SupportedResponseTypes = append(c.SupportedResponseTypes, responseTypeToken) }, - resGrants: []string{grantTypeAuthorizationCode, grantTypeImplicit, grantTypeRefreshToken, grantTypeDeviceCode}, + resGrants: []string{grantTypeAuthorizationCode, grantTypeClientCredentials, grantTypeImplicit, grantTypeRefreshToken, grantTypeDeviceCode}, }, { name: "All", @@ -1774,7 +1774,7 @@ func TestServerSupportedGrants(t *testing.T) { c.PasswordConnector = "local" c.SupportedResponseTypes = append(c.SupportedResponseTypes, responseTypeToken) }, - resGrants: []string{grantTypeAuthorizationCode, grantTypeImplicit, grantTypePassword, grantTypeRefreshToken, grantTypeDeviceCode}, + resGrants: []string{grantTypeAuthorizationCode, grantTypeClientCredentials, grantTypeImplicit, grantTypePassword, grantTypeRefreshToken, grantTypeDeviceCode}, }, }