From 8def597826bb610b3ef2b5d763a91958a3a89f0f Mon Sep 17 00:00:00 2001 From: Rodrigo Date: Thu, 28 May 2026 14:04:20 -0300 Subject: [PATCH] feat(load): support connection to devnet using authcode --- ccip/devenv/cldf.go | 53 +++-- ccip/devenv/modifier.go | 30 ++- ccip/devenv/participant_auth.go | 143 +++++++++++++ ccip/devenv/participant_auth_test.go | 261 ++++++++++++++++++++++++ ccip/devenv/participant_connect_test.go | 95 +++++++++ commonconfig/auth.go | 4 +- commonconfig/auth_test.go | 12 +- 7 files changed, 564 insertions(+), 34 deletions(-) create mode 100644 ccip/devenv/participant_auth.go create mode 100644 ccip/devenv/participant_auth_test.go create mode 100644 ccip/devenv/participant_connect_test.go diff --git a/ccip/devenv/cldf.go b/ccip/devenv/cldf.go index 01a6e1bb0..051acfbfb 100644 --- a/ccip/devenv/cldf.go +++ b/ccip/devenv/cldf.go @@ -25,25 +25,19 @@ func NewCLDF(ctx context.Context, b *blockchain.Input) (cldf_chain.BlockChain, u } for i, config := range b.Out.NetworkSpecificData.CantonData.ExternalEndpoints.Participants { - authProvider := authentication.NewInsecureStaticProvider(config.JWT) - // Get Primary Party for user - ledgerApiConn, err := grpc.NewClient( - config.GRPCLedgerAPIURL, - grpc.WithTransportCredentials(authProvider.TransportCredentials()), - grpc.WithPerRPCCredentials(authProvider.PerRPCCredentials()), - ) + authCfg, err := buildParticipantAuthConfig(config) if err != nil { - return nil, 0, fmt.Errorf("failed to create gRPC connection to Ledger API for Canton participant %d: %w", i+1, err) + return nil, 0, fmt.Errorf("participant %d auth config: %w", i+1, err) } - userResp, err := adminv2.NewUserManagementServiceClient(ledgerApiConn).GetUser(context.Background(), &adminv2.GetUserRequest{UserId: config.UserID}) + authProvider, err := participantAuthProvider(ctx, authCfg) if err != nil { - return nil, 0, fmt.Errorf("failed to get user info for user %s for Canton participant %d: %w", config.UserID, i+1, err) + return nil, 0, fmt.Errorf("participant %d auth provider: %w", i+1, err) } - party := userResp.GetUser().GetPrimaryParty() - if party == "" { - return nil, 0, fmt.Errorf("no primary party found for user %s for Canton participant %d", config.UserID, i+1) + + party, err := primaryPartyFromLedgerAPI(ctx, i+1, config.GRPCLedgerAPIURL, config.UserID, authProvider) + if err != nil { + return nil, 0, err } - _ = ledgerApiConn.Close() providerConfig.Participants[i] = cldf_canton_provider.ParticipantConfig{ Endpoints: cldf_canton_provider.Endpoints{ @@ -63,10 +57,39 @@ func NewCLDF(ctx context.Context, b *blockchain.Input) (cldf_chain.BlockChain, u AuthProvider: authProvider, } } - p, err := cldf_canton_provider.NewRPCChainProvider(d.ChainSelector, providerConfig).Initialize(context.TODO()) + p, err := cldf_canton_provider.NewRPCChainProvider(d.ChainSelector, providerConfig).Initialize(ctx) if err != nil { return nil, 0, err } return p, d.ChainSelector, nil } + +func primaryPartyFromLedgerAPI( + ctx context.Context, + participantIndex int, + grpcLedgerAPIURL string, + userID string, + authProvider authentication.Provider, +) (string, error) { + conn, err := grpc.NewClient( + grpcLedgerAPIURL, + grpc.WithTransportCredentials(authProvider.TransportCredentials()), + grpc.WithPerRPCCredentials(authProvider.PerRPCCredentials()), + ) + if err != nil { + return "", fmt.Errorf("failed to create gRPC connection to Ledger API for Canton participant %d: %w", participantIndex, err) + } + defer conn.Close() + + userResp, err := adminv2.NewUserManagementServiceClient(conn).GetUser(ctx, &adminv2.GetUserRequest{UserId: userID}) + if err != nil { + return "", fmt.Errorf("failed to get user info for user %s for Canton participant %d: %w", userID, participantIndex, err) + } + party := userResp.GetUser().GetPrimaryParty() + if party == "" { + return "", fmt.Errorf("no primary party found for user %s for Canton participant %d", userID, participantIndex) + } + + return party, nil +} diff --git a/ccip/devenv/modifier.go b/ccip/devenv/modifier.go index 817818442..9db3bef49 100644 --- a/ccip/devenv/modifier.go +++ b/ccip/devenv/modifier.go @@ -11,13 +11,11 @@ import ( ledgerv2admin "github.com/digital-asset/dazl-client/v8/go/api/com/daml/ledger/api/v2/admin" "github.com/testcontainers/testcontainers-go" "google.golang.org/grpc" - "google.golang.org/grpc/credentials/insecure" chainsel "github.com/smartcontractkit/chain-selectors" "github.com/smartcontractkit/chainlink-ccv/build/devenv/services/committeeverifier" "github.com/smartcontractkit/chainlink-ccv/build/devenv/util" "github.com/smartcontractkit/chainlink-testing-framework/framework/components/blockchain" - "github.com/smartcontractkit/go-daml/pkg/auth" "github.com/smartcontractkit/chainlink-canton/ccip" "github.com/smartcontractkit/chainlink-canton/ccip/sourcereader" @@ -113,22 +111,32 @@ func hydrateAndMarshalCantonConfig(in *committeeverifier.Input, outputs []*block // Get the full party ID (name + hex id) from the canton participant. // TODO: how to support multiple participants? - grpcURL := output.NetworkSpecificData.CantonData.ExternalEndpoints.Participants[0].GRPCLedgerAPIURL - jwt := output.NetworkSpecificData.CantonData.ExternalEndpoints.Participants[0].JWT - if grpcURL == "" || jwt == "" { - return nil, fmt.Errorf("GRPC ledger API URL or JWT is not set for chain %s, please update the config appropriately if you're using canton", strSelector) + participant := output.NetworkSpecificData.CantonData.ExternalEndpoints.Participants[0] + grpcURL := participant.GRPCLedgerAPIURL + if grpcURL == "" { + return nil, fmt.Errorf("GRPC ledger API URL is not set for chain %s, please update the config appropriately if you're using canton", strSelector) + } + + authCfg, err := buildParticipantAuthConfig(participant) + if err != nil { + return nil, fmt.Errorf("participant auth config for chain %s: %w", strSelector, err) + } + authProvider, err := participantAuthProvider(context.Background(), authCfg) + if err != nil { + return nil, fmt.Errorf("participant auth provider for chain %s: %w", strSelector, err) } cantonConfigs.BlockchainInfos[strSelector] = ccip.BlockchainInfo{ GRPCLedgerAPIURL: output.NetworkSpecificData.CantonData.InternalEndpoints.Participants[0].GRPCLedgerAPIURL, - Auth: commonconfig.AuthConfig{ - Type: commonconfig.AuthTypeInsecureStatic, - JWT: jwt, - }, + Auth: authCfg, } // find the party that starts with the prefix that is listed in the canton config. - conn, err := grpc.NewClient(grpcURL, grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithPerRPCCredentials(auth.NewBearerToken(jwt))) + conn, err := grpc.NewClient( + grpcURL, + grpc.WithTransportCredentials(authProvider.TransportCredentials()), + grpc.WithPerRPCCredentials(authProvider.PerRPCCredentials()), + ) if err != nil { return nil, fmt.Errorf("failed to create gRPC connection: %w", err) } diff --git a/ccip/devenv/participant_auth.go b/ccip/devenv/participant_auth.go new file mode 100644 index 000000000..a8ee1ddd6 --- /dev/null +++ b/ccip/devenv/participant_auth.go @@ -0,0 +1,143 @@ +package devenv + +import ( + "context" + "fmt" + "os" + "strings" + + "github.com/smartcontractkit/chainlink-deployments-framework/chain/canton/provider/authentication" + "github.com/smartcontractkit/chainlink-testing-framework/framework/components/blockchain" + + "github.com/smartcontractkit/chainlink-canton/commonconfig" +) + +const ( + envCantonAuthType = "CANTON_AUTH_TYPE" + envCantonAuthURL = "CANTON_AUTH_URL" + envCantonOAuthClientID = "CANTON_OAUTH_CLIENT_ID" + envCantonOAuthClientSecret = "CANTON_OAUTH_CLIENT_SECRET" + envOnchainCantonJWT = "ONCHAIN_CANTON_JWT_TOKEN" + envClientID = "CLIENT_ID" + envClientSecret = "CLIENT_SECRET" +) + +// buildParticipantAuthConfig returns an AuthConfig with an explicit type. +// Devenv participants with a JWT and a local (non-TLS) gRPC endpoint use insecureStatic. +// Real-chain connections default to clientCredentials from env (same as canton-login --ci). +// Static auth is used only when explicitly requested via CANTON_AUTH_TYPE=static. +func buildParticipantAuthConfig(participant blockchain.CantonParticipantEndpoints) (commonconfig.AuthConfig, error) { + if isDevenvParticipant(participant) { + return commonconfig.AuthConfig{ + Type: commonconfig.AuthTypeInsecureStatic, + JWT: participant.JWT, + }, nil + } + + authType := strings.TrimSpace(os.Getenv(envCantonAuthType)) + if authType == "" { + authType = commonconfig.AuthTypeClientCredentials + } + + switch authType { + case commonconfig.AuthTypeInsecureStatic: + jwt := participant.JWT + if jwt == "" { + return commonconfig.AuthConfig{}, fmt.Errorf("insecureStatic auth requires a JWT on the participant config") + } + + return commonconfig.AuthConfig{ + Type: commonconfig.AuthTypeInsecureStatic, + JWT: jwt, + }, nil + + case commonconfig.AuthTypeStatic: + jwt := participant.JWT + if override := strings.TrimSpace(os.Getenv(envOnchainCantonJWT)); override != "" { + jwt = override + } + if jwt == "" { + return commonconfig.AuthConfig{}, fmt.Errorf("static auth requires jwt on participant config or %s", envOnchainCantonJWT) + } + + return commonconfig.AuthConfig{ + Type: commonconfig.AuthTypeStatic, + JWT: jwt, + }, nil + + case commonconfig.AuthTypeAuthorizationCode: + return commonconfig.AuthConfig{ + Type: commonconfig.AuthTypeAuthorizationCode, + AuthURL: strings.TrimSpace(os.Getenv(envCantonAuthURL)), + ClientID: oauthClientID(), + }, nil + + case commonconfig.AuthTypeClientCredentials: + return commonconfig.AuthConfig{ + Type: commonconfig.AuthTypeClientCredentials, + AuthURL: strings.TrimSpace(os.Getenv(envCantonAuthURL)), + ClientID: oauthClientID(), + ClientSecret: oauthClientSecret(), + }, nil + + default: + return commonconfig.AuthConfig{}, fmt.Errorf("unsupported %s: %q", envCantonAuthType, authType) + } +} + +func oauthClientID() string { + return firstNonEmpty( + strings.TrimSpace(os.Getenv(envCantonOAuthClientID)), + strings.TrimSpace(os.Getenv(envClientID)), + ) +} + +func oauthClientSecret() string { + return firstNonEmpty( + strings.TrimSpace(os.Getenv(envCantonOAuthClientSecret)), + strings.TrimSpace(os.Getenv(envClientSecret)), + ) +} + +func isDevenvParticipant(participant blockchain.CantonParticipantEndpoints) bool { + if strings.TrimSpace(participant.JWT) == "" { + return false + } + + return isLocalDevenvEndpoint(participant.GRPCLedgerAPIURL) +} + +// isLocalDevenvEndpoint reports whether the gRPC ledger URL is a local devenv endpoint +// (plain gRPC, not TLS on :443). +func isLocalDevenvEndpoint(grpcURL string) bool { + grpcURL = strings.TrimSpace(grpcURL) + if grpcURL == "" { + return false + } + + hostPort := grpcURL + if idx := strings.LastIndex(hostPort, "/"); idx != -1 { + hostPort = hostPort[idx+1:] + } + + return !strings.HasSuffix(hostPort, ":443") +} + +func firstNonEmpty(values ...string) string { + for _, v := range values { + if v != "" { + return v + } + } + + return "" +} + +// participantAuthProvider validates auth config and builds an authentication.Provider. +func participantAuthProvider(ctx context.Context, auth commonconfig.AuthConfig) (authentication.Provider, error) { + if err := auth.Validate(); err != nil { + return nil, err + } + + return auth.NewProvider(ctx) +} diff --git a/ccip/devenv/participant_auth_test.go b/ccip/devenv/participant_auth_test.go new file mode 100644 index 000000000..292b68053 --- /dev/null +++ b/ccip/devenv/participant_auth_test.go @@ -0,0 +1,261 @@ +package devenv + +import ( + "testing" + + "github.com/smartcontractkit/chainlink-testing-framework/framework/components/blockchain" + + "github.com/smartcontractkit/chainlink-canton/commonconfig" +) + +const validJWT = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.KMUFsIDTnFmyG3nMiGM6H9FNFUROf3wh7SmqJp-QV30" + +func TestBuildParticipantAuthConfig(t *testing.T) { + const localGRPC = "participant1.grpc-ledger-api.localhost:8080" + const tlsGRPC = "canton-devnet.example.com:443" + + tests := []struct { + name string + participant blockchain.CantonParticipantEndpoints + env map[string]string + want commonconfig.AuthConfig + wantErr bool + validate bool + }{ + { + name: "devenv_jwt_local_endpoint_insecureStatic", + participant: blockchain.CantonParticipantEndpoints{ + GRPCLedgerAPIURL: localGRPC, + UserID: "user-participant1", + JWT: validJWT, + }, + want: commonconfig.AuthConfig{ + Type: commonconfig.AuthTypeInsecureStatic, + JWT: validJWT, + }, + validate: true, + }, + { + name: "devenv_jwt_tls_endpoint_not_insecureStatic", + participant: blockchain.CantonParticipantEndpoints{ + GRPCLedgerAPIURL: tlsGRPC, + UserID: "user-participant1", + JWT: validJWT, + }, + env: map[string]string{ + envCantonAuthURL: "https://auth.example.com/", + envCantonOAuthClientID: "client-id", + envCantonOAuthClientSecret: "client-secret", + }, + want: commonconfig.AuthConfig{ + Type: commonconfig.AuthTypeClientCredentials, + AuthURL: "https://auth.example.com/", + ClientID: "client-id", + ClientSecret: "client-secret", + }, + validate: true, + }, + { + name: "clientCredentials_from_env_without_jwt", + participant: blockchain.CantonParticipantEndpoints{ + GRPCLedgerAPIURL: tlsGRPC, + }, + env: map[string]string{ + envCantonAuthURL: "https://auth.example.com/", + envCantonOAuthClientID: "client-id", + envCantonOAuthClientSecret: "client-secret", + }, + want: commonconfig.AuthConfig{ + Type: commonconfig.AuthTypeClientCredentials, + AuthURL: "https://auth.example.com/", + ClientID: "client-id", + ClientSecret: "client-secret", + }, + validate: true, + }, + { + name: "clientCredentials_from_ci_env_names", + participant: blockchain.CantonParticipantEndpoints{ + GRPCLedgerAPIURL: tlsGRPC, + }, + env: map[string]string{ + envCantonAuthURL: "https://auth.example.com/", + envClientID: "client-id", + envClientSecret: "client-secret", + }, + want: commonconfig.AuthConfig{ + Type: commonconfig.AuthTypeClientCredentials, + AuthURL: "https://auth.example.com/", + ClientID: "client-id", + ClientSecret: "client-secret", + }, + validate: true, + }, + { + name: "clientCredentials_missing_client_secret_fails_validation", + participant: blockchain.CantonParticipantEndpoints{ + GRPCLedgerAPIURL: tlsGRPC, + }, + env: map[string]string{ + envCantonAuthURL: "https://auth.example.com/", + envCantonOAuthClientID: "client-id", + }, + want: commonconfig.AuthConfig{ + Type: commonconfig.AuthTypeClientCredentials, + AuthURL: "https://auth.example.com/", + ClientID: "client-id", + }, + validate: true, + wantErr: true, + }, + { + name: "explicit_static_with_env_jwt_override", + participant: blockchain.CantonParticipantEndpoints{ + GRPCLedgerAPIURL: tlsGRPC, + JWT: validJWT, + }, + env: map[string]string{ + envCantonAuthType: commonconfig.AuthTypeStatic, + envOnchainCantonJWT: validJWT, + }, + want: commonconfig.AuthConfig{ + Type: commonconfig.AuthTypeStatic, + JWT: validJWT, + }, + validate: true, + }, + { + name: "explicit_static_missing_jwt", + participant: blockchain.CantonParticipantEndpoints{ + GRPCLedgerAPIURL: tlsGRPC, + }, + env: map[string]string{ + envCantonAuthType: commonconfig.AuthTypeStatic, + }, + wantErr: true, + }, + { + name: "local_without_jwt_defaults_clientCredentials", + participant: blockchain.CantonParticipantEndpoints{ + GRPCLedgerAPIURL: localGRPC, + }, + env: map[string]string{ + envCantonAuthURL: "https://auth.example.com/", + envCantonOAuthClientID: "client-id", + envCantonOAuthClientSecret: "client-secret", + }, + want: commonconfig.AuthConfig{ + Type: commonconfig.AuthTypeClientCredentials, + AuthURL: "https://auth.example.com/", + ClientID: "client-id", + ClientSecret: "client-secret", + }, + validate: true, + }, + { + name: "explicit_authorizationCode", + participant: blockchain.CantonParticipantEndpoints{ + GRPCLedgerAPIURL: tlsGRPC, + }, + env: map[string]string{ + envCantonAuthType: commonconfig.AuthTypeAuthorizationCode, + envCantonAuthURL: "https://auth.example.com/", + envCantonOAuthClientID: "client-id", + }, + want: commonconfig.AuthConfig{ + Type: commonconfig.AuthTypeAuthorizationCode, + AuthURL: "https://auth.example.com/", + ClientID: "client-id", + }, + validate: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + for k := range tt.env { + t.Setenv(k, tt.env[k]) + } + for _, key := range []string{ + envCantonAuthType, + envCantonAuthURL, + envCantonOAuthClientID, + envCantonOAuthClientSecret, + envClientID, + envClientSecret, + envOnchainCantonJWT, + } { + if _, ok := tt.env[key]; !ok { + t.Setenv(key, "") + } + } + + got, err := buildParticipantAuthConfig(tt.participant) + if tt.wantErr { + if err == nil && tt.validate { + if validateErr := got.Validate(); validateErr == nil { + t.Fatal("expected error, got nil") + } + + return + } + if err == nil { + t.Fatal("expected error, got nil") + } + + return + } + if err != nil { + t.Fatalf("buildParticipantAuthConfig() error = %v", err) + } + if got != tt.want { + t.Fatalf("buildParticipantAuthConfig() = %+v, want %+v", got, tt.want) + } + if tt.validate { + if err := got.Validate(); err != nil { + t.Fatalf("Validate() error = %v", err) + } + } + }) + } +} + +func TestIsLocalDevenvEndpoint(t *testing.T) { + t.Parallel() + + tests := []struct { + grpcURL string + want bool + }{ + {grpcURL: "participant1.grpc-ledger-api.localhost:8080", want: true}, + {grpcURL: "canton-devnet.example.com:443", want: false}, + {grpcURL: "", want: false}, + } + + for _, tt := range tests { + if got := isLocalDevenvEndpoint(tt.grpcURL); got != tt.want { + t.Errorf("isLocalDevenvEndpoint(%q) = %v, want %v", tt.grpcURL, got, tt.want) + } + } +} + +func TestBuildParticipantAuthConfig_no_jwt_priority_on_real_chain(t *testing.T) { + t.Setenv(envOnchainCantonJWT, validJWT) + t.Setenv(envCantonAuthURL, "https://auth.example.com/") + t.Setenv(envCantonOAuthClientID, "client-id") + t.Setenv(envCantonOAuthClientSecret, "client-secret") + t.Setenv(envCantonAuthType, "") + + got, err := buildParticipantAuthConfig(blockchain.CantonParticipantEndpoints{ + GRPCLedgerAPIURL: "canton-devnet.example.com:443", + }) + if err != nil { + t.Fatalf("buildParticipantAuthConfig() error = %v", err) + } + if got.Type != commonconfig.AuthTypeClientCredentials { + t.Fatalf("type = %q, want clientCredentials (env JWT must not imply static)", got.Type) + } + if got.JWT != "" { + t.Fatalf("JWT should be empty for clientCredentials, got %q", got.JWT) + } +} diff --git a/ccip/devenv/participant_connect_test.go b/ccip/devenv/participant_connect_test.go new file mode 100644 index 000000000..c1e0e932d --- /dev/null +++ b/ccip/devenv/participant_connect_test.go @@ -0,0 +1,95 @@ +package devenv + +import ( + "context" + "os" + "strings" + "testing" + "time" + + "github.com/smartcontractkit/chainlink-testing-framework/framework/components/blockchain" + "google.golang.org/grpc" + "google.golang.org/grpc/connectivity" + "google.golang.org/grpc/credentials" + + "github.com/smartcontractkit/chainlink-canton/commonconfig" +) + +func TestConnectCantonParticipant(t *testing.T) { + grpcURL := strings.TrimSpace(os.Getenv("CANTON_GRPC_URL")) + authURL := strings.TrimSpace(os.Getenv(envCantonAuthURL)) + clientID := oauthClientID() + clientSecret := oauthClientSecret() + + if grpcURL == "" || authURL == "" || clientID == "" || clientSecret == "" { + t.Skip("skipping: set CANTON_GRPC_URL, CANTON_AUTH_URL, and CLIENT_ID/CLIENT_SECRET (or CANTON_OAUTH_*) to run") + } + + authCfg, err := buildParticipantAuthConfig(blockchain.CantonParticipantEndpoints{ + GRPCLedgerAPIURL: grpcURL, + }) + if err != nil { + t.Fatalf("buildParticipantAuthConfig: %v", err) + } + if authCfg.Type != commonconfig.AuthTypeClientCredentials { + t.Fatalf("expected clientCredentials auth, got %q", authCfg.Type) + } + + ctx := context.Background() + provider, err := participantAuthProvider(ctx, authCfg) + if err != nil { + t.Fatalf("participantAuthProvider: %v", err) + } + + requireBearerToken(t, ctx, provider) + + conn, err := grpc.NewClient( + grpcURL, + grpc.WithTransportCredentials(provider.TransportCredentials()), + grpc.WithPerRPCCredentials(provider.PerRPCCredentials()), + ) + if err != nil { + t.Fatalf("grpc.NewClient: %v", err) + } + defer conn.Close() + + waitForGRPCReady(t, ctx, conn) + t.Log("connected to Canton participant (client credentials OAuth + authenticated gRPC channel)") +} + +func requireBearerToken(t *testing.T, ctx context.Context, provider interface { + PerRPCCredentials() credentials.PerRPCCredentials +}) { + t.Helper() + + md, err := provider.PerRPCCredentials().GetRequestMetadata(ctx, "https://canton/") + if err != nil { + t.Fatalf("fetch OAuth access token: %v", err) + } + + for key, value := range md { + if strings.EqualFold(key, "authorization") && strings.HasPrefix(strings.ToLower(value), "bearer ") { + return + } + } + + t.Fatalf("expected bearer token in RPC metadata, got %#v", md) +} + +func waitForGRPCReady(t *testing.T, ctx context.Context, conn *grpc.ClientConn) { + t.Helper() + + waitCtx, cancel := context.WithTimeout(ctx, 30*time.Second) + defer cancel() + + conn.Connect() + for { + state := conn.GetState() + if state == connectivity.Ready { + return + } + if !conn.WaitForStateChange(waitCtx, state) { + t.Fatalf("timed out waiting for gRPC connection (last state: %s)", state) + } + } +} diff --git a/commonconfig/auth.go b/commonconfig/auth.go index a28674cbf..6f0016682 100644 --- a/commonconfig/auth.go +++ b/commonconfig/auth.go @@ -33,8 +33,8 @@ type AuthConfig struct { // Defaults to "static" when omitted (backward compatible). Type string `toml:"type" validate:"required,oneof=static insecureStatic clientCredentials authorizationCode"` - // UserID is the user ID for the authentication. Required for clientCredentials and authorizationCode only. - UserID string `toml:"user_id" validate:"required_if=Type clientCredentials,required_if=Type authorizationCode"` + // UserID is an optional Canton ledger user identifier for downstream services (not used for OAuth). + UserID string `toml:"user_id,omitempty"` // JWT is a pre-obtained token. Required when Type is "static" or "insecureStatic". JWT string `toml:"jwt,omitempty" validate:"required_if=Type static,required_if=Type insecureStatic,excluded_unless=Type static|excluded_unless=Type insecureStatic,omitempty,jwt"` diff --git a/commonconfig/auth_test.go b/commonconfig/auth_test.go index 816e2745e..937383495 100644 --- a/commonconfig/auth_test.go +++ b/commonconfig/auth_test.go @@ -82,7 +82,7 @@ func TestAuthConfig_Validate(t *testing.T) { wantErr: true, }, - // --- clientCredentials: Type, UserID, AuthURL, ClientID, ClientSecret required; AuthURL must be valid URL --- + // --- clientCredentials: Type, AuthURL, ClientID, ClientSecret required; AuthURL must be valid URL --- { name: "clientCredentials_valid", config: AuthConfig{ @@ -95,14 +95,14 @@ func TestAuthConfig_Validate(t *testing.T) { wantErr: false, }, { - name: "clientCredentials_missing_user_id", + name: "clientCredentials_valid_without_user_id", config: AuthConfig{ Type: AuthTypeClientCredentials, AuthURL: "https://auth.example.com/", ClientID: "client-id", ClientSecret: "client-secret", }, - wantErr: true, + wantErr: false, }, { name: "clientCredentials_missing_auth_url", @@ -146,7 +146,7 @@ func TestAuthConfig_Validate(t *testing.T) { wantErr: true, }, - // --- authorizationCode: Type, UserID, AuthURL, ClientID required; ClientSecret must be unset (excluded_unless clientCredentials) --- + // --- authorizationCode: Type, AuthURL, ClientID required; ClientSecret must be unset (excluded_unless clientCredentials) --- { name: "authorizationCode_valid", config: AuthConfig{ @@ -158,13 +158,13 @@ func TestAuthConfig_Validate(t *testing.T) { wantErr: false, }, { - name: "authorizationCode_missing_user_id", + name: "authorizationCode_valid_without_user_id", config: AuthConfig{ Type: AuthTypeAuthorizationCode, AuthURL: "https://auth.example.com/", ClientID: "client-id", }, - wantErr: true, + wantErr: false, }, { name: "authorizationCode_missing_auth_url",