Skip to content
Merged
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
5 changes: 4 additions & 1 deletion .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,17 @@ jobs:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
persist-credentials: false
- uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0
with:
go-version-file: "go.mod"
cache: false
- name: golangci-lint
uses: golangci/golangci-lint-action@1e7e51e771db61008b38414a730f564565cf7c20 # v9.2.0
with:
version: v2.1.0
version: v2.10.1
# Optional: golangci-lint command line arguments.
args: --timeout=10m
only-new-issues: true
Expand Down
18 changes: 18 additions & 0 deletions cmd/auth/clientCredentials.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package auth

import (
"fmt"
"strings"

"github.com/opentdf/otdfctl/cmd/common"
"github.com/opentdf/otdfctl/pkg/auth"
Expand Down Expand Up @@ -31,12 +32,24 @@ func clientCredentialsRun(cmd *cobra.Command, args []string) {
if clientSecret == "" {
clientSecret = cli.AskForSecret("Enter client secret: ")
}
var scopes []string
if cmd.Flags().Changed("scopes") {
flagScopes, err := cmd.Flags().GetStringSlice("scopes")
if err != nil {
c.ExitWithError("Failed to read scopes flag", err)
}
scopes = make([]string, 0, len(flagScopes))
for _, scope := range flagScopes {
scopes = append(scopes, strings.TrimSpace(scope))
}
}

// Set the client credentials
err := cp.SetAuthCredentials(profiles.AuthCredentials{
AuthType: profiles.AuthTypeClientCredentials,
ClientID: clientID,
ClientSecret: clientSecret,
Scopes: scopes,
})
if err != nil {
c.ExitWithError("Failed to set client credentials", err)
Expand All @@ -56,5 +69,10 @@ func newClientCredentialsCmd() *cobra.Command {
man.WithRun(clientCredentialsRun),
man.WithHiddenFlags("with-client-creds", "with-client-creds-file"),
)
doc.Flags().StringSlice(
doc.GetDocFlag("scopes").Name,
[]string{},
doc.GetDocFlag("scopes").Description,
)
return &doc.Command
}
1 change: 1 addition & 0 deletions cmd/common/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ func NewHandler(c *cli.Cli) handlers.Handler {
AuthType: profiles.AuthTypeClientCredentials,
ClientID: cc.ClientID,
ClientSecret: cc.ClientSecret,
Scopes: cc.Scopes,
}); err != nil {
cli.ExitWithError("Failed to set client credentials", err)
}
Expand Down
4 changes: 2 additions & 2 deletions docs/man/_index.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@ command:
- name: with-access-token
description: access token for authentication via bearer token
- name: with-client-creds-file
description: path to a JSON file containing a 'clientId' and 'clientSecret' for auth via client-credentials flow
description: path to a JSON file containing a 'clientId', 'clientSecret', and optional 'scopes' for auth via client-credentials flow
- name: with-client-creds
description: JSON string containing a 'clientId' and 'clientSecret' for auth via client-credentials flow
description: JSON string containing a 'clientId', 'clientSecret', and optional 'scopes' for auth via client-credentials flow
default: ""
- name: json
description: output in JSON format
Expand Down
9 changes: 9 additions & 0 deletions docs/man/auth/client-credentials.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ command:
- client-id
arbitrary_args:
- client-secret
flags:
- name: scopes
description: OIDC scopes to request (space-separated).
---

> [!NOTE]
Expand Down Expand Up @@ -40,3 +43,9 @@ Authenticate with client credentials (secret provided as argument)
```shell
otdfctl auth client-credentials <client-id> <client-secret>
```

Authenticate with client credentials and explicit scopes

```shell
otdfctl auth client-credentials <client-id> <client-secret> --scopes "api:access:read api:access:write"
```
10 changes: 5 additions & 5 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
module github.com/opentdf/otdfctl

go 1.24.0
go 1.25.0

toolchain go1.24.13
toolchain go1.25.7

require (
github.com/adrg/frontmatter v0.2.0
Expand All @@ -18,9 +18,9 @@ require (
github.com/google/uuid v1.6.0
github.com/jrschumacher/go-osprofiles v0.0.0-20251201220924-3d077c5481e5
github.com/opentdf/platform/lib/flattening v0.1.3
github.com/opentdf/platform/lib/ocrypto v0.9.0
github.com/opentdf/platform/protocol/go v0.15.0
github.com/opentdf/platform/sdk v0.12.0
github.com/opentdf/platform/lib/ocrypto v0.10.0
github.com/opentdf/platform/protocol/go v0.16.0
github.com/opentdf/platform/sdk v0.13.0
github.com/spf13/cobra v1.10.2
github.com/stretchr/testify v1.11.1
github.com/zitadel/oidc/v3 v3.45.1
Expand Down
12 changes: 6 additions & 6 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -238,12 +238,12 @@ github.com/opentdf/platform/lib/fixtures v0.4.0 h1:p3Y5MLJEBaWiSmo+QyRNTirvI8LqY
github.com/opentdf/platform/lib/fixtures v0.4.0/go.mod h1:ctyrVn+eTObHAPy3vrdPO0O1mc3vgQ6lc9pBTdhBAfo=
github.com/opentdf/platform/lib/flattening v0.1.3 h1:IuOm/wJVXNrzOV676Ticgr0wyBkL+lVjsoSfh+WSkNo=
github.com/opentdf/platform/lib/flattening v0.1.3/go.mod h1:Gs/T+6FGZKk9OAdz2Jf1R8CTGeNRYrq1lZGDeYT3hrY=
github.com/opentdf/platform/lib/ocrypto v0.9.0 h1:ZEJRFLR549unvP6aMWt2j3HT29wqBBhO9P7uudho6Ho=
github.com/opentdf/platform/lib/ocrypto v0.9.0/go.mod h1:/TtiJldbP/LO1cvX8bwhnd7SVHSUImBt1EfjG9qEo78=
github.com/opentdf/platform/protocol/go v0.15.0 h1:7m1iBCxklQy/inIonmGJnhfjkr4ZFLXVt1dL5aiO+sY=
github.com/opentdf/platform/protocol/go v0.15.0/go.mod h1:m6hTbcBrtp2jRhsAstLvPSAnm8v055fUppveG3iI6tw=
github.com/opentdf/platform/sdk v0.12.0 h1:5LkVf5Ktjt5tsc5YBxloJUYNHJ9pE5IMqjswZwBwrRE=
github.com/opentdf/platform/sdk v0.12.0/go.mod h1:jLXYHV3Am2Fq5RSaCLUDLVocwA9iO7mJkGTUqX/HOr8=
github.com/opentdf/platform/lib/ocrypto v0.10.0 h1:7dn/z/1qH3p+gWCrfOoU7hj9XF/p5N+b2JBJuWF9aK0=
github.com/opentdf/platform/lib/ocrypto v0.10.0/go.mod h1:WASkoHreqgTFImB/gJW42VTdpi9AkgkmaW19y/fU+Ew=
github.com/opentdf/platform/protocol/go v0.16.0 h1:/EumdEZY7ujYyQF+EzCBCtIMiJPvGIZpgFgg8bORiFk=
github.com/opentdf/platform/protocol/go v0.16.0/go.mod h1:4lsBu86yrOWdhqIko8/x5ndamOtM8iDNZYBguF9ZiQQ=
github.com/opentdf/platform/sdk v0.13.0 h1:jhhCLE1Y57Y20g6TUEg/zqCfid8m4KPfbaaBlg6oAsM=
github.com/opentdf/platform/sdk v0.13.0/go.mod h1:x80F65+dGzxDTq8iqVIbCrbDDB9oYCYGjh4duoH6biM=
github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs=
github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
Expand Down
42 changes: 35 additions & 7 deletions pkg/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,9 @@ import (
const authCallbackPath = "/callback"

type ClientCredentials struct {
ClientID string `json:"clientId"`
ClientSecret string `json:"clientSecret"`
ClientID string `json:"clientId"`
ClientSecret string `json:"clientSecret"` //nolint:gosec // not a hard-coded secret; populated at runtime
Scopes []string `json:"scopes,omitempty"`
}

type platformConfiguration struct {
Expand All @@ -48,6 +49,27 @@ type JWTClaims struct {
Expiration int64 `json:"exp"`
}

func NormalizeScopes(scopes []string) []string {
if len(scopes) == 0 {
return nil
}
normalized := make([]string, 0, len(scopes))
for _, scope := range scopes {
normalized = append(normalized, strings.Fields(scope)...)
}
if len(normalized) == 0 {
return nil
}
return normalized
}

func normalizeClientCredScopes(creds *ClientCredentials) {
if creds == nil {
return
}
creds.Scopes = NormalizeScopes(creds.Scopes)
}

// Retrieves credentials by reading specified file
func GetClientCredsFromFile(filepath string) (ClientCredentials, error) {
creds := ClientCredentials{}
Expand All @@ -60,6 +82,7 @@ func GetClientCredsFromFile(filepath string) (ClientCredentials, error) {
if err := json.NewDecoder(f).Decode(&creds); err != nil {
return creds, errors.Join(errors.New("failed to decode creds file"), err)
}
normalizeClientCredScopes(&creds)

return creds, nil
}
Expand All @@ -70,6 +93,7 @@ func GetClientCredsFromJSON(credsJSON []byte) (ClientCredentials, error) {
if err := json.Unmarshal(credsJSON, &creds); err != nil {
return creds, errors.Join(errors.New("failed to decode creds JSON"), err)
}
normalizeClientCredScopes(&creds)

return creds, nil
}
Expand Down Expand Up @@ -144,7 +168,7 @@ func GetSDKAuthOptionFromProfile(profile *profiles.OtdfctlProfileStore) (sdk.Opt

switch c.AuthType {
case profiles.AuthTypeClientCredentials:
return sdk.WithClientCredentials(c.ClientID, c.ClientSecret, nil), nil
return sdk.WithClientCredentials(c.ClientID, c.ClientSecret, NormalizeScopes(c.Scopes)), nil
case profiles.AuthTypeAccessToken:
tokenSource := oauth2.StaticTokenSource(buildToken(&c))
return sdk.WithOAuthAccessTokenSource(tokenSource), nil
Expand All @@ -160,7 +184,7 @@ func ValidateProfileAuthCredentials(ctx context.Context, profile *profiles.Otdfc
case "":
return ErrProfileCredentialsNotFound
case profiles.AuthTypeClientCredentials:
_, err := GetTokenWithClientCreds(ctx, profile.GetEndpoint(), c.ClientID, c.ClientSecret, profile.GetTLSNoVerify())
_, err := GetTokenWithClientCreds(ctx, profile.GetEndpoint(), c.ClientID, c.ClientSecret, profile.GetTLSNoVerify(), c.Scopes)
if err != nil {
return err
}
Expand All @@ -180,7 +204,7 @@ func GetTokenWithProfile(ctx context.Context, profile *profiles.OtdfctlProfileSt

switch c.AuthType {
case profiles.AuthTypeClientCredentials:
return GetTokenWithClientCreds(ctx, profile.GetEndpoint(), c.ClientID, c.ClientSecret, profile.GetTLSNoVerify())
return GetTokenWithClientCreds(ctx, profile.GetEndpoint(), c.ClientID, c.ClientSecret, profile.GetTLSNoVerify(), c.Scopes)
case profiles.AuthTypeAccessToken:
return buildToken(&c), nil
default:
Expand All @@ -189,15 +213,19 @@ func GetTokenWithProfile(ctx context.Context, profile *profiles.OtdfctlProfileSt
}

// Uses the OAuth2 client credentials flow to obtain a token.
func GetTokenWithClientCreds(ctx context.Context, endpoint string, clientID string, clientSecret string, tlsNoVerify bool) (*oauth2.Token, error) {
func GetTokenWithClientCreds(ctx context.Context, endpoint string, clientID string, clientSecret string, tlsNoVerify bool, scopes []string) (*oauth2.Token, error) {
rp, err := newOidcRelyingParty(ctx, endpoint, tlsNoVerify, oidcClientCredentials{
clientID: clientID,
clientSecret: clientSecret,
})
if err != nil {
return nil, err
}
return oidcrp.ClientCredentials(ctx, rp, url.Values{})
params := url.Values{}
if normalized := NormalizeScopes(scopes); len(normalized) > 0 {
params.Set("scope", strings.Join(normalized, " "))
}
return oidcrp.ClientCredentials(ctx, rp, params)
}

const (
Expand Down
1 change: 1 addition & 0 deletions pkg/profiles/profileAuthCreds.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ type AuthCredentials struct {
ClientID string `json:"clientId"`
// Used for client credentials
ClientSecret string `json:"clientSecret,omitempty"`
Scopes []string `json:"scopes,omitempty"`
AccessToken AuthCredentialsAccessToken `json:"accessToken,omitempty"`
}

Expand Down
Loading