Skip to content

Commit

Permalink
Add PoP token support to interactive+spn get-token/convert-kubeconfig…
Browse files Browse the repository at this point in the history
… flows (#319)

- added [PoP token support]((https://learn.microsoft.com/en-us/entra/msal/dotnet/advanced/proof-of-possession-tokens)) to `interactive` and `spn` login mode
---------

Co-authored-by: rharpavat <rharpavat@Chimaera>
Co-authored-by: Julien Stroheker <juliens@microsoft.com>
  • Loading branch information
3 people authored Sep 6, 2023
1 parent 9791038 commit c4cf27c
Show file tree
Hide file tree
Showing 41 changed files with 3,419 additions and 340 deletions.
1 change: 1 addition & 0 deletions docs/book/src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
- [Workload Identity](./concepts/login-modes/workloadidentity.md)
- [Resource Owner Password Credential](./concepts/login-modes/ropc.md)
- [Using kubelogin with AKS](./concepts/aks.md)
- [Using kubelogin to get Proof-of-Possession (PoP) tokens for Azure Arc](./concepts/azure-arc.md)
- [Command-Line Tool](./cli-reference.md)
- [convert-kubeconfig](./cli/convert-kubeconfig.md)
- [get-token](./cli/get-token.md)
Expand Down
2 changes: 2 additions & 0 deletions docs/book/src/cli/convert-kubeconfig.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ Flags:
--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, 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-enabled set to true to request a proof-of-possession/PoP token, or false to request a regular bearer token. Only works with interactive and spn login modes. --pop-claims must be provided if --pop-enabled is true
--pop-claims claims to include when requesting a PoP token, formatted as a comma-separated string of key=value pairs. Must include the u-claim, `u=ARM_ID` containing the ARM ID of the cluster (host). --pop-enabled must be set to true if --pop-claims are provided
--server-id string AAD server application ID
-t, --tenant-id string AAD tenant ID. It may be specified in AZURE_TENANT_ID environment variable
--token-cache-dir string directory to cache token (default "${HOME}/.kube/cache/kubelogin/")
Expand Down
2 changes: 2 additions & 0 deletions docs/book/src/cli/get-token.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ ECRET environment variable
-l, --login string Login method. Supported methods: devicecode, interactive, spn, ropc, msi, azurecli, workloadidentity. It may be specified in A
AD_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-enabled set to true to request a proof-of-possession/PoP token, or false to request a regular bearer token. Only works with interactive and spn login modes. --pop-claims must be provided if --pop-enabled is true
--pop-claims claims to include when requesting a PoP token, formatted as a comma-separated string of key=value pairs. Must include the u-claim, `u=ARM_ID` containing the ARM ID of the cluster (host). --pop-enabled must be set to true if --pop-claims are provided
--server-id string AAD server application ID
-t, --tenant-id string AAD tenant ID. It may be specified in AZURE_TENANT_ID environment variable
--token-cache-dir string directory to cache token (default "${HOME}/.kube/cache/kubelogin/")
Expand Down
10 changes: 10 additions & 0 deletions docs/book/src/concepts/azure-arc.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Using kubelogin with Azure Arc

kubelogin can be used to authenticate with Azure Arc-enabled clusters by requesting a [proof-of-possession (PoP) token](https://learn.microsoft.com/en-us/entra/msal/dotnet/advanced/proof-of-possession-tokens). This can be done by providing both of the following flags together:

1. `--pop-enabled`: indicates that `kubelogin` should request a PoP token instead of a regular bearer token
2. `--pop-claims`: is a comma-separated list of `key=value` claims to include in the PoP token. At minimum, this must include the u-claim as `u=ARM_ID_OF_CLUSTER`, which specifies the host that the requested token should allow access on.

These flags can be provided to either `kubelogin get-token` directly to get a PoP token, or to `kubelogin convert-kubeconfig` for `kubectl` to request the token internally.

PoP token requests only work with `interactive` and `spn` login modes; these flags will be ignored if provided for other login modes.
10 changes: 10 additions & 0 deletions docs/book/src/concepts/login-modes/interactive.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ In this login mode, the access token will be cached at `${HOME}/.kube/cache/kube

## Usage Examples

### Bearer token with interactive flow
```sh
export KUBECONFIG=/path/to/kubeconfig

Expand All @@ -16,6 +17,15 @@ kubelogin convert-kubeconfig -l interactive
kubectl get nodes
```

### Proof-of-possession (PoP) token with interactive flow
```sh
export KUBECONFIG=/path/to/kubeconfig

kubelogin convert-kubeconfig -l interactive --pop-enabled --pop-claims "u=/ARM/ID/OF/CLUSTER"

kubectl get nodes
```

## References

- https://learn.microsoft.com/en-us/python/api/azure-identity/azure.identity.interactivebrowsercredential?view=azure-python
12 changes: 12 additions & 0 deletions docs/book/src/concepts/login-modes/sp.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,18 @@ export AZURE_CLIENT_CERTIFICATE_PASSWORD=<pfx password>
kubectl get nodes
```

### Proof-of-possession (PoP) token with client secret from environment variables
```sh
export KUBECONFIG=/path/to/kubeconfig

kubelogin convert-kubeconfig -l spn --pop-enabled --pop-claims "u=/ARM/ID/OF/CLUSTER"

export AAD_SERVICE_PRINCIPAL_CLIENT_ID=<spn client id>
export AAD_SERVICE_PRINCIPAL_CLIENT_SECRET=<spn secret>

kubectl get nodes
```

## Restrictions

- on AKS, it will only work with managed AAD
Expand Down
7 changes: 4 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,11 @@ require (
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.0
github.com/Azure/go-autorest/autorest v0.11.29
github.com/Azure/go-autorest/autorest/adal v0.9.23
github.com/AzureAD/microsoft-authentication-library-for-go v1.0.0
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.0
github.com/golang-jwt/jwt/v4 v4.5.0
github.com/golang-jwt/jwt/v5 v5.0.0
github.com/golang/mock v1.6.0
github.com/google/go-cmp v0.5.9
github.com/google/uuid v1.3.0
github.com/spf13/cobra v1.7.0
github.com/spf13/pflag v1.0.5
Expand Down Expand Up @@ -36,11 +39,9 @@ require (
github.com/go-openapi/jsonreference v0.20.1 // indirect
github.com/go-openapi/swag v0.22.3 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/google/btree v1.1.2 // indirect
github.com/google/gnostic v0.6.9 // indirect
github.com/google/go-cmp v0.5.9 // indirect
github.com/google/gofuzz v1.2.0 // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect
Expand Down
6 changes: 4 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ github.com/Azure/go-autorest/logger v0.2.1 h1:IG7i4p/mDa2Ce4TRyAO8IHnVhAVF3RFU+Z
github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8=
github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo=
github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU=
github.com/AzureAD/microsoft-authentication-library-for-go v1.0.0 h1:OBhqkivkhkMqLPymWEppkm7vgPQY2XsHoEkaMQ0AdZY=
github.com/AzureAD/microsoft-authentication-library-for-go v1.0.0/go.mod h1:kgDmCTgBzIEPFElEF+FK0SdjAor06dRq2Go927dnQ6o=
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.0 h1:hVeq+yCyUi+MsoO/CU95yqCIcdzra5ovzk8Q2BBpV2M=
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.0/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
Expand Down Expand Up @@ -74,6 +74,8 @@ github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69
github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang-jwt/jwt/v5 v5.0.0 h1:1n1XNM9hk7O9mnQoNBGolZvzebBQ7p93ULHRc28XJUE=
github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
Expand Down
65 changes: 63 additions & 2 deletions pkg/converter/convert.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ const (
argAuthorityHost = "--authority-host"
argFederatedTokenFile = "--federated-token-file"
argTokenCacheDir = "--token-cache-dir"
argIsPoPTokenEnabled = "--pop-enabled"
argPoPTokenClaims = "--pop-claims"

flagAzureConfigDir = "azure-config-dir"
flagClientID = "client-id"
Expand All @@ -51,6 +53,8 @@ const (
flagAuthorityHost = "authority-host"
flagFederatedTokenFile = "federated-token-file"
flagTokenCacheDir = "token-cache-dir"
flagIsPoPTokenEnabled = "pop-enabled"
flagPoPTokenClaims = "pop-claims"

execName = "kubelogin"
getTokenCommand = "get-token"
Expand All @@ -64,7 +68,16 @@ To learn more, please go to https://azure.github.io/kubelogin/
azureConfigDir = "AZURE_CONFIG_DIR"
)

func getArgValues(o Options, authInfo *api.AuthInfo) (argServerIDVal, argClientIDVal, argEnvironmentVal, argTenantIDVal, argTokenCacheDirVal string, argIsLegacyConfigModeVal bool) {
func getArgValues(o Options, authInfo *api.AuthInfo) (
argServerIDVal,
argClientIDVal,
argEnvironmentVal,
argTenantIDVal,
argTokenCacheDirVal,
argPoPTokenClaimsVal string,
argIsLegacyConfigModeVal,
argIsPoPTokenEnabledVal bool,
) {
if authInfo == nil {
return
}
Expand Down Expand Up @@ -129,6 +142,20 @@ func getArgValues(o Options, authInfo *api.AuthInfo) (argServerIDVal, argClientI
argTokenCacheDirVal = getExecArg(authInfo, argTokenCacheDir)
}

if o.isSet(flagIsPoPTokenEnabled) {
argIsPoPTokenEnabledVal = o.TokenOptions.IsPoPTokenEnabled
} else {
if found := getExecBoolArg(authInfo, argIsPoPTokenEnabled); found {
argIsPoPTokenEnabledVal = true
}
}

if o.isSet(flagPoPTokenClaims) {
argPoPTokenClaimsVal = o.TokenOptions.PoPTokenClaims
} else {
argPoPTokenClaimsVal = getExecArg(authInfo, argPoPTokenClaims)
}

return
}

Expand Down Expand Up @@ -198,7 +225,7 @@ func Convert(o Options, pathOptions *clientcmd.PathOptions) error {

klog.V(5).Info("converting...")

argServerIDVal, argClientIDVal, argEnvironmentVal, argTenantIDVal, argTokenCacheDirVal, isLegacyConfigMode := getArgValues(o, authInfo)
argServerIDVal, argClientIDVal, argEnvironmentVal, argTenantIDVal, argTokenCacheDirVal, argPoPTokenClaimsVal, isLegacyConfigMode, isPoPTokenEnabled := getArgValues(o, authInfo)
exec := &api.ExecConfig{
Command: execName,
Args: []string{
Expand Down Expand Up @@ -282,6 +309,12 @@ func Convert(o Options, pathOptions *clientcmd.PathOptions) error {
exec.Args = append(exec.Args, argEnvironment, argEnvironmentVal)
}

// PoP token flags are optional but must be provided together
exec.Args, err = validatePoPClaims(exec.Args, isPoPTokenEnabled, argPoPTokenClaims, argPoPTokenClaimsVal)
if err != nil {
return err
}

case token.ServicePrincipalLogin:

if argClientIDVal == "" {
Expand Down Expand Up @@ -317,6 +350,12 @@ func Convert(o Options, pathOptions *clientcmd.PathOptions) error {
exec.Args = append(exec.Args, argIsLegacy)
}

// PoP token flags are optional but must be provided together
exec.Args, err = validatePoPClaims(exec.Args, isPoPTokenEnabled, argPoPTokenClaims, argPoPTokenClaimsVal)
if err != nil {
return err
}

case token.MSILogin:

if o.isSet(flagClientID) {
Expand Down Expand Up @@ -420,3 +459,25 @@ func getExecBoolArg(authInfoPtr *api.AuthInfo, someArg string) bool {
}
return false
}

// If enabling PoP token support, users must provide both "--pop-enabled" and "--pop-claims" flags together.
// If either is provided without the other, validation should throw an error, otherwise the get-token command
// will fail under the hood.
func validatePoPClaims(args []string, isPopTokenEnabled bool, popTokenClaimsFlag, popTokenClaimsVal string) ([]string, error) {
if isPopTokenEnabled && popTokenClaimsVal == "" {
// pop-enabled and pop-claims must be provided together
return args, fmt.Errorf("%s is required when specifying %s", argPoPTokenClaims, argIsPoPTokenEnabled)
}

if popTokenClaimsVal != "" && !isPopTokenEnabled {
// pop-enabled and pop-claims must be provided together
return args, fmt.Errorf("%s is required when specifying %s", argIsPoPTokenEnabled, argPoPTokenClaims)
}

if isPopTokenEnabled && popTokenClaimsVal != "" {
args = append(args, argIsPoPTokenEnabled)
args = append(args, popTokenClaimsFlag, popTokenClaimsVal)
}

return args, nil
}
Loading

0 comments on commit c4cf27c

Please sign in to comment.