Repository: Azure/kubelogin

Issue:
Support Azure Pipelines Environment Variables for AzurePipelinesCredential
## Problem

The `azurepipelinescredential` was added in commit 721015c but it doesn't recognize Azure Pipelines-specific environment variables that Azure DevOps automatically sets when using service connections.

### Current Behavior

Users must manually specify these flags even when using Azure Pipelines tasks:
- `--tenant-id`
- `--client-id` 
- `--azure-pipelines-service-connection-id`

### Expected Behavior

When using Azure Resource Manager service connections in Azure Pipelines (e.g., with `AzureCLI@2` task with `addSpnToEnvironment: true`), the following environment variables are automatically set and should be recognized:
- `AZURESUBSCRIPTION_TENANT_ID` - The tenant ID of the service connection
- `AZURESUBSCRIPTION_SERVICE_CONNECTION_ID` - The resource ID of the service connection
- `AZURESUBSCRIPTION_CLIENT_ID` - The client ID associated with the service connection

### Why This Matters

1. **Consistency**: Other login methods (Workload Identity, SPN, etc.) properly recognize Azure-standard environment variables via `UpdateFromEnv()`
2. **Usability**: Users shouldn't need to manually extract and pass values that Azure Pipelines already provides
3. **Automation**: Simplifies pipeline configuration and reduces potential for configuration errors

## Proposed Solution

### 1. Add New Environment Variable Constants

**File**: `pkg/internal/env/variables.go`

Add constants for Azure Pipelines service connection environment variables:
```go
// env vars used by Azure Pipelines service connections
AzureSubscriptionTenantID              = "AZURESUBSCRIPTION_TENANT_ID"
AzureSubscriptionServiceConnectionID   = "AZURESUBSCRIPTION_SERVICE_CONNECTION_ID"
AzureSubscriptionClientID              = "AZURESUBSCRIPTION_CLIENT_ID"
```

### 2. Update `UpdateFromEnv()` Function

**File**: `pkg/internal/token/options.go`

Add conditional block for `AzurePipelinesLogin` (similar to existing `WorkloadIdentityLogin` block):
```go
if o.LoginMethod == AzurePipelinesLogin {
    if v, ok := os.LookupEnv(env.AzureSubscriptionClientID); ok {
        o.ClientID = v
    }
    if v, ok := os.LookupEnv(env.AzureSubscriptionTenantID); ok {
        o.TenantID = v
    }
    if v, ok := os.LookupEnv(env.AzureSubscriptionServiceConnectionID); ok {
        o.AzurePipelinesServiceConnectionID = v
    }
}
```

**Note**: Command-line flags should take precedence over environment variables for backward compatibility.

### 3. Update Flag Help Text

**File**: `pkg/internal/token/options.go`

Update help text for relevant flags to mention the environment variables:
- `--tenant-id`: Mention `AZURESUBSCRIPTION_TENANT_ID` for Azure Pipelines
- `--client-id`: Mention `AZURESUBSCRIPTION_CLIENT_ID` for Azure Pipelines
- `--azure-pipelines-service-connection-id`: Mention `AZURESUBSCRIPTION_SERVICE_CONNECTION_ID`

### 4. Add Comprehensive Tests

**File**: `pkg/internal/token/options_test.go`

Add test cases for:
- Environment variable recognition for all three variables
- Command-line flags override environment variables
- Environment variables only read when `LoginMethod == AzurePipelinesLogin`
- Behavior when `DisableEnvironmentOverride` is true
- Test isolation to avoid environment variable pollution

### 5. Update Documentation

**File**: `docs/book/src/concepts/login-modes/azurepipelines.md`

Add section documenting:
- Automatic environment variable support
- Simplified examples without explicit flag passing
- How Azure Pipelines tasks automatically set these variables
- Examples with `AzureCLI@2` and other tasks

**Files**: `docs/book/src/cli/get-token.md` and `docs/book/src/cli/convert-kubeconfig.md`

Update CLI reference to mention environment variables for Azure Pipelines login method.

## Implementation Considerations

### Precedence Order
1. Command-line flags (highest priority)
2. Environment variables (AZURESUBSCRIPTION_*)
3. This maintains backward compatibility

### Timing
- `UpdateFromEnv()` is called before `Validate()` in the execution flow
- Environment variables are read after login method is determined
- Ensures proper conditional logic

### Backward Compatibility
- Existing users with explicit flags are not affected
- Users relying on current behavior see no change
- Only users who want to leverage environment variables benefit

### Security
- These environment variables are automatically set by Azure Pipelines
- They don't expose credentials directly (SYSTEM_ACCESSTOKEN is separate)
- No additional security concerns introduced

## Files to Modify

1. `pkg/internal/env/variables.go` - Add new constants
2. `pkg/internal/token/options.go` - Update `UpdateFromEnv()` and `AddFlags()` help text
3. `pkg/internal/token/options_test.go` - Add comprehensive test coverage
4. `docs/book/src/concepts/login-modes/azurepipelines.md` - Update documentation
5. `docs/book/src/cli/get-token.md` - Update CLI reference
6. `docs/book/src/cli/convert-kubeconfig.md` - Update CLI reference

