Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds Azure Developer CLI (azd) as a new login method #398

Merged
merged 6 commits into from
Feb 6, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/book/src/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@
- [non-interactive user principal login](./concepts/login-modes/ropc.md) using [Resource owner login flow](https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth-ropc)
- [non-interactive managed service identity login](./concepts/login-modes/msi.md)
- [non-interactive Azure CLI token login (AKS only)](./concepts/login-modes/azurecli.md)
- [non-interactive Azure Developer CLI token login (AKS only)](./concepts/login-modes/azd.md)
- [non-interactive workload identity login](./concepts/login-modes/workloadidentity.md)
1 change: 1 addition & 0 deletions docs/book/src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
- [Login Modes](./concepts/login-modes.md)
- [Device Code](./concepts/login-modes/devicecode.md)
- [Azure CLI](./concepts/login-modes/azurecli.md)
- [Azure Developer CLI](./concepts/login-modes/azd.md)
- [Web Browser Interactive](./concepts/login-modes/interactive.md)
- [Service Principal](./concepts/login-modes/sp.md)
- [Managed Service Identity](./concepts/login-modes/msi.md)
Expand Down
2 changes: 1 addition & 1 deletion docs/book/src/cli/convert-kubeconfig.md
wbreza marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ Flags:
--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, workloadidentity. It may be specified in AAD_LOGIN_METHOD environment variable (default "devicecode")
-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-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
Expand Down
2 changes: 1 addition & 1 deletion docs/book/src/cli/get-token.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ ECRET environment variable
-h, --help help for get-token
--identity-resource-id string Managed Identity resource id.
--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 A
-l, --login string Login method. Supported methods: devicecode, interactive, spn, ropc, msi, azurecli, azd, 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
Expand Down
27 changes: 27 additions & 0 deletions docs/book/src/concepts/login-modes/azd.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Azure Developer CLI (azd)

This login mode uses the already logged-in context performed by Azure Developer CLI to get the access token.
The token will be issued in the same Azure AD tenant as in `azd auth login`.

`kubelogin` will not cache any token since it's already managed by Azure Developer CLI.

> ### NOTE
>
> This login mode only works with managed AAD in AKS.

## Usage Examples

```sh
azd auth login

export KUBECONFIG=/path/to/kubeconfig

kubelogin convert-kubeconfig -l azd

kubectl get nodes
```

## References

- https://learn.microsoft.com/azure/developer/azure-developer-cli/overview
- https://github.com/azure/azure-dev
6 changes: 3 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ go 1.20

