Skip to content

Commit b4bf699

Browse files
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 <[email protected]> * include pkce_verifier + upgrade oauth2 Signed-off-by: Houssem Ben Mabrouk <[email protected]> * append issuer prefix to device redirectURI Signed-off-by: Houssem Ben Mabrouk <[email protected]> * fix lint? Signed-off-by: Houssem Ben Mabrouk <[email protected]> * fix test Signed-off-by: Houssem Ben Mabrouk <[email protected]> --------- Signed-off-by: Houssem Ben Mabrouk <[email protected]> * test to be reverted Signed-off-by: Houssem Ben Mabrouk <[email protected]> * Revert "test to be reverted" This reverts commit 65c6d32. * add client_credentials to default oauth2 grant types Signed-off-by: Houssem Ben Mabrouk <[email protected]> * Modify client credential grant (#6) * feat: dynamic oauth2 credentials client_credential flow Signed-off-by: Houssem Ben Mabrouk <[email protected]> * adding tests for client_credentials flow Signed-off-by: Houssem Ben Mabrouk <[email protected]> * better credentials handling + adjust tests Signed-off-by: Houssem Ben Mabrouk <[email protected]> * fix lint Signed-off-by: Houssem Ben Mabrouk <[email protected]> --------- Signed-off-by: Houssem Ben Mabrouk <[email protected]> --------- Signed-off-by: Houssem Ben Mabrouk <[email protected]>
1 parent 4c5351c commit b4bf699

File tree

9 files changed

+353
-17
lines changed

9 files changed

+353
-17
lines changed

cmd/dex/serve.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -586,6 +586,7 @@ func applyConfigOverrides(options serveOptions, config *Config) {
586586
"refresh_token",
587587
"urn:ietf:params:oauth:grant-type:device_code",
588588
"urn:ietf:params:oauth:grant-type:token-exchange",
589+
"client_credentials",
589590
}
590591
}
591592
}

connector/oidc/oidc.go

Lines changed: 98 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"encoding/json"
77
"errors"
88
"fmt"
9+
"io"
910
"net/http"
1011
"net/url"
1112
"strings"
@@ -40,6 +41,11 @@ type Config struct {
4041

4142
Scopes []string `json:"scopes"` // defaults to "profile" and "email"
4243

44+
PKCE struct {
45+
// Configurable key which controls if pkce challenge should be created or not
46+
Enabled bool `json:"enabled"` // defaults to "false"
47+
} `json:"pkce"`
48+
4349
// HostedDomains was an optional list of whitelisted domains when using the OIDC connector with Google.
4450
// Only users from a whitelisted domain were allowed to log in.
4551
// 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
247253
promptType = *c.PromptType
248254
}
249255

