cli/azd/cmd/auth_token.go (143 lines of code) (raw):
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package cmd
import (
"context"
"fmt"
"io"
"os"
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
"github.com/azure/azure-dev/cli/azd/cmd/actions"
"github.com/azure/azure-dev/cli/azd/internal"
"github.com/azure/azure-dev/cli/azd/pkg/account"
"github.com/azure/azure-dev/cli/azd/pkg/auth"
"github.com/azure/azure-dev/cli/azd/pkg/cloud"
"github.com/azure/azure-dev/cli/azd/pkg/contracts"
"github.com/azure/azure-dev/cli/azd/pkg/environment"
"github.com/azure/azure-dev/cli/azd/pkg/output"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)
type authTokenFlags struct {
tenantID string
scopes []string
global *internal.GlobalCommandOptions
}
func newAuthTokenFlags(cmd *cobra.Command, global *internal.GlobalCommandOptions) *authTokenFlags {
flags := &authTokenFlags{}
flags.Bind(cmd.Flags(), global)
return flags
}
func newAuthTokenCmd() *cobra.Command {
return &cobra.Command{
Use: "token --output json",
Hidden: true,
}
}
func (f *authTokenFlags) Bind(local *pflag.FlagSet, global *internal.GlobalCommandOptions) {
f.global = global
local.StringArrayVar(&f.scopes, "scope", nil, "The scope to use when requesting an access token")
local.StringVar(&f.tenantID, "tenant-id", "", "The tenant id to use when requesting an access token.")
}
type CredentialProviderFn func(context.Context, *auth.CredentialForCurrentUserOptions) (azcore.TokenCredential, error)
type authTokenAction struct {
credentialProvider CredentialProviderFn
formatter output.Formatter
writer io.Writer
envResolver environment.EnvironmentResolver
subResolver account.SubscriptionTenantResolver
flags *authTokenFlags
cloud *cloud.Cloud
}
func newAuthTokenAction(
credentialProvider CredentialProviderFn,
formatter output.Formatter,
writer io.Writer,
flags *authTokenFlags,
envResolver environment.EnvironmentResolver,
subResolver account.SubscriptionTenantResolver,
cloud *cloud.Cloud,
) actions.Action {
return &authTokenAction{
credentialProvider: credentialProvider,
envResolver: envResolver,
subResolver: subResolver,
formatter: formatter,
writer: writer,
flags: flags,
cloud: cloud,
}
}
func getTenantIdFromAzdEnv(
ctx context.Context,
envResolver environment.EnvironmentResolver,
subResolver account.SubscriptionTenantResolver) (tenantId string, err error) {
azdEnv, err := envResolver(ctx)
if err != nil {
// No azd env, return empty tenantId
return tenantId, nil
}
subIdAtAzdEnv := azdEnv.GetSubscriptionId()
if subIdAtAzdEnv == "" {
// azd env found, but missing or empty subscriptionID
return tenantId, nil
}
tenantId, err = subResolver.LookupTenant(ctx, subIdAtAzdEnv)
if err != nil {
return tenantId, fmt.Errorf(
"resolving the Azure Directory from azd environment (%s): %w",
azdEnv.Name(),
err)
}
return tenantId, nil
}
func getTenantIdFromEnv(
ctx context.Context,
subResolver account.SubscriptionTenantResolver) (tenantId string, err error) {
subIdAtSysEnv, found := os.LookupEnv(environment.SubscriptionIdEnvVarName)
if !found {
// no env var from system
return tenantId, nil
}
tenantId, err = subResolver.LookupTenant(ctx, subIdAtSysEnv)
if err != nil {
return tenantId, fmt.Errorf(
"resolving the Azure Directory from system environment (%s): %w", environment.SubscriptionIdEnvVarName, err)
}
return tenantId, nil
}
func (a *authTokenAction) Run(ctx context.Context) (*actions.ActionResult, error) {
if len(a.flags.scopes) == 0 {
a.flags.scopes = auth.LoginScopes(a.cloud)
}
var cred azcore.TokenCredential
// 1) flag --tenant-id is the highest priority. If it is not use, azd will check if subscriptionId is set as env var
tenantId := a.flags.tenantID
// 2) From azd env
if tenantId == "" {
tenantIdFromAzdEnv, err := getTenantIdFromAzdEnv(ctx, a.envResolver, a.subResolver)
if err != nil {
return nil, err
}
tenantId = tenantIdFromAzdEnv
}
// 3) From system env
if tenantId == "" {
tenantIdFromSysEnv, err := getTenantIdFromEnv(ctx, a.subResolver)
if err != nil {
return nil, err
}
tenantId = tenantIdFromSysEnv
}
// If tenantId is still empty, the fallback is to use current logged in user's home-tenant id.
cred, err := a.credentialProvider(ctx, &auth.CredentialForCurrentUserOptions{
NoPrompt: true,
TenantID: tenantId,
})
if err != nil {
return nil, err
}
token, err := cred.GetToken(ctx, policy.TokenRequestOptions{
Scopes: a.flags.scopes,
})
if err != nil {
return nil, fmt.Errorf("fetching token: %w", err)
}
res := contracts.AuthTokenResult{
Token: token.Token,
ExpiresOn: contracts.RFC3339Time(token.ExpiresOn),
}
return nil, a.formatter.Format(res, a.writer, nil)
}