require (
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.1
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.4.0
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.5.1
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.2.1
Expand Down Expand Up @@ -59,12 +59,12 @@ require (
github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/xlab/treeprint v1.2.0 // indirect
go.starlark.net v0.0.0-20230525235612-a134d8f9ddca // indirect
golang.org/x/net v0.19.0 // indirect
golang.org/x/net v0.20.0 // indirect
golang.org/x/oauth2 v0.8.0 // indirect
golang.org/x/sync v0.2.0 // indirect
golang.org/x/sys v0.16.0 // indirect
Expand Down
14 changes: 7 additions & 7 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.1 h1:lGlwhPtrX6EVml1hO0ivjkUxsSyl4dsiw9qcA1k/3IQ=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.1/go.mod h1:RKUqNu35KJYcVG/fqTRqmuXJZYNhYkBrnC/hX7yGbTA=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.4.0 h1:BMAjVKJM0U/CYF27gA0ZMmXGkOcvfFtD0oHVZ1TIPRI=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.4.0/go.mod h1:1fXstnBMas5kzG+S3q8UoJcmyU6nUeunJcMDHcRYHhs=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.5.1 h1:sO0/P7g68FrryJzljemN+6GTssUXdANk6aJ7T1ZxnsQ=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.5.1/go.mod h1:h8hyGFDsU5HMivxiS2iYFZsgDbU9OnnJ163x5UGVKYo=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.1 h1:6oNBlSdi1QqM1PNW7FPA6xOGA5UNsXnkaYZz9vdPGhA=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.1/go.mod h1:s4kgfzA0covAXNicZHDMN58jExvcng2mC/DepXiF1EI=
github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs=
Expand Down Expand Up @@ -135,8 +135,8 @@ github.com/onsi/ginkgo/v2 v2.9.4 h1:xR7vG4IXt5RWx6FfIjyAtsoMAtnc3C/rFXBBd2AjZwE=
github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE=
github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI=
github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU=
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
Expand Down Expand Up @@ -200,8 +200,8 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo=
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.8.0 h1:6dkIjl3j3LtZ/O3sTgZTMsLKSftL/B8Zgq4huOIIUu8=
golang.org/x/oauth2 v0.8.0/go.mod h1:yr7u4HXZRm1R1kBWqr/xKNqewf0plRYoB7sla+BCIXE=
Expand All @@ -220,10 +220,10 @@ golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
Expand Down
5 changes: 5 additions & 0 deletions pkg/internal/converter/convert.go
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,11 @@ func Convert(o Options, pathOptions *clientcmd.PathOptions) error {
}

switch o.TokenOptions.LoginMethod {
case token.AzureDeveloperCLILogin:
wbreza marked this conversation as resolved.
Show resolved Hide resolved
if o.isSet(flagTenantID) {
exec.Args = append(exec.Args, argTenantID, o.TokenOptions.TenantID)
}

case token.AzureCLILogin:

if o.azureConfigDir != "" {
Expand Down
87 changes: 87 additions & 0 deletions pkg/internal/token/azuredevelopercli.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package token

import (
"context"
"encoding/json"
"errors"
"fmt"
"strconv"
"time"

"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
"github.com/Azure/go-autorest/autorest/adal"
)

type AzureDeveloperCLIToken struct {
resourceID string
tenantID string
cred *azidentity.AzureDeveloperCLICredential
timeout time.Duration
}

// newAzureDeveloperCLIToken returns a TokenProvider that will fetch a token for the user currently logged into the Azure Developer CLI.
// Required arguments the resourceID (which is used as the scope) and an optional tenantID.
func newAzureDeveloperCLIToken(resourceID string, tenantID string, timeout time.Duration) (TokenProvider, error) {
if resourceID == "" {
return nil, errors.New("resourceID cannot be empty")
}

if timeout <= 0 {
timeout = defaultTimeout
}

// Request a new Azure Developer CLI token provider
cred, err := azidentity.NewAzureDeveloperCLICredential(&azidentity.AzureDeveloperCLICredentialOptions{
TenantID: tenantID,
})
if err != nil {
return nil, fmt.Errorf("unable to create credential. Received: %v", err)
}

return &AzureDeveloperCLIToken{
resourceID: resourceID,
tenantID: tenantID,
cred: cred,
timeout: timeout,
}, nil
}

// Token fetches an azcore.AccessToken from the Azure Developer CLI SDK and converts it to an adal.Token for use with kubelogin.
func (p *AzureDeveloperCLIToken) Token(ctx context.Context) (adal.Token, error) {
emptyToken := adal.Token{}

if p.cred == nil {
return emptyToken, errors.New("credential is nil. Create new instance with newAzureDeveloperCLIToken function")
}

ctx, cancel := context.WithTimeout(ctx, p.timeout)
defer cancel()

policyOptions := policy.TokenRequestOptions{
TenantID: p.tenantID,
Scopes: []string{fmt.Sprintf("%s/.default", p.resourceID)},
}

// Use the token provider to get a new token with the new context
azdAccessToken, err := p.cred.GetToken(ctx, policyOptions)
if err != nil {
return emptyToken, fmt.Errorf("expected an empty error but received: %v", err)
}

if azdAccessToken.Token == "" {
return emptyToken, errors.New("did not receive a token")
}

// azurecore.AccessTokens have ExpiresOn as Time.Time. We need to convert it to JSON.Number
// by fetching the time in seconds since the Unix epoch via Unix() and then converting to a
// JSON.Number via formatting as a string using a base-10 int64 conversion.
expiresOn := json.Number(strconv.FormatInt(azdAccessToken.ExpiresOn.Unix(), 10))

// Re-wrap the azurecore.AccessToken into an adal.Token
return adal.Token{
AccessToken: azdAccessToken.Token,
ExpiresOn: expiresOn,
Resource: p.resourceID,
}, nil
}
26 changes: 26 additions & 0 deletions pkg/internal/token/azuredevelopercli_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package token

import (
"context"
"testing"

"github.com/Azure/kubelogin/pkg/internal/testutils"
)

func TestNewAzureDeveloperCLITokenEmpty(t *testing.T) {
// Using default timeout for testing
_, err := newAzureDeveloperCLIToken("", "", defaultTimeout)

if !testutils.ErrorContains(err, "resourceID cannot be empty") {
t.Errorf("unexpected error: %v", err)
}
}

func TestNewAzureDeveloperCLIToken(t *testing.T) {
azd := AzureDeveloperCLIToken{}
_, err := azd.Token(context.TODO())

if !testutils.ErrorContains(err, "credential is nil") {
t.Errorf("unexpected error: %v", err)
}
}
2 changes: 1 addition & 1 deletion pkg/internal/token/execCredentialPlugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ func New(o *Options) (ExecCredentialPlugin, error) {
return nil, err
}
disableTokenCache := false
if o.LoginMethod == ServicePrincipalLogin || o.LoginMethod == MSILogin || o.LoginMethod == WorkloadIdentityLogin || o.LoginMethod == AzureCLILogin {
if o.LoginMethod == ServicePrincipalLogin || o.LoginMethod == MSILogin || o.LoginMethod == WorkloadIdentityLogin || o.LoginMethod == AzureCLILogin || o.LoginMethod == AzureDeveloperCLILogin {
disableTokenCache = true
}
return &execCredentialPlugin{
Expand Down
19 changes: 10 additions & 9 deletions pkg/internal/token/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,14 +38,15 @@ type Options struct {
const (
defaultEnvironmentName = "AzurePublicCloud"

DeviceCodeLogin = "devicecode"
InteractiveLogin = "interactive"
ServicePrincipalLogin = "spn"
ROPCLogin = "ropc"
MSILogin = "msi"
AzureCLILogin = "azurecli"
WorkloadIdentityLogin = "workloadidentity"
manualTokenLogin = "manual_token"
DeviceCodeLogin = "devicecode"
InteractiveLogin = "interactive"
ServicePrincipalLogin = "spn"
ROPCLogin = "ropc"
MSILogin = "msi"
AzureCLILogin = "azurecli"
AzureDeveloperCLILogin = "azd"
WorkloadIdentityLogin = "workloadidentity"
manualTokenLogin = "manual_token"
)

var (
Expand All @@ -54,7 +55,7 @@ var (
)

func init() {
supportedLogin = []string{DeviceCodeLogin, InteractiveLogin, ServicePrincipalLogin, ROPCLogin, MSILogin, AzureCLILogin, WorkloadIdentityLogin}
supportedLogin = []string{DeviceCodeLogin, InteractiveLogin, ServicePrincipalLogin, ROPCLogin, MSILogin, AzureCLILogin, AzureDeveloperCLILogin, WorkloadIdentityLogin}
}

func GetSupportedLogins() string {
Expand Down
2 changes: 2 additions & 0 deletions pkg/internal/token/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ func NewTokenProvider(o *Options) (TokenProvider, error) {
return newAzureCLIToken(o.ServerID, o.TenantID, o.Timeout)
case WorkloadIdentityLogin:
return newWorkloadIdentityToken(o.ClientID, o.FederatedTokenFile, o.AuthorityHost, o.ServerID, o.TenantID)
case AzureDeveloperCLILogin:
return newAzureDeveloperCLIToken(o.ServerID, o.TenantID, o.Timeout)
}

return nil, errors.New("unsupported token provider")
Expand Down