256+
// pkce
257+
pkceVerifier := ""
258+
if c.PKCE.Enabled {
259+
pkceVerifier = oauth2.GenerateVerifier()
260+
}
261+
250262
clientID := c.ClientID
251263
return &oidcConnector{
252264
provider: provider,
@@ -259,8 +271,9 @@ func (c *Config) Open(id string, logger log.Logger) (conn connector.Connector, e
259271
RedirectURL: c.RedirectURI,
260272
},
261273
verifier: provider.Verifier(
262-
&oidc.Config{ClientID: clientID},
274+
&oidc.Config{ClientID: clientID, SkipClientIDCheck: len(clientID) == 0},
263275
),
276+
pkceVerifier: pkceVerifier,
264277
logger: logger,
265278
cancel: cancel,
266279
httpClient: httpClient,
@@ -290,6 +303,7 @@ type oidcConnector struct {
290303
redirectURI string
291304
oauth2Config *oauth2.Config
292305
verifier *oidc.IDTokenVerifier
306+
pkceVerifier string
293307
cancel context.CancelFunc
294308
logger log.Logger
295309
httpClient *http.Client
@@ -328,6 +342,10 @@ func (c *oidcConnector) LoginURL(s connector.Scopes, callbackURL, state string)
328342
if s.OfflineAccess {
329343
opts = append(opts, oauth2.AccessTypeOffline, oauth2.SetAuthURLParam("prompt", c.promptType))
330344
}
345+
346+
if c.pkceVerifier != "" {
347+
opts = append(opts, oauth2.S256ChallengeOption(c.pkceVerifier))
348+
}
331349
return c.oauth2Config.AuthCodeURL(state, opts...), nil
332350
}
333351

@@ -351,17 +369,93 @@ const (
351369
exchangeCaller
352370
)
353371

372+
func (c *oidcConnector) getTokenViaClientCredentials(r *http.Request) (token *oauth2.Token, err error) {
373+
// Setup default clientID & clientSecret
374+
clientID := c.oauth2Config.ClientID
375+
clientSecret := c.oauth2Config.ClientSecret
376+
377+
// Override clientID & clientSecret if they exist!
378+
q := r.Form
379+
if q.Has("custom_client_id") && q.Has("custom_client_secret") {
380+
clientID = q.Get("custom_client_id")
381+
clientSecret = q.Get("custom_client_secret")
382+
}
383+
384+
// Check if oauth2 credentials are not empty
385+
if len(clientID) == 0 || len(clientSecret) == 0 {
386+
return nil, fmt.Errorf("oidc: unable to get clientID or clientSecret")
387+
}
388+
389+
// Construct data to be sent to the external IdP
390+
data := url.Values{
391+
"grant_type": {"client_credentials"},
392+
"client_id": {clientID},
393+
"client_secret": {clientSecret},
394+
"scope": {strings.Join(c.oauth2Config.Scopes, " ")},
395+
}
396+
397+
// Request token from external IdP
398+
resp, err := c.httpClient.PostForm(c.oauth2Config.Endpoint.TokenURL, data)
399+
if err != nil {
400+
return nil, fmt.Errorf("oidc: failed to get token: %v", err)
401+
}
402+
defer resp.Body.Close()
403+
404+
if resp.StatusCode != http.StatusOK {
405+
return nil, fmt.Errorf("oidc: issuer returned an error: %v", resp.Status)
406+
}
407+
408+
body, err := io.ReadAll(resp.Body)
409+
if err != nil {
410+
return nil, fmt.Errorf("oidc: failed to get read token body: %v", err)
411+
}
412+
413+
type AccessTokenType struct {
414+
AccessToken string `json:"access_token"`
415+
TokenType string `json:"token_type"`
416+
ExpiresIn int `json:"expires_in"`
417+
}
418+
response := AccessTokenType{}
419+
if err = json.Unmarshal(body, &response); err != nil {
420+
return nil, fmt.Errorf("oidc: unable to parse response: %v", err)
421+
}
422+
token = &oauth2.Token{
423+
AccessToken: response.AccessToken,
424+
Expiry: time.Now().Add(time.Second * time.Duration(response.ExpiresIn)),
425+
}
426+
raw := make(map[string]interface{})
427+
json.Unmarshal(body, &raw) // no error checks for optional fields
428+
token = token.WithExtra(raw)
429+
430+
return token, nil
431+
}
432+
354433
func (c *oidcConnector) HandleCallback(s connector.Scopes, r *http.Request) (identity connector.Identity, err error) {
355434
q := r.URL.Query()
356435
if errType := q.Get("error"); errType != "" {
357436
return identity, &oauth2Error{errType, q.Get("error_description")}
358437
}
359438

360439
ctx := context.WithValue(r.Context(), oauth2.HTTPClient, c.httpClient)
440+
var token *oauth2.Token
441+
if q.Has("code") {
442+
// exchange code to token
443+
var opts []oauth2.AuthCodeOption
361444

362-
token, err := c.oauth2Config.Exchange(ctx, q.Get("code"))
363-
if err != nil {
364-
return identity, fmt.Errorf("oidc: failed to get token: %v", err)
445+
if c.pkceVerifier != "" {
446+
opts = append(opts, oauth2.VerifierOption(c.pkceVerifier))
447+
}
448+
449+
token, err = c.oauth2Config.Exchange(ctx, q.Get("code"), opts...)
450+
if err != nil {
451+
return identity, fmt.Errorf("oidc: failed to get token: %v", err)
452+
}
453+
} else {
454+
// get token via client_credentials
455+
token, err = c.getTokenViaClientCredentials(r)
456+
if err != nil {
457+
return identity, err
458+
}
365459
}
366460
return c.createIdentity(ctx, identity, token, createCaller)
367461
}

0 commit comments

Comments
 (0)