From 948d7f424e561ab74f6ffbf1703551f238c9710c Mon Sep 17 00:00:00 2001 From: Houssem Ben Mabrouk <127757500+orange-hbenmabrouk@users.noreply.github.com> Date: Mon, 22 Apr 2024 16:49:13 +0200 Subject: [PATCH] implement custom client_credentials grant & add support of pkce to device_grant (#7) * Feature/integrate client credentials nd pcke verifie (#5) * implement client_credentials grant type Signed-off-by: Houssem Ben Mabrouk * include pkce_verifier + upgrade oauth2 Signed-off-by: Houssem Ben Mabrouk * append issuer prefix to device redirectURI Signed-off-by: Houssem Ben Mabrouk * fix lint? Signed-off-by: Houssem Ben Mabrouk * fix test Signed-off-by: Houssem Ben Mabrouk --------- Signed-off-by: Houssem Ben Mabrouk * test to be reverted Signed-off-by: Houssem Ben Mabrouk * Revert "test to be reverted" This reverts commit 65c6d327a50eb885873c0a6ad35b36a1440e87e9. * add client_credentials to default oauth2 grant types Signed-off-by: Houssem Ben Mabrouk * Modify client credential grant (#6) * feat: dynamic oauth2 credentials client_credential flow Signed-off-by: Houssem Ben Mabrouk * adding tests for client_credentials flow Signed-off-by: Houssem Ben Mabrouk * better credentials handling + adjust tests Signed-off-by: Houssem Ben Mabrouk * fix lint Signed-off-by: Houssem Ben Mabrouk --------- Signed-off-by: Houssem Ben Mabrouk --------- Signed-off-by: Houssem Ben Mabrouk --- cmd/dex/serve.go | 1 + connector/oidc/oidc.go | 102 ++++++++++++++++++- connector/oidc/oidc_test.go | 187 +++++++++++++++++++++++++++++++++-- server/deviceflowhandlers.go | 4 +- server/handlers.go | 2 + server/oauth2.go | 1 + server/refreshhandlers.go | 61 ++++++++++++ server/server.go | 1 + server/server_test.go | 11 ++- 9 files changed, 353 insertions(+), 17 deletions(-) diff --git a/cmd/dex/serve.go b/cmd/dex/serve.go index 9461a6220a..900f26df97 100644 --- a/cmd/dex/serve.go +++ b/cmd/dex/serve.go @@ -586,6 +586,7 @@ func applyConfigOverrides(options serveOptions, config *Config) { "refresh_token", "urn:ietf:params:oauth:grant-type:device_code", "urn:ietf:params:oauth:grant-type:token-exchange", + "client_credentials", } } } diff --git a/connector/oidc/oidc.go b/connector/oidc/oidc.go index c2918006c8..3e20262019 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" @@ -40,6 +41,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. @@ -247,6 +253,12 @@ func (c *Config) Open(id string, logger log.Logger) (conn connector.Connector, e promptType = *c.PromptType } + // pkce + pkceVerifier := "" + if c.PKCE.Enabled { + pkceVerifier = oauth2.GenerateVerifier() + } + clientID := c.ClientID return &oidcConnector{ provider: provider, @@ -259,8 +271,9 @@ func (c *Config) Open(id string, logger log.Logger) (conn connector.Connector, e RedirectURL: c.RedirectURI, }, verifier: provider.Verifier( - &oidc.Config{ClientID: clientID}, + &oidc.Config{ClientID: clientID, SkipClientIDCheck: len(clientID) == 0}, ), + pkceVerifier: pkceVerifier, logger: logger, cancel: cancel, httpClient: httpClient, @@ -290,6 +303,7 @@ type oidcConnector struct { redirectURI string oauth2Config *oauth2.Config verifier *oidc.IDTokenVerifier + pkceVerifier string cancel context.CancelFunc logger log.Logger httpClient *http.Client @@ -328,6 +342,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 } @@ -351,6 +369,67 @@ const ( exchangeCaller ) +func (c *oidcConnector) getTokenViaClientCredentials(r *http.Request) (token *oauth2.Token, err error) { + // Setup default clientID & clientSecret + clientID := c.oauth2Config.ClientID + clientSecret := c.oauth2Config.ClientSecret + + // Override clientID & clientSecret if they exist! + q := r.Form + if q.Has("custom_client_id") && q.Has("custom_client_secret") { + clientID = q.Get("custom_client_id") + clientSecret = q.Get("custom_client_secret") + } + + // Check if oauth2 credentials are not empty + if len(clientID) == 0 || len(clientSecret) == 0 { + return nil, fmt.Errorf("oidc: unable to get clientID or clientSecret") + } + + // Construct data to be sent to the external IdP + data := url.Values{ + "grant_type": {"client_credentials"}, + "client_id": {clientID}, + "client_secret": {clientSecret}, + "scope": {strings.Join(c.oauth2Config.Scopes, " ")}, + } + + // Request token from external IdP + 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 != "" { @@ -358,10 +437,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(r) + 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 e621a55ffb..310ef386ee 100644 --- a/connector/oidc/oidc_test.go +++ b/connector/oidc/oidc_test.go @@ -12,6 +12,7 @@ import ( "fmt" "net/http" "net/http/httptest" + "net/url" "reflect" "strings" "testing" @@ -63,7 +64,14 @@ func TestHandleCallback(t *testing.T) { expectPreferredUsername string expectedEmailField string token map[string]interface{} + pkce bool newGroupFromClaims []NewGroupFromClaims + expectedHandlerError error + clientID string + clientSecret string + customClientID string + customClientSecret string + clientCredentials bool }{ { name: "simpleCase", @@ -80,6 +88,8 @@ func TestHandleCallback(t *testing.T) { "email": "emailvalue", "email_verified": true, }, + clientCredentials: false, + expectedHandlerError: nil, }, { name: "customEmailClaim", @@ -95,6 +105,7 @@ func TestHandleCallback(t *testing.T) { "mail": "emailvalue", "email_verified": true, }, + clientCredentials: false, }, { name: "overrideWithCustomEmailClaim", @@ -112,6 +123,7 @@ func TestHandleCallback(t *testing.T) { "custommail": "customemailvalue", "email_verified": true, }, + clientCredentials: false, }, { name: "email_verified not in claims, configured to be skipped", @@ -124,6 +136,7 @@ func TestHandleCallback(t *testing.T) { "name": "namevalue", "email": "emailvalue", }, + clientCredentials: false, }, { name: "withUserIDKey", @@ -137,6 +150,7 @@ func TestHandleCallback(t *testing.T) { "email": "emailvalue", "email_verified": true, }, + clientCredentials: false, }, { name: "withUserNameKey", @@ -150,6 +164,7 @@ func TestHandleCallback(t *testing.T) { "email": "emailvalue", "email_verified": true, }, + clientCredentials: false, }, { name: "withPreferredUsernameKey", @@ -165,6 +180,7 @@ func TestHandleCallback(t *testing.T) { "email": "emailvalue", "email_verified": true, }, + clientCredentials: false, }, { name: "withoutPreferredUsernameKeyAndBackendReturns", @@ -179,6 +195,7 @@ func TestHandleCallback(t *testing.T) { "email": "emailvalue", "email_verified": true, }, + clientCredentials: false, }, { name: "withoutPreferredUsernameKeyAndBackendNotReturn", @@ -192,6 +209,7 @@ func TestHandleCallback(t *testing.T) { "email": "emailvalue", "email_verified": true, }, + clientCredentials: false, }, { name: "emptyEmailScope", @@ -205,6 +223,7 @@ func TestHandleCallback(t *testing.T) { "name": "namevalue", "user_name": "username", }, + clientCredentials: false, }, { name: "emptyEmailScopeButEmailProvided", @@ -219,6 +238,7 @@ func TestHandleCallback(t *testing.T) { "user_name": "username", "email": "emailvalue", }, + clientCredentials: false, }, { name: "customGroupsKey", @@ -236,6 +256,7 @@ func TestHandleCallback(t *testing.T) { "email": "emailvalue", "cognito:groups": []string{"group3", "group4"}, }, + clientCredentials: false, }, { name: "customGroupsKeyButGroupsProvided", @@ -254,6 +275,7 @@ func TestHandleCallback(t *testing.T) { "groups": []string{"group1", "group2"}, "cognito:groups": []string{"group3", "group4"}, }, + clientCredentials: false, }, { name: "customGroupsKeyDespiteGroupsProvidedButOverride", @@ -273,6 +295,7 @@ func TestHandleCallback(t *testing.T) { "groups": []string{"group1", "group2"}, "cognito:groups": []string{"group3", "group4"}, }, + clientCredentials: false, }, { name: "singularGroupResponseAsString", @@ -289,6 +312,7 @@ func TestHandleCallback(t *testing.T) { "email": "emailvalue", "email_verified": true, }, + clientCredentials: false, }, { name: "newGroupFromClaims", @@ -346,7 +370,6 @@ func TestHandleCallback(t *testing.T) { Prefix: "bk", }, }, - token: map[string]interface{}{ "sub": "subvalue", "name": "namevalue", @@ -362,6 +385,121 @@ func TestHandleCallback(t *testing.T) { }, "non-string-claim2": 666, }, + clientCredentials: false, + }, + { + 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, + clientCredentials: false, + }, + { + 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, + clientCredentials: false, + }, + { + name: "withCustomCredentials", + userIDKey: "", // not configured + userNameKey: "", // not configured + clientID: "", // not configured + clientSecret: "", // not configured + expectUserID: "subvalue", + expectUserName: "namevalue", + expectGroups: nil, + expectedEmailField: "emailvalue", + customClientID: "clientidvalue", + customClientSecret: "clientsecretvalue", + scopes: []string{"openid"}, + token: map[string]interface{}{ + "sub": "subvalue", + "name": "namevalue", + "email": "emailvalue", + "email_verified": true, + }, + expectedHandlerError: nil, + clientCredentials: true, + }, + { + name: "withConfiguredAndCustomCredentials", + userIDKey: "", // not configured + userNameKey: "", // not configured + clientID: "defaultClientID", + clientSecret: "defaultClientSecret", + expectUserID: "subvalue", + expectUserName: "namevalue", + expectGroups: nil, + expectedEmailField: "emailvalue", + customClientID: "clientidvalue", + customClientSecret: "clientsecretvalue", + scopes: []string{"openid"}, + token: map[string]interface{}{ + "sub": "subvalue", + "name": "namevalue", + "email": "emailvalue", + "email_verified": true, + }, + expectedHandlerError: fmt.Errorf("expected audience \"defaultClientID\""), + clientCredentials: true, + }, + { + name: "withoutBothCredentials", + userIDKey: "", // not configured + userNameKey: "", // not configured + clientID: "", // not configured + clientSecret: "", // not configured + expectUserID: "", + expectUserName: "", + expectGroups: nil, + expectedEmailField: "", + customClientID: "", // not configured in the request + customClientSecret: "", // not configured in the request + scopes: []string{"openid"}, + token: nil, + expectedHandlerError: fmt.Errorf("oidc: unable to get clientID or clientSecret"), + clientCredentials: true, + }, + { + name: "missingConfiguredAndASingleCustomCredential", + userIDKey: "", // not configured + userNameKey: "", // not configured + clientID: "", // not configured + clientSecret: "", // not configured + expectUserID: "", + expectUserName: "", + expectGroups: nil, + expectedEmailField: "", + customClientID: "clientidvalue", + customClientSecret: "", // not configured in the request + scopes: []string{"openid"}, + token: nil, + expectedHandlerError: fmt.Errorf("oidc: unable to get clientID or clientSecret"), + clientCredentials: true, }, } @@ -384,8 +522,8 @@ func TestHandleCallback(t *testing.T) { basicAuth := true config := Config{ Issuer: serverURL, - ClientID: "clientID", - ClientSecret: "clientSecret", + ClientID: tc.clientID, + ClientSecret: tc.clientSecret, Scopes: scopes, RedirectURI: fmt.Sprintf("%s/callback", serverURL), UserIDKey: tc.userIDKey, @@ -399,22 +537,32 @@ func TestHandleCallback(t *testing.T) { config.ClaimMapping.EmailKey = tc.emailKey config.ClaimMapping.GroupsKey = tc.groupsKey config.ClaimMutations.NewGroupFromClaims = tc.newGroupFromClaims + config.PKCE.Enabled = tc.pkce conn, err := newConnector(config) if err != nil { t.Fatal("failed to create new connector", err) } + var req *http.Request + if tc.clientCredentials { + req, err = newRequestWithoutAuthCode(testServer.URL) + data := url.Values{} + data.Set("custom_client_id", tc.customClientID) + data.Set("custom_client_secret", tc.customClientSecret) + req.Form = data + } else { + req, err = newRequestWithAuthCode(testServer.URL, "someCode") + } - req, err := newRequestWithAuthCode(testServer.URL, "someCode") if err != nil { t.Fatal("failed to create request", err) } identity, err := conn.HandleCallback(connector.Scopes{Groups: true}, req) + compareErrors(t, err, tc.expectedHandlerError) if err != nil { - t.Fatal("handle callback failed", err) + return } - expectEquals(t, identity.UserID, tc.expectUserID) expectEquals(t, identity.Username, tc.expectUserName) expectEquals(t, identity.PreferredUsername, tc.expectPreferredUsername) @@ -792,6 +940,15 @@ func newRequestWithAuthCode(serverURL string, code string) (*http.Request, error return req, nil } +func newRequestWithoutAuthCode(serverURL string) (*http.Request, error) { + req, err := http.NewRequest("GET", serverURL, nil) + if err != nil { + return nil, fmt.Errorf("failed to create request: %v", err) + } + + return req, nil +} + func n(pub *rsa.PublicKey) string { return encode(pub.N.Bytes()) } @@ -812,3 +969,21 @@ func expectEquals(t *testing.T, a interface{}, b interface{}) { t.Errorf("Expected %+v to equal %+v", a, b) } } + +func compareErrors(t *testing.T, a error, b error) { + if a == nil && b == nil { + return + } + if a == nil && b != nil { + t.Errorf("Expected \"%+v\" to be nil", b) + return + } + if a != nil && b == nil { + t.Errorf("Expected \"%+v\" to be \"%+v\"", b, a) + return + } + + if !strings.Contains(a.Error(), b.Error()) { + t.Errorf("Expected \"%+v\" to be a part of \"%+v\"", b, a) + } +} diff --git a/server/deviceflowhandlers.go b/server/deviceflowhandlers.go index 5683e9441a..5f129716dd 100644 --- a/server/deviceflowhandlers.go +++ b/server/deviceflowhandlers.go @@ -91,7 +91,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 @@ -434,7 +434,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 5faab2c9ae..a69db46055 100644 --- a/server/handlers.go +++ b/server/handlers.go @@ -853,6 +853,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) case grantTypeTokenExchange: diff --git a/server/oauth2.go b/server/oauth2.go index 2f2fb74f40..4d98a13312 100644 --- a/server/oauth2.go +++ b/server/oauth2.go @@ -131,6 +131,7 @@ const ( grantTypeImplicit = "implicit" grantTypePassword = "password" grantTypeDeviceCode = "urn:ietf:params:oauth:grant-type:device_code" + grantTypeClientCredentials = "client_credentials" grantTypeTokenExchange = "urn:ietf:params:oauth:grant-type:token-exchange" ) diff --git a/server/refreshhandlers.go b/server/refreshhandlers.go index b3918ab475..c84609d7e3 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 + } + + // Callback + 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 1eaf191543..5fb24b3a63 100644 --- a/server/server.go +++ b/server/server.go @@ -229,6 +229,7 @@ func newServer(ctx context.Context, c Config, rotationStrategy rotationStrategy) grantTypeRefreshToken: true, grantTypeDeviceCode: true, grantTypeTokenExchange: true, + grantTypeClientCredentials: true, } supportedRes := make(map[string]bool) diff --git a/server/server_test.go b/server/server_test.go index 25d1909dc2..cf884bad67 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -106,6 +106,7 @@ func newTestServer(ctx context.Context, t *testing.T, updateConfig func(c *Confi grantTypeTokenExchange, grantTypeImplicit, grantTypePassword, + grantTypeClientCredentials, }, } if updateConfig != nil { @@ -1631,7 +1632,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(ctx, client); err != nil { @@ -1765,7 +1766,7 @@ func TestServerSupportedGrants(t *testing.T) { { name: "Simple", config: func(c *Config) {}, - resGrants: []string{grantTypeAuthorizationCode, grantTypeRefreshToken, grantTypeDeviceCode, grantTypeTokenExchange}, + resGrants: []string{grantTypeAuthorizationCode, grantTypeClientCredentials, grantTypeRefreshToken, grantTypeDeviceCode, grantTypeTokenExchange}, }, { name: "Minimal", @@ -1775,12 +1776,12 @@ func TestServerSupportedGrants(t *testing.T) { { name: "With password connector", config: func(c *Config) { c.PasswordConnector = "local" }, - resGrants: []string{grantTypeAuthorizationCode, grantTypePassword, grantTypeRefreshToken, grantTypeDeviceCode, grantTypeTokenExchange}, + resGrants: []string{grantTypeAuthorizationCode, grantTypeClientCredentials, grantTypePassword, grantTypeRefreshToken, grantTypeDeviceCode, grantTypeTokenExchange}, }, { name: "With token response", config: func(c *Config) { c.SupportedResponseTypes = append(c.SupportedResponseTypes, responseTypeToken) }, - resGrants: []string{grantTypeAuthorizationCode, grantTypeImplicit, grantTypeRefreshToken, grantTypeDeviceCode, grantTypeTokenExchange}, + resGrants: []string{grantTypeAuthorizationCode, grantTypeClientCredentials, grantTypeImplicit, grantTypeRefreshToken, grantTypeDeviceCode, grantTypeTokenExchange}, }, { name: "All", @@ -1788,7 +1789,7 @@ func TestServerSupportedGrants(t *testing.T) { c.PasswordConnector = "local" c.SupportedResponseTypes = append(c.SupportedResponseTypes, responseTypeToken) }, - resGrants: []string{grantTypeAuthorizationCode, grantTypeImplicit, grantTypePassword, grantTypeRefreshToken, grantTypeDeviceCode, grantTypeTokenExchange}, + resGrants: []string{grantTypeAuthorizationCode, grantTypeClientCredentials, grantTypeImplicit, grantTypePassword, grantTypeRefreshToken, grantTypeDeviceCode, grantTypeTokenExchange}, }, }