Skip to content

Commit

Permalink
add client_credentials grant type
Browse files Browse the repository at this point in the history
Signed-off-by: Houssem Ben Mabrouk <[email protected]>
  • Loading branch information
orange-hbenmabrouk committed Oct 6, 2023
1 parent a990fe3 commit e738058
Show file tree
Hide file tree
Showing 5 changed files with 130 additions and 5 deletions.
1 change: 1 addition & 0 deletions cmd/dex/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -560,6 +560,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",
}
}
}
Expand Down
67 changes: 62 additions & 5 deletions connector/oidc/oidc.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"strings"
Expand Down Expand Up @@ -261,19 +262,75 @@ const (
exchangeCaller
)

func (c *oidcConnector) getTokenViaClientCredentials(scopes string) (token *oauth2.Token, err error) {
if scopes == "" {
scopes = strings.Join(c.oauth2Config.Scopes, " ")
}
data := url.Values{
"grant_type": {"client_credentials"},
"client_id": {c.oauth2Config.ClientID},
"client_secret": {c.oauth2Config.ClientSecret},
"scope": {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,
TokenType: "urn:ietf:params:oauth:token-type:id_token",
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 != "" {
return identity, &oauth2Error{errType, q.Get("error_description")}
}

ctx := context.WithValue(r.Context(), oauth2.HTTPClient, c.httpClient)

token, err := c.oauth2Config.Exchange(ctx, q.Get("code"))
if err != nil {
return identity, fmt.Errorf("oidc: failed to get token: %v", err)
if q.Has("code") {
// exchange code to token
token, err := c.oauth2Config.Exchange(ctx, q.Get("code"))
if err != nil {
return identity, fmt.Errorf("oidc: failed to get token: %v", err)
}
return c.createIdentity(ctx, identity, token, createCaller)
} else {
// get token via client_credentials
token, err := c.getTokenViaClientCredentials(r.Form.Get("scope"))
if err != nil {
return identity, err
}
return c.createIdentity(ctx, identity, token, exchangeCaller)
}
return c.createIdentity(ctx, identity, token, createCaller)
}

// Refresh is used to refresh a session with the refresh token provided by the IdP
Expand Down
65 changes: 65 additions & 0 deletions server/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -840,6 +840,8 @@ func (s *Server) handleToken(w http.ResponseWriter, r *http.Request) {
s.handleDeviceToken(w, r)
case grantTypeAuthorizationCode:
s.withClientFromStorage(w, r, s.handleAuthCode)
case grantTypeClientCredentials:
s.withClientFromStorage(w, r, s.handleClientCredentials)
case grantTypeRefreshToken:
s.withClientFromStorage(w, r, s.handleRefreshToken)
case grantTypePassword:
Expand Down Expand Up @@ -1096,6 +1098,69 @@ func (s *Server) handleUserInfo(w http.ResponseWriter, r *http.Request) {
w.Write(claims)
}

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,
}

// we can't pass the scope "groups" to the request and so the response doesn't include the scope "groups"
scopes = append(scopes, "groups") // FIXME: tell multipass to fix this?
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)
}

func (s *Server) handlePasswordGrant(w http.ResponseWriter, r *http.Request, client storage.Client) {
// Parse the fields
if err := r.ParseForm(); err != nil {
Expand Down
1 change: 1 addition & 0 deletions server/oauth2.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ const (
grantTypePassword = "password"
grantTypeDeviceCode = "urn:ietf:params:oauth:grant-type:device_code"
grantTypeTokenExchange = "urn:ietf:params:oauth:grant-type:token-exchange"
grantTypeClientCredentials = "client_credentials"
)

const (
Expand Down
1 change: 1 addition & 0 deletions server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,7 @@ func newServer(ctx context.Context, c Config, rotationStrategy rotationStrategy)
grantTypeRefreshToken: true,
grantTypeDeviceCode: true,
grantTypeTokenExchange: true,
grantTypeClientCredentials: true,
}
supportedRes := make(map[string]bool)

Expand Down

0 comments on commit e738058

Please sign in to comment.