From ecf05651536de5f398af513f554cd87d37012681 Mon Sep 17 00:00:00 2001 From: Jorge Turrado Date: Fri, 10 Apr 2026 17:50:49 +0200 Subject: [PATCH] feat: Support setting federated token as env Signed-off-by: Jorge Turrado --- README.md | 7 ++- core/clients/workload_identity_flow.go | 10 +++- core/clients/workload_identity_flow_test.go | 51 ++++++++++++++++++++- 3 files changed, 65 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index dff4aef7f..f9bb567f0 100644 --- a/README.md +++ b/README.md @@ -147,13 +147,18 @@ For each authentication method, the try order is: ```go // Using wokload identity federation flow config.WithWorkloadIdentityFederationAuth() -// With the custom path for the external OIDC token +// With the external OIDC token +config.WithWorkloadIdentityFederationToken("OIDC Token") +// OR With the custom path for the external OIDC token config.WithWorkloadIdentityFederationPath("/path/to/your/federated/token") // For the service account config.WithServiceAccountEmail("my-sa@sa-stackit.cloud") ``` **B. Environment Variables** ```bash +# Preferred: provide the external OIDC token directly +# (has priority over STACKIT_FEDERATED_TOKEN_FILE) +STACKIT_FEDERATED_TOKEN= # With the custom path for the external OIDC token STACKIT_FEDERATED_TOKEN_FILE=/path/to/your/federated/token # For the service account diff --git a/core/clients/workload_identity_flow.go b/core/clients/workload_identity_flow.go index 73a1ed272..dfb404bf6 100644 --- a/core/clients/workload_identity_flow.go +++ b/core/clients/workload_identity_flow.go @@ -5,6 +5,7 @@ import ( "fmt" "net/http" "net/url" + "os" "strings" "sync" "time" @@ -15,6 +16,7 @@ import ( const ( clientIDEnv = "STACKIT_SERVICE_ACCOUNT_EMAIL" + FederatedTokenEnv = "STACKIT_FEDERATED_TOKEN" //nolint:gosec // This is not a secret, just the env variable name FederatedTokenFileEnv = "STACKIT_FEDERATED_TOKEN_FILE" //nolint:gosec // This is not a secret, just the env variable name wifTokenEndpointEnv = "STACKIT_IDP_TOKEN_ENDPOINT" //nolint:gosec // This is not a secret, just the env variable name wifTokenExpirationEnv = "STACKIT_IDP_TOKEN_EXPIRATION_SECONDS" //nolint:gosec // This is not a secret, just the env variable name @@ -134,7 +136,13 @@ func (c *WorkloadIdentityFederationFlow) Init(cfg *WorkloadIdentityFederationFlo } if c.config.FederatedTokenFunction == nil { - c.config.FederatedTokenFunction = oidcadapters.ReadJWTFromFileSystem(utils.GetEnvOrDefault(FederatedTokenFileEnv, defaultFederatedTokenPath)) + if token, ok := os.LookupEnv(FederatedTokenEnv); ok { + c.config.FederatedTokenFunction = func(_ context.Context) (string, error) { + return token, nil + } + } else { + c.config.FederatedTokenFunction = oidcadapters.ReadJWTFromFileSystem(utils.GetEnvOrDefault(FederatedTokenFileEnv, defaultFederatedTokenPath)) + } } c.tokenExpirationLeeway = defaultTokenExpirationLeeway diff --git a/core/clients/workload_identity_flow_test.go b/core/clients/workload_identity_flow_test.go index 7d59593f4..a839395bb 100644 --- a/core/clients/workload_identity_flow_test.go +++ b/core/clients/workload_identity_flow_test.go @@ -25,6 +25,8 @@ func TestWorkloadIdentityFlowInit(t *testing.T) { customTokenUrlEnv bool tokenExpiration string validAssertion bool + federatedTokenAsEnv bool + tokenFilePathEnv string tokenFilePathAsEnv bool missingTokenFilePath bool wantErr bool @@ -53,6 +55,19 @@ func TestWorkloadIdentityFlowInit(t *testing.T) { validAssertion: true, wantErr: false, }, + { + name: "ok using federated token from env", + clientID: "test@stackit.cloud", + federatedTokenAsEnv: true, + wantErr: false, + }, + { + name: "federated token env has priority over invalid token file", + clientID: "test@stackit.cloud", + federatedTokenAsEnv: true, + tokenFilePathEnv: "/tmp/not-existing-token-file", + wantErr: false, + }, { name: "missing client id", validAssertion: true, @@ -87,7 +102,19 @@ func TestWorkloadIdentityFlowInit(t *testing.T) { flowConfig.TokenExpiration = tt.tokenExpiration } - if !tt.missingTokenFilePath { + if tt.federatedTokenAsEnv { + token, err := signTokenWithSubject("subject", time.Minute) + if err != nil { + t.Fatalf("failed to create token: %v", err) + } + t.Setenv("STACKIT_FEDERATED_TOKEN", token) + } + + if tt.tokenFilePathEnv != "" { + t.Setenv("STACKIT_FEDERATED_TOKEN_FILE", tt.tokenFilePathEnv) + } + + if !tt.missingTokenFilePath && !tt.federatedTokenAsEnv { file, err := os.CreateTemp("", "*.token") if err != nil { log.Fatal(err) @@ -118,6 +145,17 @@ func TestWorkloadIdentityFlowInit(t *testing.T) { if err := flow.Init(flowConfig); (err != nil) != tt.wantErr { t.Errorf("KeyFlow.Init() error = %v, wantErr %v", err, tt.wantErr) } + + if tt.federatedTokenAsEnv && !tt.wantErr { + tokenFromConfig, err := flow.config.FederatedTokenFunction(context.Background()) + if err != nil { + t.Fatalf("getting federated token from config: %v", err) + } + tokenFromEnv := os.Getenv("STACKIT_FEDERATED_TOKEN") + if tokenFromConfig != tokenFromEnv { + t.Errorf("federated token mismatch, want env token") + } + } if flow.config == nil { t.Error("config is nil") } @@ -156,6 +194,7 @@ func TestWorkloadIdentityFlowRoundTrip(t *testing.T) { clientID string validAssertion bool injectToken bool + tokenAsEnv bool wantErr bool }{ { @@ -177,6 +216,13 @@ func TestWorkloadIdentityFlowRoundTrip(t *testing.T) { validAssertion: false, wantErr: true, }, + { + name: "token from env ok", + clientID: "test@stackit.cloud", + validAssertion: true, + tokenAsEnv: true, + wantErr: false, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -268,6 +314,9 @@ func TestWorkloadIdentityFlowRoundTrip(t *testing.T) { flowConfig.FederatedTokenFunction = func(context.Context) (string, error) { return token, nil } + } else if tt.tokenAsEnv { + t.Setenv("STACKIT_FEDERATED_TOKEN", token) + t.Setenv("STACKIT_FEDERATED_TOKEN_FILE", "/tmp/not-existing-token-file") } else { file, err := os.CreateTemp("", "*.token") if err != nil {