Repository: Azure/kubelogin

Issue:
Add argument for listening address or redirect uri
### Problem
At the moment, it is very difficult to authenticate via the interactive login mode within a docker container. (Device-code is not an allowed mode for our use case unfortunately.) Due to the fact that the redirect port is constantly changing, it is difficult to orchestrate a docker compose to expose the right port given that you need to know it in advance.

### Suggestion
Using some inspiration from [int128/kubelogin](https://github.com/int128/kubelogin/blob/master/docs/usage.md#authorization-code-flow), add the following argument to customise the authorisation flow:
- --listen-address=127.0.0.1:12345

Discussion:
@mwdomino: I'm also very interested in this. I'd be happy to raise a PR myself if a maintainer could point me in the right direction as to where the change would be made.

Using my LSP I've landed in a bunch of interfaces in the `azidentity` package and got lost :)@weinong: https://github.com/Azure/kubelogin/pull/661@weinong: you can try it out at https://github.com/weinong/kubelogin/releases/tag/v0.2.8@weinong: @TazBruce I tried it in docker container. The interactive mode will kick off a browser which is unavailable in the container. How did you solve that?@TazBruce: @weinong Thanks for looking into this! I've just had a go at trying the command and I can't seem to find it in 0.2.8? Looking at the [changelog](https://github.com/Azure/kubelogin/compare/v0.2.7...v0.2.8), I can't seem to find the changes

```
❯ kubelogin --version
kubelogin version
git hash: v0.2.8/d7f1c16c95cc0a1a3beb056374def7b744a38b3a
Go version: go1.23.7
Build time: 2025-04-25T17:17:57Z
Platform: darwin/arm64
❯ kubelogin convert-kubeconfig --redirect-url localhost:8181
Error: unknown flag: --redirect-url
❯ kubelogin convert-kubeconfig -h
convert kubeconfig to use exec auth module

Usage:
  kubelogin convert-kubeconfig [flags]

Flags:
      --authority-host string                Workload Identity authority host. It may be specified in AZURE_AUTHORITY_HOST environment variable
      --azure-config-dir string              Azure CLI config path
      --cache-dir string                     directory to cache authentication record (default "/Users/941028/.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-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
      --context string                       The name of the kubeconfig context to use
      --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
  -e, --environment string                   Azure environment name (default "AzurePublicCloud")
      --federated-token-file string          Workload Identity federated token file. It may be specified in AZURE_FEDERATED_TOKEN_FILE environment variable
  -h, --help                                 help for convert-kubeconfig
      --identity-resource-id string          Managed Identity resource id.
      --kubeconfig string                    Path to the kubeconfig file to use for CLI requests.
      --legacy                               set to true to get token with 'spn:' prefix in audience claim
  -l, --login string                         Login method. Supported methods: devicecode, interactive, spn, ropc, msi, azurecli, azd, workloadidentity. It may be specified in AAD_LOGIN_METHOD environment variable (default "devicecode")
      --password string                      password for ropc login flow. It may be specified in AAD_USER_PRINCIPAL_PASSWORD or AZURE_PASSWORD environment variable
      --pop-claims key=val,key2=val2         contains a comma-separated list of claims to attach to the pop token in the format key=val,key2=val2. At minimum, specify the ARM ID of the cluster as `u=ARM_ID`
      --pop-enabled                          set to true to use a PoP token for authentication or false to use a regular bearer token
      --server-id string                     AAD server application ID
  -t, --tenant-id string                     AAD tenant ID. It may be specified in AZURE_TENANT_ID environment variable
      --timeout duration                     Timeout duration for Azure CLI token requests. It may be specified in AZURE_CLI_TIMEOUT environment variable (default 1m0s)
      --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

Global Flags:
      --logtostderr   log to standard error instead of files (default true)
  -v, --v Level       number for the log level verbosity
```

In terms of how I use it, it's slightly jank but I overwrite the xdg-open binary with my own shell script that console logs the link being opened. For instance:
```
# Dockerfile
COPY ./open-browser.sh /usr/local/bin/xdg-open
RUN chmod +x /usr/local/bin/xdg-open

#docker-compose.yml
environment:
- BROWSER=xdg-open

# open-browder.sh
#!/bin/bash
echo "Open url in browser: $1" >&2
```


@weinong: @TazBruce I published that in my fork's release. you can download it manually to try it out. i plan to address other issues before I cut the new release here.
https://github.com/weinong/kubelogin/releases/tag/v0.2.8@TazBruce: @weinong  Whoops my bad! Saw the docs were already updated and got excited 😆  I've had a go at pulling down your release and it appears that no matter the url I provide, the listening address always remains as localhost. Despite having 8181 exposed in my docker compose, no luck reaching it. To be able to reach an address from a docker compose, I believe the local address needs to be 0.0.0.0?

docker compose
```
services:
  dev:
    ports:
      - "127.0.0.1:8181:8181" # Port for kubelogin
```

kube config
```
- name: clusterUser_blah
  user:
    exec:
      apiVersion: client.authentication.k8s.io/v1beta1
      args:
      - get-token
      - --environment
      - xxx
      - --server-id
      - xxxx
      - --client-id
      - xxx
      - --tenant-id
      - xxxx
      - --redirect-url
      - http://0.0.0.0:8181
      - --login
      - interactive
      command: kubelogin
      env: null
      interactiveMode: IfAvailable
      provideClusterInfo: false
```

command
```
p941028@50d5ca1e5efd:/src$ kubectl get ns
Open url in browser: https://login.microsoftonline.com/xxxx/oauth2/v2.0/authorize?client_id=xxxx&code_challenge=xxxx&code_challenge_method=S256&prompt=select_account&redirect_uri=http%3A%2F%2Flocalhost%3A8181&response_type=code&scope=xxx&state=xxx
```

networking within container
```
netstat -ltp
(Not all processes could be identified, non-owned process info
 will not be shown, you would have to be root to see it all.)
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name
tcp        0      0 127.0.0.11:39225        0.0.0.0:*               LISTEN      -
tcp        0      0 localhost:8181          0.0.0.0:*               LISTEN      1394/rosetta
```

@weinong: I think it's a requirement in msal.
https://github.com/AzureAD/microsoft-authentication-library-for-go/blob/b4b8bfc9569042572ccb82b648ea509075fadb74/apps/public/public.go#L594@weinong: Yeah, this code confirms that it only uses the port from redirect url to run the local server
https://github.com/AzureAD/microsoft-authentication-library-for-go/blob/b4b8bfc9569042572ccb82b648ea509075fadb74/apps/public/public.go#L721-L729@TazBruce: Finally had some time to have a go at getting this to work. It's slightly unintuitive, but we got your implementation working by putting a proxy in front to redirect requests to the container. 

![Image](https://github.com/user-attachments/assets/3cae0a33-4a10-4224-99e8-b9aec391e9fa)

As there's no way to overwrite the listening address, we've got a seperate container listening on the original port, and redirecting it to the dev container. With your fork, it works perfectly!

Is there anything else needed to merge this across? @weinong?
@weinong: @TazBruce https://github.com/Azure/kubelogin/releases/tag/v0.2.9 is released

Solution patch:
diff --git a/docs/book/src/cli/convert-kubeconfig.md b/docs/book/src/cli/convert-kubeconfig.md
index 252b16f..d3e9220 100644
--- a/docs/book/src/cli/convert-kubeconfig.md
+++ b/docs/book/src/cli/convert-kubeconfig.md
@@ -34,6 +34,7 @@ Flags:
       --password string                      password for ropc login flow. It may be specified in AAD_USER_PRINCIPAL_PASSWORD or AZURE_PASSWORD environment variable
       --pop-claims key=val,key2=val2         contains a comma-separated list of claims to attach to the pop token in the format key=val,key2=val2. At minimum, specify the ARM ID of the cluster as `u=ARM_ID`
       --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
       --timeout duration                     Timeout duration for Azure CLI token requests. It may be specified in AZURE_CLI_TIMEOUT environment variable (default 30s)
diff --git a/docs/book/src/cli/get-token.md b/docs/book/src/cli/get-token.md
index 7a5e6e1..d26341c 100644
--- a/docs/book/src/cli/get-token.md
+++ b/docs/book/src/cli/get-token.md
@@ -29,6 +29,7 @@ Flags:
       --password string                      password for ropc login flow. It may be specified in AAD_USER_PRINCIPAL_PASSWORD or AZURE_PASSWORD environment variable
       --pop-claims key=val,key2=val2         contains a comma-separated list of claims to attach to the pop token in the format key=val,key2=val2. At minimum, specify the ARM ID of the cluster as `u=ARM_ID`
       --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
       --timeout duration                     Timeout duration for Azure CLI token requests. It may be specified in AZURE_CLI_TIMEOUT environment variable (default 30s)
diff --git a/docs/book/src/concepts/login-modes/interactive.md b/docs/book/src/concepts/login-modes/interactive.md
index c50d193..cc49f27 100644
--- a/docs/book/src/concepts/login-modes/interactive.md
+++ b/docs/book/src/concepts/login-modes/interactive.md
@@ -1,12 +1,14 @@
 # Web Browser Interactive
 
 This login mode will automatically open a browser to login the user. 
-Once authenticated, the browser will redirect back to a local web server with the credentials. 
+Once authenticated, the browser will redirect back to a local web server with access token.
+The redirect URL can be set via `--redirect-url`.
 This login mode complies with Conditional Access policy.
 
 ## Usage Examples
 
 ### Bearer token with interactive flow
+
 ```sh
 export KUBECONFIG=/path/to/kubeconfig
 
@@ -15,7 +17,19 @@ kubelogin convert-kubeconfig -l interactive
 kubectl get nodes
 ```
 
+### Specifying Redirect URL
+
+```sh
+export KUBECONFIG=/path/to/kubeconfig
+
+kubelogin convert-kubeconfig -l interactive --redirect-url http://localhost:8080
+
+kubectl get nodes
+```
+
+
 ### Proof-of-possession (PoP) token with interactive flow
+
 ```sh
 export KUBECONFIG=/path/to/kubeconfig
 
diff --git a/pkg/internal/converter/convert.go b/pkg/internal/converter/convert.go
index ac792c7..a2fabed 100644
--- a/pkg/internal/converter/convert.go
+++ b/pkg/internal/converter/convert.go
@@ -37,6 +37,7 @@ const (
 	argIsPoPTokenEnabled          = "--pop-enabled"
 	argPoPTokenClaims             = "--pop-claims"
 	argDisableEnvironmentOverride = "--disable-environment-override"
+	argRedirectURL                = "--redirect-url"
 
 	flagAzureConfigDir             = "azure-config-dir"
 	flagClientID                   = "client-id"
@@ -59,6 +60,7 @@ const (
 	flagIsPoPTokenEnabled          = "pop-enabled"
 	flagPoPTokenClaims             = "pop-claims"
 	flagDisableEnvironmentOverride = "disable-environment-override"
+	flagRedirectURL                = "redirect-url"
 
 	execName        = "kubelogin"
 	getTokenCommand = "get-token"
@@ -78,7 +80,8 @@ func getArgValues(o Options, authInfo *api.AuthInfo) (
 	argEnvironmentVal,
 	argTenantIDVal,
 	argAuthRecordCacheDirVal,
-	argPoPTokenClaimsVal string,
+	argPoPTokenClaimsVal,
+	argRedirectURLVal string,
 	argIsLegacyConfigModeVal,
 	argIsPoPTokenEnabledVal bool,
 ) {
@@ -164,6 +167,12 @@ func getArgValues(o Options, authInfo *api.AuthInfo) (
 		argPoPTokenClaimsVal = getExecArg(authInfo, argPoPTokenClaims)
 	}
 
+	if o.isSet(flagRedirectURL) {
+		argRedirectURLVal = o.TokenOptions.RedirectURL
+	} else {
+		argRedirectURLVal = getExecArg(authInfo, argRedirectURL)
+	}
+
 	return
 }
 
@@ -233,7 +242,15 @@ func Convert(o Options, pathOptions *clientcmd.PathOptions) error {
 
 		klog.V(5).Info("converting...")
 
-		argServerIDVal, argClientIDVal, argEnvironmentVal, argTenantIDVal, argAuthRecordCacheDirVal, argPoPTokenClaimsVal, isLegacyConfigMode, isPoPTokenEnabled := getArgValues(o, authInfo)
+		argServerIDVal,
+			argClientIDVal,
+			argEnvironmentVal,
+			argTenantIDVal,
+			argAuthRecordCacheDirVal,
+			argPoPTokenClaimsVal,
+			argRedirectURLVal,
+			isLegacyConfigMode,
+			isPoPTokenEnabled := getArgValues(o, authInfo)
 		exec := &api.ExecConfig{
 			Command: execName,
 			Args: []string{
@@ -328,6 +345,10 @@ func Convert(o Options, pathOptions *clientcmd.PathOptions) error {
 				return err
 			}
 
+			if argRedirectURLVal != "" {
+				exec.Args = append(exec.Args, argRedirectURL, argRedirectURLVal)
+			}
+
 		case token.ServicePrincipalLogin:
 
 			if argClientIDVal == "" {
diff --git a/pkg/internal/token/interactivebrowsercredential.go b/pkg/internal/token/interactivebrowsercredential.go
index 13c2705..cbe087d 100644
--- a/pkg/internal/token/interactivebrowsercredential.go
+++ b/pkg/internal/token/interactivebrowsercredential.go
@@ -42,6 +42,7 @@ func newInteractiveBrowserCredential(opts *Options, record azidentity.Authentica
 		ClientID:                 opts.ClientID,
 		TenantID:                 opts.TenantID,
 		DisableInstanceDiscovery: opts.DisableInstanceDiscovery,
+		RedirectURL:              opts.RedirectURL,
 	}
 
 	if opts.httpClient != nil {
diff --git a/pkg/internal/token/options.go b/pkg/internal/token/options.go
index b43157f..a8d7de2 100644
--- a/pkg/internal/token/options.go
+++ b/pkg/internal/token/options.go
@@ -41,6 +41,7 @@ type Options struct {
 	UsePersistentCache         bool
 	DisableInstanceDiscovery   bool
 	httpClient                 *http.Client
+	RedirectURL                string
 }
 
 const (
@@ -119,6 +120,7 @@ func (o *Options) AddFlags(fs *pflag.FlagSet) {
 	fs.StringVar(&o.PoPTokenClaims, "pop-claims", o.PoPTokenClaims, "contains a comma-separated list of claims to attach to the pop token in the format `key=val,key2=val2`. At minimum, specify the ARM ID of the cluster as `u=ARM_ID`")
 	fs.BoolVar(&o.DisableEnvironmentOverride, "disable-environment-override", o.DisableEnvironmentOverride, "Enable or disable the use of env-variables. Default false")
 	fs.BoolVar(&o.DisableInstanceDiscovery, "disable-instance-discovery", o.DisableInstanceDiscovery, "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")
+	fs.StringVar(&o.RedirectURL, "redirect-url", o.RedirectURL, "The URL Microsoft Entra ID will redirect to with the access token. This is only used for interactive login. This is an optional parameter.")
 }
 
 func (o *Options) Validate() error {
@@ -275,19 +277,23 @@ func (o *Options) GetCloudConfiguration() cloud.Configuration {
 
 func (o *Options) ToString() string {
 	azureConfigDir := os.Getenv("AZURE_CONFIG_DIR")
-	return fmt.Sprintf("Login Method: %s, Environment: %s, TenantID: %s, ServerID: %s, ClientID: %s, IsLegacy: %t, msiResourceID: %s, Timeout: %v, authRecordCacheDir: %s, tokenauthRecordFile: %s, AZURE_CONFIG_DIR: %s",
-		o.LoginMethod,
-		o.Environment,
-		o.TenantID,
-		o.ServerID,
-		o.ClientID,
-		o.IsLegacy,
-		o.IdentityResourceID,
-		o.Timeout,
-		o.AuthRecordCacheDir,
-		o.authRecordCacheFile,
-		azureConfigDir,
-	)
+
+	parts := []string{
+		fmt.Sprintf("Login Method: %s", o.LoginMethod),
+		fmt.Sprintf("Environment: %s", o.Environment),
+		fmt.Sprintf("TenantID: %s", o.TenantID),
+		fmt.Sprintf("ServerID: %s", o.ServerID),
+		fmt.Sprintf("ClientID: %s", o.ClientID),
+		fmt.Sprintf("IsLegacy: %t", o.IsLegacy),
+		fmt.Sprintf("msiResourceID: %s", o.IdentityResourceID),
+		fmt.Sprintf("Timeout: %v", o.Timeout),
+		fmt.Sprintf("authRecordCacheDir: %s", o.AuthRecordCacheDir),
+		fmt.Sprintf("tokenauthRecordFile: %s", o.authRecordCacheFile),
+		fmt.Sprintf("AZURE_CONFIG_DIR: %s", azureConfigDir),
+		fmt.Sprintf("RedirectURL: %s", o.RedirectURL),
+	}
+
+	return strings.Join(parts, ", ")
 }
 
 func getAuthenticationRecordFileName(o *Options) string {



Test patch:
diff --git a/pkg/internal/converter/convert_test.go b/pkg/internal/converter/convert_test.go
index 91dfb93..47a4a23 100644
--- a/pkg/internal/converter/convert_test.go
+++ b/pkg/internal/converter/convert_test.go
@@ -32,6 +32,7 @@ func TestConvert(t *testing.T) {
 		federatedTokenFile = "/tmp/file"
 		authRecordCacheDir = "/tmp/token_dir"
 		azureCLIDir        = "/tmp/foo"
+		redirectURL        = "http://localhost:8000"
 	)
 	testData := []struct {
 		name                string
@@ -1208,6 +1209,35 @@ func TestConvert(t *testing.T) {
 			},
 			command: execName,
 		},
+		{
+			name: "with exec format kubeconfig, convert from devicecode to interactive with redirect url override",
+			execArgItems: []string{
+				getTokenCommand,
+				argServerID, serverID,
+				argClientID, clientID,
+				argTenantID, tenantID,
+				argEnvironment, envName,
+				argLoginMethod, token.DeviceCodeLogin,
+			},
+			overrideFlags: map[string]string{
+				flagLoginMethod: token.InteractiveLogin,
+				flagServerID:    serverID,
+				flagClientID:    clientID,
+				flagTenantID:    tenantID,
+				flagEnvironment: envName,
+				flagRedirectURL: redirectURL,
+			},
+			expectedArgs: []string{
+				getTokenCommand,
+				argServerID, serverID,
+				argClientID, clientID,
+				argTenantID, tenantID,
+				argEnvironment, envName,
+				argLoginMethod, token.InteractiveLogin,
+				argRedirectURL, redirectURL,
+			},
+			command: execName,
+		},
 		{
 			name: "convert with context specified, auth info not specified by the context should not be changed",
 			authProviderConfig: map[string]string{