## Success Criteria

- [ ] Users can run kubelogin in Azure Pipelines without explicitly passing flags when environment variables are available
- [ ] All existing tests pass
- [ ] New tests validate environment variable recognition
- [ ] Documentation clearly explains the feature
- [ ] Backward compatibility is maintained

## Example Usage

### Before (current)

```yaml
steps:
- task: AzureCLI@2
  inputs:
    azureSubscription: 'my-service-connection'
    scriptType: 'bash'
    inlineScript: |
      kubelogin convert-kubeconfig \
        --login azurepipelines \
        --tenant-id $(tenant-id) \
        --client-id $(client-id) \
        --server-id $(server-id) \
        --azure-pipelines-service-connection-id $(service-connection-resource-id)
    addSpnToEnvironment: true
```

### After (proposed)

```yaml
steps:
- task: AzureCLI@2
  inputs:
    azureSubscription: 'my-service-connection'
    scriptType: 'bash'
    inlineScript: |
      kubelogin convert-kubeconfig \
        --login azurepipelines \
        --server-id $(server-id)
      # tenant-id, client-id, and service-connection-id are automatically detected
    addSpnToEnvironment: true
```

## Related

- Commit 721015c: Add AzurePipelinesCredential to authentication options (#703)
- Similar pattern exists for WorkloadIdentityLogin in `UpdateFromEnv()`


Hints from issue comments:
@gravufo: This doesn't work at all when not using WorkloadIdentity. You can see [here](https://github.com/microsoft/azure-pipelines-tasks/blob/dbb0aed155d1ab67e0b2f69590d64ef10932f574/Tasks/AzureCLIV2/azureclitask.ts#L277) that the environment variables expected are only set when using WorkloadIdentity.
@weinong @weinong: @gravufo `AzurePipelinesCredential` doesn't work outside of workload identity use case. what is your scenario? @gravufo: @weinong nowhere is it written that this required workload identity...we still have service connections using spn. We thought this feature was smart enough to auto detect which login method to use based on the service connection configuration.@weinong: it does say that here
https://learn.microsoft.com/en-us/azure/devops/pipelines/library/connect-to-azure?view=azure-devops#create-an-azure-resource-manager-service-connection-that-uses-workload-identity-federation@gravufo: @weinong ah it's the underlying AzurePipelinesCredential that only works for WorkloadIdentity. My bad.

I thought (and would have liked) this feature to automatically select the proper variables based on whether the service connection is setup to use workload identity or service principal authentication.
Nowhere in the documentation ([here](https://azure.github.io/kubelogin/concepts/login-modes/azurepipelines.html)) is it written that this feature only works with workload identity authentication so it wasn't clear to me.

In fact, it even says:
```
Note: When using AzureCLI@2 task with Azure Resource Manager service connections, Azure Pipelines automatically sets the following environment variables which kubelogin will use if the corresponding flags are not provided:

- AZURESUBSCRIPTION_TENANT_ID - Automatically used for --tenant-id
- AZURESUBSCRIPTION_CLIENT_ID - Automatically used for --client-id
- AZURESUBSCRIPTION_SERVICE_CONNECTION_ID - Automatically used for --azure-pipelines-service-connection-id
This means you only need to provide the --server-id parameter when these environment variables are available.
```
Which is not true since those variables are only set when using workload identity authentication in an ARM service connection as I mentioned in my first comment.

Ideally, kubelogin could have automatically detected if the environment variable `idToken` has a value. If so, it is using workload identity. If not, it is using spn authentication.
This is what we currently do in our pipeline:
```powershell
if ($env:idToken -ne $null) {
  $loginType = "workloadidentity"
} else {
  $loginType = "spn"
}

# Convert kubeconfig to be able to login to Azure AD RBAC clusters
$params = "convert-kubeconfig", "-l", "$loginType", "--tenant-id", $env:tenantId, "--client-id", $env:servicePrincipalId
Write-Host "##[command]kubelogin $params"
kubelogin @params
```
I was expecting to replace all of that with a simple `kubelogin convert-kubeconfig -l azurepipelines` since the other variables can be easily inferred.

Oh well, I guess I'll just keep what I had.
Thanks for the information!

Solution patch:
diff --git a/docs/book/src/cli/get-token.md b/docs/book/src/cli/get-token.md
index febb6a2..2341e9e 100644
--- a/docs/book/src/cli/get-token.md
+++ b/docs/book/src/cli/get-token.md
@@ -13,11 +13,11 @@ Usage:
 
 Flags:
       --authority-host string                          Workload Identity authority host. It may be specified in AZURE_AUTHORITY_HOST environment variable
-      --azure-pipelines-service-connection-id string   Service connection (resource) ID used by azurepipelines login method
+      --azure-pipelines-service-connection-id string   Service connection (resource) ID used by azurepipelines login method. It may be specified in AZURESUBSCRIPTION_SERVICE_CONNECTION_ID environment variable
       --cache-dir string                               directory to cache authentication record (default "/home/weinongw/.kube/cache/kubelogin/")
       --client-certificate string            AAD client cert in pfx. Used in spn login. It may be specified in AAD_SERVICE_PRINCIPAL_CLIENT_CERTIFICATE or AZURE_CLIENT_CERTIFICATE_PATH environment variable
       --client-certificate-password string   Password for AAD client cert. Used in spn login. It may be specified in AAD_SERVICE_PRINCIPAL_CLIENT_CERTIFICATE_PASSWORD or AZURE_CLIENT_CERTIFICATE_PASSWORD environment variable
-      --client-id string                     AAD client application ID. It may be specified in AAD_SERVICE_PRINCIPAL_CLIENT_ID or AZURE_CLIENT_ID environment variable
+      --client-id string                     AAD client application ID. It may be specified in AAD_SERVICE_PRINCIPAL_CLIENT_ID or AZURE_CLIENT_ID environment variable. For Azure Pipelines login, it may be specified in AZURESUBSCRIPTION_CLIENT_ID environment variable
       --client-secret string                 AAD client application secret. Used in spn login. It may be specified in AAD_SERVICE_PRINCIPAL_CLIENT_SECRET or AZURE_CLIENT_SECRET environment variable
       --disable-environment-override         Enable or disable the use of env-variables. Default false
       --disable-instance-discovery           set to true to disable instance discovery in environments with their own simple Identity Provider (not AAD) that do not have instance metadata discovery endpoint. Default false
@@ -33,7 +33,7 @@ Flags:
       --pop-enabled                          set to true to use a PoP token for authentication or false to use a regular bearer token
       --redirect-url string                  The URL Microsoft Entra ID will redirect to with the access token. This is only used for interactive login. This is an optional parameter.
       --server-id string                     AAD server application ID
-  -t, --tenant-id string                     AAD tenant ID. It may be specified in AZURE_TENANT_ID environment variable
+  -t, --tenant-id string                     AAD tenant ID. It may be specified in AZURE_TENANT_ID environment variable. For Azure Pipelines login, it may be specified in AZURESUBSCRIPTION_TENANT_ID environment variable
       --timeout duration                     Timeout duration for Azure CLI token requests. It may be specified in AZURE_CLI_TIMEOUT environment variable (default 30s)
       --use-azurerm-env-vars                 Use environment variable names of Terraform Azure Provider (ARM_CLIENT_ID, ARM_CLIENT_SECRET, ARM_CLIENT_CERTIFICATE_PATH, ARM_CLIENT_CERTIFICATE_PASSWORD, ARM_TENANT_ID)
       --username string                      user name for ropc login flow. It may be specified in AAD_USER_PRINCIPAL_NAME or AZURE_USERNAME environment variable
@@ -232,6 +232,28 @@ users:
 
 ### Azure Pipelines
 
+When using `AzureCLI@2` task with Azure Resource Manager service connections, environment variables are automatically set. You only need to provide the `--server-id`:
+
+```yaml
+kind: Config
+preferences: {}
+users:
+  - name: demouser
+    user:
+      exec:
+        apiVersion: client.authentication.k8s.io/v1beta1
+        args:
+          - get-token
+          - --server-id
+          - <AAD server app ID>
+          - --login
+          - azurepipelines
+        command: kubelogin
+        env: null
+```
+
+If environment variables are not available, provide all parameters explicitly:
+
 ```yaml
 kind: Config
 preferences: {}
@@ -255,3 +277,8 @@ users:
         command: kubelogin
         env: null
 ```
+
+> **Note**: When using `AzureCLI@2` task with Azure Resource Manager service connections, the following environment variables are automatically set and used:
+> - `AZURESUBSCRIPTION_TENANT_ID` for `--tenant-id`
+> - `AZURESUBSCRIPTION_CLIENT_ID` for `--client-id`
+> - `AZURESUBSCRIPTION_SERVICE_CONNECTION_ID` for `--azure-pipelines-service-connection-id`
diff --git a/docs/book/src/concepts/login-modes/azurepipelines.md b/docs/book/src/concepts/login-modes/azurepipelines.md
index 50013e0..6e871dd 100644
--- a/docs/book/src/concepts/login-modes/azurepipelines.md
+++ b/docs/book/src/concepts/login-modes/azurepipelines.md
@@ -23,6 +23,13 @@ The authentication leverages Azure Pipelines' managed identity integration throu
 - `--server-id`: Application ID of the server/resource (typically the AKS cluster's server ID)
 - `--azure-pipelines-service-connection-id`: The resource ID of the Azure Resource Manager service connection
 
+> **Note**: When using `AzureCLI@2` task with Azure Resource Manager service connections, Azure Pipelines automatically sets the following environment variables which kubelogin will use if the corresponding flags are not provided:
+> - `AZURESUBSCRIPTION_TENANT_ID` - Automatically used for `--tenant-id`
+> - `AZURESUBSCRIPTION_CLIENT_ID` - Automatically used for `--client-id`
+> - `AZURESUBSCRIPTION_SERVICE_CONNECTION_ID` - Automatically used for `--azure-pipelines-service-connection-id`
+>
+> This means you only need to provide the `--server-id` parameter when these environment variables are available.
+
 ## Usage Examples
 
 ### Basic Usage in Pipeline
@@ -37,7 +44,32 @@ steps:
     scriptType: 'bash'
     scriptLocation: 'inlineScript'
     inlineScript: |
+      # Download kubeconfig from AKS
+      az aks get-credentials -g ${RESOURCE_GROUP} -n ${AKS_NAME}
+      
       # Configure kubeconfig to use azurepipelines login
+      # tenant-id, client-id, and service-connection-id are automatically detected from environment variables
+      kubelogin convert-kubeconfig --login azurepipelines
+      
+      # Now kubectl commands will authenticate using Azure Pipelines credentials
+      kubectl get nodes
+```
+
+### Basic Usage with Explicit Parameters
+
+If you prefer to explicitly provide all parameters:
+
+```yaml
+# azure-pipelines.yml
+steps:
+- task: AzureCLI@2
+  displayName: 'Deploy to AKS'
+  inputs:
+    azureSubscription: 'my-service-connection'
+    scriptType: 'bash'
+    scriptLocation: 'inlineScript'
+    inlineScript: |
+      # Configure kubeconfig to use azurepipelines login with explicit parameters
       kubelogin convert-kubeconfig \
         --login azurepipelines \
         --tenant-id $(tenant-id) \
@@ -47,13 +79,18 @@ steps:
       
       # Now kubectl commands will authenticate using Azure Pipelines credentials
       kubectl get nodes
-    addSpnToEnvironment: true  # This enables SYSTEM_ACCESSTOKEN
 ```
 
 ### Direct Token Retrieval
 
 ```bash
 # In Azure DevOps pipeline (with "Allow scripts to access the OAuth token" enabled)
+# Simplified version - uses environment variables automatically set by Azure Pipelines
+kubelogin get-token \
+  --login azurepipelines \
+  --server-id <cluster-server-id>
+
+# Or with explicit parameters
 kubelogin get-token \
   --login azurepipelines \
   --tenant-id <tenant-id> \
@@ -62,42 +99,27 @@ kubelogin get-token \
   --azure-pipelines-service-connection-id <service-connection-resource-id>
 ```
 
-### Terraform Integration
+## Environment Variable Support
 
-```yaml
-# azure-pipelines.yml for Terraform deployments
-steps:
-- task: TerraformTaskV3@3
-  displayName: 'Terraform Apply'
-  inputs:
-    provider: 'azurerm'
-    command: 'apply'
-    workingDirectory: '$(System.DefaultWorkingDirectory)/terraform'
-    environmentServiceNameAzureRM: 'my-service-connection'
-    commandOptions: |
-      -auto-approve
-  env:
-    # Configure kubeconfig for kubectl provider in Terraform
-    KUBECONFIG: $(Agent.TempDirectory)/kubeconfig
-- script: |
-    # Convert kubeconfig to use azurepipelines authentication
-    kubelogin convert-kubeconfig \
-      --login azurepipelines \
-      --tenant-id $(tenant-id) \
-      --client-id $(client-id) \
-      --server-id $(server-id) \
-      --azure-pipelines-service-connection-id $(service-connection-resource-id) \
-      --kubeconfig $(Agent.TempDirectory)/kubeconfig
-  displayName: 'Configure kubectl authentication'
-  condition: always()
-```
+When using `AzureCLI@2` task with Azure Resource Manager service connections, Azure Pipelines automatically sets environment variables for the service connection. Kubelogin automatically detects and uses these variables:
+
+| Environment Variable | Used For | Command-line Flag Equivalent |
+|---------------------|----------|------------------------------|
+| `AZURESUBSCRIPTION_TENANT_ID` | Tenant ID | `--tenant-id` |
+| `AZURESUBSCRIPTION_CLIENT_ID` | Client ID | `--client-id` |
+| `AZURESUBSCRIPTION_SERVICE_CONNECTION_ID` | Service Connection ID | `--azure-pipelines-service-connection-id` |
+
+**Precedence**: Command-line flags always take precedence over environment variables. This allows you to override specific values when needed.
+
+**Disabling Environment Variables**: You can use the `--disable-environment-override` flag to ignore all environment variables and require explicit parameters.
 
 ## How It Works
 
 1. **Service Connection**: Azure DevOps service connections provide managed identity or service principal authentication to Azure resources
 2. **System Access Token**: When "Allow scripts to access the OAuth token" is enabled, Azure Pipelines provides a `SYSTEM_ACCESSTOKEN` environment variable
-3. **OIDC Integration**: The `azurepipelines` login method uses Azure SDK's `AzurePipelinesCredential` to exchange the system access token for an Azure AD token
-4. **Token Caching**: Authentication tokens are cached to improve performance across multiple kubectl operations
+3. **Environment Variables**: When using `AzureCLI@2` task with Azure Resource Manager service connections, Azure Pipelines automatically sets subscription-specific environment variables
+4. **OIDC Integration**: The `azurepipelines` login method uses Azure SDK's `AzurePipelinesCredential` to exchange the system access token for an Azure AD token
+5. **Token Caching**: Authentication tokens are cached to improve performance across multiple kubectl operations
 
 ## Troubleshooting
 
diff --git a/pkg/internal/env/variables.go b/pkg/internal/env/variables.go
index 6cf3384..ea8e913 100644
--- a/pkg/internal/env/variables.go
+++ b/pkg/internal/env/variables.go
@@ -31,4 +31,9 @@ const (
 	// env vars used by Azure Pipelines
 	SystemAccessToken    = "SYSTEM_ACCESSTOKEN"
 	SystemOIDCRequestURI = "SYSTEM_OIDCREQUESTURI"
+
+	// env vars used by Azure Pipelines service connections
+	AzureSubscriptionTenantID            = "AZURESUBSCRIPTION_TENANT_ID"
+	AzureSubscriptionServiceConnectionID = "AZURESUBSCRIPTION_SERVICE_CONNECTION_ID"
+	AzureSubscriptionClientID            = "AZURESUBSCRIPTION_CLIENT_ID"
 )
diff --git a/pkg/internal/token/options.go b/pkg/internal/token/options.go
index ad21073..2989fc6 100644
--- a/pkg/internal/token/options.go
+++ b/pkg/internal/token/options.go
@@ -93,7 +93,7 @@ func (o *Options) AddFlags(fs *pflag.FlagSet) {
 	fs.StringVarP(&o.LoginMethod, "login", "l", o.LoginMethod,
 		fmt.Sprintf("Login method. Supported methods: %s. It may be specified in %s environment variable", GetSupportedLogins(), env.LoginMethod))
 	fs.StringVar(&o.ClientID, "client-id", o.ClientID,
-		fmt.Sprintf("AAD client application ID. It may be specified in %s or %s environment variable", env.KubeloginClientID, env.AzureClientID))
+		fmt.Sprintf("AAD client application ID. It may be specified in %s or %s environment variable. For Azure Pipelines login, it may be specified in %s environment variable", env.KubeloginClientID, env.AzureClientID, env.AzureSubscriptionClientID))
 	fs.StringVar(&o.ClientSecret, "client-secret", o.ClientSecret,
 		fmt.Sprintf("AAD client application secret. Used in spn login. It may be specified in %s or %s environment variable", env.KubeloginClientSecret, env.AzureClientSecret))
 	fs.StringVar(&o.ClientCert, "client-certificate", o.ClientCert,
@@ -111,11 +111,11 @@ func (o *Options) AddFlags(fs *pflag.FlagSet) {
 	fs.StringVar(&o.AuthorityHost, "authority-host", o.AuthorityHost,
 		fmt.Sprintf("Workload Identity authority host. It may be specified in %s environment variable", env.AzureAuthorityHost))
 	fs.StringVar(&o.AzurePipelinesServiceConnectionID, "azure-pipelines-service-connection-id", o.AzurePipelinesServiceConnectionID,
-		"Service connection (resource) ID used by azurepipelines login method")
+		fmt.Sprintf("Service connection (resource) ID used by azurepipelines login method. It may be specified in %s environment variable", env.AzureSubscriptionServiceConnectionID))
 	fs.StringVar(&o.AuthRecordCacheDir, "token-cache-dir", o.AuthRecordCacheDir, "directory to cache authentication record")
 	_ = fs.MarkDeprecated("token-cache-dir", "use --cache-dir instead")
 	fs.StringVar(&o.AuthRecordCacheDir, "cache-dir", o.AuthRecordCacheDir, "directory to cache authentication record")
-	fs.StringVarP(&o.TenantID, "tenant-id", "t", o.TenantID, fmt.Sprintf("AAD tenant ID. It may be specified in %s environment variable", env.AzureTenantID))
+	fs.StringVarP(&o.TenantID, "tenant-id", "t", o.TenantID, fmt.Sprintf("AAD tenant ID. It may be specified in %s environment variable. For Azure Pipelines login, it may be specified in %s environment variable", env.AzureTenantID, env.AzureSubscriptionTenantID))
 	fs.StringVarP(&o.Environment, "environment", "e", o.Environment, "Azure environment name")
 	fs.BoolVar(&o.IsLegacy, "legacy", o.IsLegacy, "set to true to get token with 'spn:' prefix in audience claim")
 	fs.BoolVar(&o.UseAzureRMTerraformEnv, "use-azurerm-env-vars", o.UseAzureRMTerraformEnv,
@@ -168,22 +168,6 @@ func (o *Options) Validate() error {
 		return fmt.Errorf("timeout must be greater than 0")
 	}
 
-	// Azure Pipelines login method validation
-	if o.LoginMethod == AzurePipelinesLogin {
-		if o.TenantID == "" {
-			return fmt.Errorf("tenant ID is required for azurepipelines login method")
-		}
-		if o.AzurePipelinesServiceConnectionID == "" {
-			return fmt.Errorf("--azure-pipelines-service-connection-id is required for --login azurepipelines")
-		}
-		if os.Getenv(env.SystemAccessToken) == "" {
-			return fmt.Errorf("environment variable %s not set; enable \"Allow scripts to access the OAuth token\" in the pipeline", env.SystemAccessToken)
-		}
-		if os.Getenv(env.SystemOIDCRequestURI) == "" {
-			return fmt.Errorf("environment variable %s not set; this should be automatically set by Azure Pipelines", env.SystemOIDCRequestURI)
-		}
-	}
-
 	return nil
 }
 
@@ -267,6 +251,25 @@ func (o *Options) UpdateFromEnv() {
 			o.AuthorityHost = v
 		}
 	}
+
+	if o.LoginMethod == AzurePipelinesLogin {
+		if o.ClientID == "" {
+			if v, ok := os.LookupEnv(env.AzureSubscriptionClientID); ok {
+				o.ClientID = v
+			}
+		}
+		if o.TenantID == "" {
+			if v, ok := os.LookupEnv(env.AzureSubscriptionTenantID); ok {
+				o.TenantID = v
+			}
+		}
+		if o.AzurePipelinesServiceConnectionID == "" {
+			if v, ok := os.LookupEnv(env.AzureSubscriptionServiceConnectionID); ok {
+				o.AzurePipelinesServiceConnectionID = v
+			}
+		}
+	}
+
 	if v, ok := os.LookupEnv("AZURE_CLI_TIMEOUT"); ok {
 		if timeout, err := time.ParseDuration(v); err == nil {
 			o.Timeout = timeout



Test patch:
diff --git a/pkg/internal/token/options_test.go b/pkg/internal/token/options_test.go
index b13fc7a..6fd0e6f 100644
--- a/pkg/internal/token/options_test.go
+++ b/pkg/internal/token/options_test.go
@@ -124,7 +124,7 @@ func TestOptions(t *testing.T) {
 			errorSubstring   string
 		}{
 			{
-				name: "valid azurepipelines login",
+				name: "valid azurepipelines login with all parameters",
 				setupEnv: func() {
 					t.Setenv(env.SystemAccessToken, "test-token")
 					t.Setenv(env.SystemOIDCRequestURI, "https://test.oidc.request.uri")
@@ -133,72 +133,43 @@ func TestOptions(t *testing.T) {
 					o := defaultOptions()
 					o.LoginMethod = AzurePipelinesLogin
 					o.TenantID = "test-tenant"
+					o.ClientID = "test-client"
 					o.AzurePipelinesServiceConnectionID = "test-service-connection"
 					return o
 				},
 				expectError: false,
 			},
 			{
-				name: "azurepipelines missing tenant ID",
+				name: "azurepipelines login without tenant ID is valid (can come from env)",
 				setupEnv: func() {
 					t.Setenv(env.SystemAccessToken, "test-token")
 					t.Setenv(env.SystemOIDCRequestURI, "https://test.oidc.request.uri")
+					t.Setenv(env.AzureSubscriptionTenantID, "env-tenant-id")
 				},
 				options: func() Options {
 					o := defaultOptions()
 					o.LoginMethod = AzurePipelinesLogin
+					o.ClientID = "test-client"
 					o.AzurePipelinesServiceConnectionID = "test-service-connection"
 					return o
 				},
-				expectError:    true,
-				errorSubstring: "tenant ID is required for azurepipelines login method",
+				expectError: false,
 			},
 			{
-				name: "azurepipelines missing service connection ID",
+				name: "azurepipelines login without service connection ID is valid (can come from env)",
 				setupEnv: func() {
 					t.Setenv(env.SystemAccessToken, "test-token")
 					t.Setenv(env.SystemOIDCRequestURI, "https://test.oidc.request.uri")
+					t.Setenv(env.AzureSubscriptionServiceConnectionID, "env-service-connection")
 				},
 				options: func() Options {
 					o := defaultOptions()
 					o.LoginMethod = AzurePipelinesLogin
 					o.TenantID = "test-tenant"
+					o.ClientID = "test-client"
 					return o
 				},
-				expectError:    true,
-				errorSubstring: "--azure-pipelines-service-connection-id is required for --login azurepipelines",
-			},
-			{
-				name: "azurepipelines missing SYSTEM_ACCESSTOKEN",
-				setupEnv: func() {
-					t.Setenv(env.SystemOIDCRequestURI, "https://test.oidc.request.uri")
-					// Don't set SYSTEM_ACCESSTOKEN
-				},
-				options: func() Options {
-					o := defaultOptions()
-					o.LoginMethod = AzurePipelinesLogin
-					o.TenantID = "test-tenant"
-					o.AzurePipelinesServiceConnectionID = "test-service-connection"
-					return o
-				},
-				expectError:    true,
-				errorSubstring: fmt.Sprintf("environment variable %s not set", env.SystemAccessToken),
-			},
-			{
-				name: "azurepipelines missing SYSTEM_OIDCREQUESTURI",
-				setupEnv: func() {
-					t.Setenv(env.SystemAccessToken, "test-token")
-					// Don't set SYSTEM_OIDCREQUESTURI
-				},
-				options: func() Options {
-					o := defaultOptions()
-					o.LoginMethod = AzurePipelinesLogin
-					o.TenantID = "test-tenant"
-					o.AzurePipelinesServiceConnectionID = "test-service-connection"
-					return o
-				},
-				expectError:    true,
-				errorSubstring: fmt.Sprintf("environment variable %s not set", env.SystemOIDCRequestURI),
+				expectError: false,
 			},
 		}
 
@@ -207,6 +178,8 @@ func TestOptions(t *testing.T) {
 				// Clean up environment variables before each test
 				originalSystemAccessToken := os.Getenv(env.SystemAccessToken)
 				originalSystemOIDCRequestURI := os.Getenv(env.SystemOIDCRequestURI)
+				originalTenantID := os.Getenv(env.AzureSubscriptionTenantID)
+				originalServiceConnectionID := os.Getenv(env.AzureSubscriptionServiceConnectionID)
 				defer func() {
 					if originalSystemAccessToken != "" {
 						os.Setenv(env.SystemAccessToken, originalSystemAccessToken)
@@ -218,6 +191,16 @@ func TestOptions(t *testing.T) {
 					} else {
 						os.Unsetenv(env.SystemOIDCRequestURI)
 					}
+					if originalTenantID != "" {
+						os.Setenv(env.AzureSubscriptionTenantID, originalTenantID)
+					} else {
+						os.Unsetenv(env.AzureSubscriptionTenantID)
+					}
+					if originalServiceConnectionID != "" {
+						os.Setenv(env.AzureSubscriptionServiceConnectionID, originalServiceConnectionID)
+					} else {
+						os.Unsetenv(env.AzureSubscriptionServiceConnectionID)
+					}
 				}()
 
 				test.setupEnv()
@@ -458,6 +441,140 @@ func TestDisableEnvironmentOverride(t *testing.T) {
 	})
 }
 
+func TestAzurePipelinesEnvironmentVariables(t *testing.T) {
+	const (
+		testClientID            = "test-client-id"
+		testTenantID            = "test-tenant-id"
+		testServiceConnectionID = "test-service-connection-id"
+	)
+
+	t.Run("Azure Pipelines environment variables are read when LoginMethod is AzurePipelinesLogin", func(t *testing.T) {
+		t.Setenv(env.AzureSubscriptionClientID, testClientID)
+		t.Setenv(env.AzureSubscriptionTenantID, testTenantID)
+		t.Setenv(env.AzureSubscriptionServiceConnectionID, testServiceConnectionID)
+
+		o := Options{LoginMethod: AzurePipelinesLogin}
+		o.UpdateFromEnv()
+
+		if o.ClientID != testClientID {
+			t.Fatalf("expected ClientID to be '%s', got '%s'", testClientID, o.ClientID)
+		}
+		if o.TenantID != testTenantID {
+			t.Fatalf("expected TenantID to be '%s', got '%s'", testTenantID, o.TenantID)
+		}
+		if o.AzurePipelinesServiceConnectionID != testServiceConnectionID {
+			t.Fatalf("expected AzurePipelinesServiceConnectionID to be '%s', got '%s'", testServiceConnectionID, o.AzurePipelinesServiceConnectionID)
+		}
+	})
+
+	t.Run("Azure Pipelines environment variables are not read for other login methods", func(t *testing.T) {
+		t.Setenv(env.AzureSubscriptionClientID, testClientID)
+		t.Setenv(env.AzureSubscriptionTenantID, testTenantID)
+		t.Setenv(env.AzureSubscriptionServiceConnectionID, testServiceConnectionID)
+
+		o := Options{LoginMethod: DeviceCodeLogin}
+		o.UpdateFromEnv()
+
+		if o.ClientID == testClientID {
+			t.Fatalf("Azure Pipelines ClientID should not be read for non-AzurePipelines login method")
+		}
+		if o.TenantID == testTenantID {
+			t.Fatalf("Azure Pipelines TenantID should not be read for non-AzurePipelines login method")
+		}
+		if o.AzurePipelinesServiceConnectionID == testServiceConnectionID {
+			t.Fatalf("Azure Pipelines ServiceConnectionID should not be read for non-AzurePipelines login method")
+		}
+	})
+
+	t.Run("Command-line flags take precedence over Azure Pipelines environment variables", func(t *testing.T) {
+		t.Setenv(env.AzureSubscriptionClientID, "env-client-id")
+		t.Setenv(env.AzureSubscriptionTenantID, "env-tenant-id")
+		t.Setenv(env.AzureSubscriptionServiceConnectionID, "env-service-connection-id")
+
+		o := Options{
+			LoginMethod:                       AzurePipelinesLogin,
+			ClientID:                          "flag-client-id",
+			TenantID:                          "flag-tenant-id",
+			AzurePipelinesServiceConnectionID: "flag-service-connection-id",
+		}
+		o.UpdateFromEnv()
+
+		// Command-line flags should take precedence (not be overwritten)
+		if o.ClientID != "flag-client-id" {
+			t.Fatalf("expected ClientID to remain 'flag-client-id', got '%s'", o.ClientID)
+		}
+		if o.TenantID != "flag-tenant-id" {
+			t.Fatalf("expected TenantID to remain 'flag-tenant-id', got '%s'", o.TenantID)
+		}
+		if o.AzurePipelinesServiceConnectionID != "flag-service-connection-id" {
+			t.Fatalf("expected AzurePipelinesServiceConnectionID to remain 'flag-service-connection-id', got '%s'", o.AzurePipelinesServiceConnectionID)
+		}
+	})
+
+	t.Run("Azure Pipelines environment variables are not read when DisableEnvironmentOverride is true", func(t *testing.T) {
+		t.Setenv(env.AzureSubscriptionClientID, testClientID)
+		t.Setenv(env.AzureSubscriptionTenantID, testTenantID)
+		t.Setenv(env.AzureSubscriptionServiceConnectionID, testServiceConnectionID)
+
+		o := Options{
+			LoginMethod:                 AzurePipelinesLogin,
+			DisableEnvironmentOverride:  true,
+		}
+		o.UpdateFromEnv()
+
+		if o.ClientID != "" {
+			t.Fatalf("expected ClientID to be empty when DisableEnvironmentOverride is true, got '%s'", o.ClientID)
+		}
+		if o.TenantID != "" {
+			t.Fatalf("expected TenantID to be empty when DisableEnvironmentOverride is true, got '%s'", o.TenantID)
+		}
+		if o.AzurePipelinesServiceConnectionID != "" {
+			t.Fatalf("expected AzurePipelinesServiceConnectionID to be empty when DisableEnvironmentOverride is true, got '%s'", o.AzurePipelinesServiceConnectionID)
+		}
+	})
+
+	t.Run("Azure Pipelines environment variables set LoginMethod from env", func(t *testing.T) {
+		t.Setenv(env.LoginMethod, AzurePipelinesLogin)
+		t.Setenv(env.AzureSubscriptionClientID, testClientID)
+		t.Setenv(env.AzureSubscriptionTenantID, testTenantID)
+		t.Setenv(env.AzureSubscriptionServiceConnectionID, testServiceConnectionID)
+
+		o := Options{}
+		o.UpdateFromEnv()
+
+		if o.LoginMethod != AzurePipelinesLogin {
+			t.Fatalf("expected LoginMethod to be '%s', got '%s'", AzurePipelinesLogin, o.LoginMethod)
+		}
+		if o.ClientID != testClientID {
+			t.Fatalf("expected ClientID to be '%s', got '%s'", testClientID, o.ClientID)
+		}
+		if o.TenantID != testTenantID {
+			t.Fatalf("expected TenantID to be '%s', got '%s'", testTenantID, o.TenantID)
+		}
+		if o.AzurePipelinesServiceConnectionID != testServiceConnectionID {
+			t.Fatalf("expected AzurePipelinesServiceConnectionID to be '%s', got '%s'", testServiceConnectionID, o.AzurePipelinesServiceConnectionID)
+		}
+	})
+
+	t.Run("Azure Pipelines environment variables partially set", func(t *testing.T) {
+		t.Setenv(env.AzureSubscriptionClientID, testClientID)
+		// Only set ClientID, not TenantID or ServiceConnectionID
+
+		o := Options{LoginMethod: AzurePipelinesLogin}
+		o.UpdateFromEnv()
+
+		if o.ClientID != testClientID {
+			t.Fatalf("expected ClientID to be '%s', got '%s'", testClientID, o.ClientID)
+		}
+		if o.TenantID != "" {
+			t.Fatalf("expected TenantID to be empty, got '%s'", o.TenantID)
+		}
+		if o.AzurePipelinesServiceConnectionID != "" {
+			t.Fatalf("expected AzurePipelinesServiceConnectionID to be empty, got '%s'", o.AzurePipelinesServiceConnectionID)
+		}
+	})
+}
+
 func TestGetCloudConfiguration(t *testing.T) {
 	testCases := []struct {
 		name        string