Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions server/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -864,6 +864,8 @@ func (s *Server) handleToken(w http.ResponseWriter, r *http.Request) {
s.withClientFromStorage(w, r, s.handlePasswordGrant)
case grantTypeTokenExchange:
s.withClientFromStorage(w, r, s.handleTokenExchange)
case grantTypeClientCredentials:
s.withClientFromStorage(w, r, s.handleClientCredentialsGrant)
default:
s.tokenErrHelper(w, errUnsupportedGrantType, "", http.StatusBadRequest)
}
Expand Down Expand Up @@ -1116,6 +1118,35 @@ func (s *Server) handleUserInfo(w http.ResponseWriter, r *http.Request) {
w.Write(claims)
}

func (s *Server) handleClientCredentialsGrant(w http.ResponseWriter, r *http.Request, client storage.Client) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct me if I'm wrong, but client credentials grant isn't supposed to work with public clients, is it?

Shouldn't there be some sort of check here?

Technically, withClientFromStorage checks the secret, but public clients have empty secret, so it will always match.

If I'm right, this is a rather serious vulnerability.

@cardoe any chance you could give this a try in your build?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, it would probably be a good idea to cover at least the happy path with tests.

if err := r.ParseForm(); err != nil {
s.tokenErrHelper(w, errInvalidRequest, "Couldn't parse data", http.StatusBadRequest)
return
}
q := r.Form

nonce := q.Get("nonce")
scopes := strings.Fields(q.Get("scope"))

claims := storage.Claims{UserID: client.ID}

accessToken, _, err := s.newAccessToken(r.Context(), client.ID, claims, scopes, nonce, "client")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is generating an access token always necessary? Shouldn't it be returned based on the requested scopes?

if err != nil {
s.logger.ErrorContext(r.Context(), "failed to create new access token", "err", err)
s.tokenErrHelper(w, errServerError, err.Error(), http.StatusInternalServerError)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We've just merged #4457 removing internal error messages from responses. I think a generic error message should be returned here.

See the linked PR.

return
}

idToken, expiry, err := s.newIDToken(r.Context(), client.ID, claims, scopes, nonce, accessToken, "", "client")
if err != nil {
s.tokenErrHelper(w, errServerError, fmt.Sprintf("failed to create ID token: %v", err), http.StatusInternalServerError)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above.

return
}

resp := s.toAccessTokenResponse(idToken, accessToken, "", expiry)
s.writeAccessToken(w, resp)
}

func (s *Server) handlePasswordGrant(w http.ResponseWriter, r *http.Request, client storage.Client) {
ctx := r.Context()
// Parse the fields
Expand Down
1 change: 1 addition & 0 deletions server/handlers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ func TestHandleDiscovery(t *testing.T) {
Introspect: fmt.Sprintf("%s/token/introspect", httpServer.URL),
GrantTypes: []string{
"authorization_code",
"client_credentials",
"refresh_token",
"urn:ietf:params:oauth:grant-type:device_code",
"urn:ietf:params:oauth:grant-type:token-exchange",
Expand Down
1 change: 1 addition & 0 deletions server/oauth2.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,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 @@ -239,6 +239,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
9 changes: 5 additions & 4 deletions server/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ func newTestServer(t *testing.T, updateConfig func(c *Config)) (*httptest.Server
grantTypeTokenExchange,
grantTypeImplicit,
grantTypePassword,
grantTypeClientCredentials,
},
}
if updateConfig != nil {
Expand Down Expand Up @@ -1757,7 +1758,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",
Expand All @@ -1767,20 +1768,20 @@ 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",
config: func(c *Config) {
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},
},
}

Expand Down