Skip to content

Commit 44f15e7

Browse files
committed
feat(oauth2): populate groups claim in client_credentials tokens
Add a ClientCredentialsClaims sub-struct to the Client definition, allowing static clients to declare identity claims (starting with groups) that are included in tokens issued via client_credentials. This keeps the Client struct focused on application-level concerns (ID, secret, redirect URIs) while providing a dedicated home for identity attributes needed by RBAC-aware consumers. Configuration example: staticClients: - id: my-service secret: "..." clientCredentialsClaims: groups: - admin-group When the groups scope is requested in a client_credentials grant, the groups from clientCredentialsClaims are included in the token. If clientCredentialsClaims is not set, behavior is unchanged. Fixes #4690 Signed-off-by: Carles Arnal <carlesarnal92@gmail.com>
1 parent 896c695 commit 44f15e7

3 files changed

Lines changed: 65 additions & 17 deletions

File tree

server/handlers.go

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1880,12 +1880,16 @@ func (s *Server) handleClientCredentialsGrant(w http.ResponseWriter, r *http.Req
18801880
UserID: client.ID,
18811881
}
18821882

1883-
// Only populate Username/PreferredUsername when the profile scope is requested.
1883+
// Populate optional claims based on requested scopes.
18841884
for _, scope := range scopes {
1885-
if scope == scopeProfile {
1885+
switch scope {
1886+
case scopeProfile:
18861887
claims.Username = client.Name
18871888
claims.PreferredUsername = client.Name
1888-
break
1889+
case scopeGroups:
1890+
if client.ClientCredentialsClaims != nil {
1891+
claims.Groups = client.ClientCredentialsClaims.Groups
1892+
}
18891893
}
18901894
}
18911895

server/handlers_test.go

Lines changed: 46 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1068,14 +1068,16 @@ func TestHandlePasswordLoginWithSkipApproval(t *testing.T) {
10681068

10691069
func TestHandleClientCredentials(t *testing.T) {
10701070
tests := []struct {
1071-
name string
1072-
clientID string
1073-
clientSecret string
1074-
scopes string
1075-
wantCode int
1076-
wantAccessTok bool
1077-
wantIDToken bool
1078-
wantUsername string
1071+
name string
1072+
clientID string
1073+
clientSecret string
1074+
clientCredentialsClaims *storage.ClientCredentialsClaims
1075+
scopes string
1076+
wantCode int
1077+
wantAccessTok bool
1078+
wantIDToken bool
1079+
wantUsername string
1080+
wantGroups []string
10791081
}{
10801082
{
10811083
name: "Basic grant, no scopes",
@@ -1115,6 +1117,28 @@ func TestHandleClientCredentials(t *testing.T) {
11151117
wantIDToken: true,
11161118
wantUsername: "Test Client",
11171119
},
1120+
{
1121+
name: "With groups scope and clientCredentialsClaims groups populated",
1122+
clientID: "test",
1123+
clientSecret: "barfoo",
1124+
clientCredentialsClaims: &storage.ClientCredentialsClaims{
1125+
Groups: []string{"admin-group", "dev-group"},
1126+
},
1127+
scopes: "openid groups",
1128+
wantCode: 200,
1129+
wantAccessTok: true,
1130+
wantIDToken: true,
1131+
wantGroups: []string{"admin-group", "dev-group"},
1132+
},
1133+
{
1134+
name: "With groups scope but no clientCredentialsClaims configured",
1135+
clientID: "test",
1136+
clientSecret: "barfoo",
1137+
scopes: "openid groups",
1138+
wantCode: 200,
1139+
wantAccessTok: true,
1140+
wantIDToken: true,
1141+
},
11181142
{
11191143
name: "Invalid client secret",
11201144
clientID: "test",
@@ -1155,10 +1179,11 @@ func TestHandleClientCredentials(t *testing.T) {
11551179

11561180
// Create a confidential client for testing.
11571181
err := s.storage.CreateClient(ctx, storage.Client{
1158-
ID: "test",
1159-
Secret: "barfoo",
1160-
RedirectURIs: []string{"https://example.com/callback"},
1161-
Name: "Test Client",
1182+
ID: "test",
1183+
Secret: "barfoo",
1184+
RedirectURIs: []string{"https://example.com/callback"},
1185+
Name: "Test Client",
1186+
ClientCredentialsClaims: tc.clientCredentialsClaims,
11621187
})
11631188
require.NoError(t, err)
11641189

@@ -1214,8 +1239,9 @@ func TestHandleClientCredentials(t *testing.T) {
12141239
require.Equal(t, tc.clientID, sub.UserId)
12151240

12161241
var claims struct {
1217-
Name string `json:"name"`
1218-
PreferredUsername string `json:"preferred_username"`
1242+
Name string `json:"name"`
1243+
PreferredUsername string `json:"preferred_username"`
1244+
Groups []string `json:"groups"`
12191245
}
12201246
require.NoError(t, idToken.Claims(&claims))
12211247

@@ -1226,6 +1252,12 @@ func TestHandleClientCredentials(t *testing.T) {
12261252
require.Empty(t, claims.Name)
12271253
require.Empty(t, claims.PreferredUsername)
12281254
}
1255+
1256+
if tc.wantGroups != nil {
1257+
require.Equal(t, tc.wantGroups, claims.Groups)
1258+
} else {
1259+
require.Empty(t, claims.Groups)
1260+
}
12291261
} else {
12301262
require.Empty(t, resp.IDToken)
12311263
}

storage/storage.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,18 @@ type Client struct {
191191
// MFAChain is an ordered list of MFA authenticator IDs that a user must complete
192192
// during login. Empty means no MFA required.
193193
MFAChain []string `json:"mfaChain"`
194+
195+
// ClientCredentialsClaims holds identity claims used when issuing tokens via the
196+
// client_credentials grant. Kept separate from core Client fields to avoid mixing
197+
// application identity (ID, secret, redirect URIs) with user-like identity attributes.
198+
ClientCredentialsClaims *ClientCredentialsClaims `json:"clientCredentialsClaims,omitempty"`
199+
}
200+
201+
// ClientCredentialsClaims contains claims that are included in tokens issued via
202+
// the client_credentials grant. This is scoped to client_credentials to keep the
203+
// Client struct focused on application-level concerns.
204+
type ClientCredentialsClaims struct {
205+
Groups []string `json:"groups,omitempty"`
194206
}
195207

196208
// Claims represents the ID Token claims supported by the server.

0 commit comments

Comments
 (0)