package auth

// Copyright 2017 Microsoft Corporation
//
//  Licensed under the Apache License, Version 2.0 (the "License");
//  you may not use this file except in compliance with the License.
//  You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
//  Unless required by applicable law or agreed to in writing, software
//  distributed under the License is distributed on an "AS IS" BASIS,
//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//  See the License for the specific language governing permissions and
//  limitations under the License.

import (
	"bytes"
	"context"
	"encoding/binary"
	"encoding/json"
	"errors"
	"fmt"
	"io/ioutil"
	"log"
	"os"
	"strings"
	"unicode/utf16"

	"github.com/Azure/go-autorest/autorest"
	"github.com/Azure/go-autorest/autorest/adal"
	"github.com/Azure/go-autorest/autorest/azure"
	"github.com/Azure/go-autorest/autorest/azure/cli"
	"github.com/Azure/go-autorest/logger"
	"github.com/dimchansky/utfbom"
)

// The possible keys in the Values map.
const (
	SubscriptionID          = "AZURE_SUBSCRIPTION_ID"
	TenantID                = "AZURE_TENANT_ID"
	AuxiliaryTenantIDs      = "AZURE_AUXILIARY_TENANT_IDS"
	ClientID                = "AZURE_CLIENT_ID"
	ClientSecret            = "AZURE_CLIENT_SECRET"
	CertificatePath         = "AZURE_CERTIFICATE_PATH"
	CertificatePassword     = "AZURE_CERTIFICATE_PASSWORD"
	Username                = "AZURE_USERNAME"
	Password                = "AZURE_PASSWORD"
	EnvironmentName         = "AZURE_ENVIRONMENT"
	Resource                = "AZURE_AD_RESOURCE"
	ActiveDirectoryEndpoint = "ActiveDirectoryEndpoint"
	ResourceManagerEndpoint = "ResourceManagerEndpoint"
	GraphResourceID         = "GraphResourceID"
	SQLManagementEndpoint   = "SQLManagementEndpoint"
	GalleryEndpoint         = "GalleryEndpoint"
	ManagementEndpoint      = "ManagementEndpoint"
)

// NewAuthorizerFromEnvironment creates an Authorizer configured from environment variables in the order:
// 1. Client credentials
// 2. Client certificate
// 3. Username password
// 4. MSI
func NewAuthorizerFromEnvironment() (autorest.Authorizer, error) {
	logger.Instance.Writeln(logger.LogInfo, "NewAuthorizerFromEnvironment() determining authentication mechanism")
	settings, err := GetSettingsFromEnvironment()
	if err != nil {
		return nil, err
	}
	return settings.GetAuthorizer()
}

// NewAuthorizerFromEnvironmentWithResource creates an Authorizer configured from environment variables in the order:
// 1. Client credentials
// 2. Client certificate
// 3. Username password
// 4. MSI
func NewAuthorizerFromEnvironmentWithResource(resource string) (autorest.Authorizer, error) {
	logger.Instance.Writeln(logger.LogInfo, "NewAuthorizerFromEnvironmentWithResource() determining authentication mechanism")
	settings, err := GetSettingsFromEnvironment()
	if err != nil {
		return nil, err
	}
	settings.Values[Resource] = resource
	return settings.GetAuthorizer()
}

// EnvironmentSettings contains the available authentication settings.
type EnvironmentSettings struct {
	Values      map[string]string
	Environment azure.Environment
}

// GetSettingsFromEnvironment returns the available authentication settings from the environment.
func GetSettingsFromEnvironment() (s EnvironmentSettings, err error) {
	s = EnvironmentSettings{
		Values: map[string]string{},
	}
	s.setValue(SubscriptionID)
	s.setValue(TenantID)
	s.setValue(AuxiliaryTenantIDs)
	s.setValue(ClientID)
	s.setValue(ClientSecret)
	s.setValue(CertificatePath)
	s.setValue(CertificatePassword)
	s.setValue(Username)
	s.setValue(Password)
	s.setValue(EnvironmentName)
	s.setValue(Resource)
	if v := s.Values[EnvironmentName]; v == "" {
		s.Environment = azure.PublicCloud
	} else {
		s.Environment, err = azure.EnvironmentFromName(v)
	}
	if s.Values[Resource] == "" {
		s.Values[Resource] = s.Environment.ResourceManagerEndpoint
	}
	return
}

// GetSubscriptionID returns the available subscription ID or an empty string.
func (settings EnvironmentSettings) GetSubscriptionID() string {
	return settings.Values[SubscriptionID]
}

// adds the specified environment variable value to the Values map if it exists
func (settings EnvironmentSettings) setValue(key string) {
	if v := os.Getenv(key); v != "" {
		logger.Instance.Writef(logger.LogInfo, "GetSettingsFromEnvironment() found environment var %s\n", key)
		settings.Values[key] = v
	}
}

// helper to return client and tenant IDs
func (settings EnvironmentSettings) getClientAndTenant() (string, string) {
	clientID := settings.Values[ClientID]
	tenantID := settings.Values[TenantID]
	return clientID, tenantID
}

// GetClientCredentials creates a config object from the available client credentials.
// An error is returned if no client credentials are available.
func (settings EnvironmentSettings) GetClientCredentials() (ClientCredentialsConfig, error) {
	secret := settings.Values[ClientSecret]
	if secret == "" {
		logger.Instance.Writeln(logger.LogInfo, "EnvironmentSettings.GetClientCredentials() missing client secret")
		return ClientCredentialsConfig{}, errors.New("missing client secret")
	}
	clientID, tenantID := settings.getClientAndTenant()
	config := NewClientCredentialsConfig(clientID, secret, tenantID)
	config.AADEndpoint = settings.Environment.ActiveDirectoryEndpoint
	config.Resource = settings.Values[Resource]
	if auxTenants, ok := settings.Values[AuxiliaryTenantIDs]; ok {
		config.AuxTenants = strings.Split(auxTenants, ";")
		for i := range config.AuxTenants {
			config.AuxTenants[i] = strings.TrimSpace(config.AuxTenants[i])
		}
	}
	return config, nil
}

// GetClientCertificate creates a config object from the available certificate credentials.
// An error is returned if no certificate credentials are available.
func (settings EnvironmentSettings) GetClientCertificate() (ClientCertificateConfig, error) {
	certPath := settings.Values[CertificatePath]
	if certPath == "" {
		logger.Instance.Writeln(logger.LogInfo, "EnvironmentSettings.GetClientCertificate() missing certificate path")
		return ClientCertificateConfig{}, errors.New("missing certificate path")
	}
	certPwd := settings.Values[CertificatePassword]
	clientID, tenantID := settings.getClientAndTenant()
	config := NewClientCertificateConfig(certPath, certPwd, clientID, tenantID)
	config.AADEndpoint = settings.Environment.ActiveDirectoryEndpoint
	config.Resource = settings.Values[Resource]
	return config, nil
}

// GetUsernamePassword creates a config object from the available username/password credentials.
// An error is returned if no username/password credentials are available.
func (settings EnvironmentSettings) GetUsernamePassword() (UsernamePasswordConfig, error) {
	username := settings.Values[Username]
	password := settings.Values[Password]
	if username == "" || password == "" {
		logger.Instance.Writeln(logger.LogInfo, "EnvironmentSettings.GetUsernamePassword() missing username and/or password")
		return UsernamePasswordConfig{}, errors.New("missing username/password")
	}
	clientID, tenantID := settings.getClientAndTenant()
	config := NewUsernamePasswordConfig(username, password, clientID, tenantID)
	config.AADEndpoint = settings.Environment.ActiveDirectoryEndpoint
	config.Resource = settings.Values[Resource]
	return config, nil
}

// GetMSI creates a MSI config object from the available client ID.
func (settings EnvironmentSettings) GetMSI() MSIConfig {
	config := NewMSIConfig()
	config.Resource = settings.Values[Resource]
	config.ClientID = settings.Values[ClientID]
	return config
}

// GetDeviceFlow creates a device-flow config object from the available client and tenant IDs.
func (settings EnvironmentSettings) GetDeviceFlow() DeviceFlowConfig {
	clientID, tenantID := settings.getClientAndTenant()
	config := NewDeviceFlowConfig(clientID, tenantID)
	config.AADEndpoint = settings.Environment.ActiveDirectoryEndpoint
	config.Resource = settings.Values[Resource]
	return config
}

// GetAuthorizer creates an Authorizer configured from environment variables in the order:
// 1. Client credentials
// 2. Client certificate
// 3. Username password
// 4. MSI
func (settings EnvironmentSettings) GetAuthorizer() (autorest.Authorizer, error) {
	//1.Client Credentials
	if c, e := settings.GetClientCredentials(); e == nil {
		logger.Instance.Writeln(logger.LogInfo, "EnvironmentSettings.GetAuthorizer() using client secret credentials")
		return c.Authorizer()
	}

	//2. Client Certificate
	if c, e := settings.GetClientCertificate(); e == nil {
		logger.Instance.Writeln(logger.LogInfo, "EnvironmentSettings.GetAuthorizer() using client certificate credentials")
		return c.Authorizer()
	}

	//3. Username Password
	if c, e := settings.GetUsernamePassword(); e == nil {
		logger.Instance.Writeln(logger.LogInfo, "EnvironmentSettings.GetAuthorizer() using user name/password credentials")
		return c.Authorizer()
	}

	// 4. MSI
	if !adal.MSIAvailable(context.Background(), nil) {
		return nil, errors.New("MSI not available")
	}
	logger.Instance.Writeln(logger.LogInfo, "EnvironmentSettings.GetAuthorizer() using MSI authentication")
	return settings.GetMSI().Authorizer()
}

// NewAuthorizerFromFile creates an Authorizer configured from a configuration file in the following order.
// 1. Client credentials
// 2. Client certificate
// The path to the configuration file must be specified in the AZURE_AUTH_LOCATION environment variable.
// resourceBaseURI - used to determine the resource type
func NewAuthorizerFromFile(resourceBaseURI string) (autorest.Authorizer, error) {
	settings, err := GetSettingsFromFile()
	if err != nil {
		return nil, err
	}
	return settings.GetAuthorizer(resourceBaseURI)
}

// GetAuthorizer create an Authorizer in the following order.
// 1. Client credentials
// 2. Client certificate
// resourceBaseURI - used to determine the resource type
func (settings FileSettings) GetAuthorizer(resourceBaseURI string) (autorest.Authorizer, error) {
	if resourceBaseURI == "" {
		resourceBaseURI = azure.PublicCloud.ServiceManagementEndpoint
	}
	if a, err := settings.ClientCredentialsAuthorizer(resourceBaseURI); err == nil {
		return a, err
	}
	if a, err := settings.ClientCertificateAuthorizer(resourceBaseURI); err == nil {
		return a, err
	}
	return nil, errors.New("auth file missing client and certificate credentials")
}

// NewAuthorizerFromFileWithResource creates an Authorizer configured from a configuration file in the following order.
// 1. Client credentials
// 2. Client certificate
// The path to the configuration file must be specified in the AZURE_AUTH_LOCATION environment variable.
func NewAuthorizerFromFileWithResource(resource string) (autorest.Authorizer, error) {
	s, err := GetSettingsFromFile()
	if err != nil {
		return nil, err
	}
	if a, err := s.ClientCredentialsAuthorizerWithResource(resource); err == nil {
		return a, err
	}
	if a, err := s.ClientCertificateAuthorizerWithResource(resource); err == nil {
		return a, err
	}
	return nil, errors.New("auth file missing client and certificate credentials")
}

// NewAuthorizerFromCLI creates an Authorizer configured from Azure CLI 2.0 for local development scenarios.
func NewAuthorizerFromCLI() (autorest.Authorizer, error) {
	settings, err := GetSettingsFromEnvironment()
	if err != nil {
		return nil, err
	}

	if settings.Values[Resource] == "" {
		settings.Values[Resource] = settings.Environment.ResourceManagerEndpoint
	}

	return NewAuthorizerFromCLIWithResource(settings.Values[Resource])
}

// NewAuthorizerFromCLIWithResource creates an Authorizer configured from Azure CLI 2.0 for local development scenarios.
func NewAuthorizerFromCLIWithResource(resource string) (autorest.Authorizer, error) {
	token, err := cli.GetTokenFromCLI(resource)
	if err != nil {
		return nil, err
	}

	adalToken, err := token.ToADALToken()
	if err != nil {
		return nil, err
	}

	return autorest.NewBearerAuthorizer(&adalToken), nil
}

// GetSettingsFromFile returns the available authentication settings from an Azure CLI authentication file.
func GetSettingsFromFile() (FileSettings, error) {
	s := FileSettings{}
	fileLocation := os.Getenv("AZURE_AUTH_LOCATION")
	if fileLocation == "" {
		return s, errors.New("environment variable AZURE_AUTH_LOCATION is not set")
	}

	contents, err := ioutil.ReadFile(fileLocation)
	if err != nil {
		return s, err
	}

	// Auth file might be encoded
	decoded, err := decode(contents)
	if err != nil {
		return s, err
	}

	authFile := map[string]interface{}{}
	err = json.Unmarshal(decoded, &authFile)
	if err != nil {
		return s, err
	}

	s.Values = map[string]string{}
	s.setKeyValue(ClientID, authFile["clientId"])
	s.setKeyValue(ClientSecret, authFile["clientSecret"])
	s.setKeyValue(CertificatePath, authFile["clientCertificate"])
	s.setKeyValue(CertificatePassword, authFile["clientCertificatePassword"])
	s.setKeyValue(SubscriptionID, authFile["subscriptionId"])
	s.setKeyValue(TenantID, authFile["tenantId"])
	s.setKeyValue(ActiveDirectoryEndpoint, authFile["activeDirectoryEndpointUrl"])
	s.setKeyValue(ResourceManagerEndpoint, authFile["resourceManagerEndpointUrl"])
	s.setKeyValue(GraphResourceID, authFile["activeDirectoryGraphResourceId"])
	s.setKeyValue(SQLManagementEndpoint, authFile["sqlManagementEndpointUrl"])
	s.setKeyValue(GalleryEndpoint, authFile["galleryEndpointUrl"])
	s.setKeyValue(ManagementEndpoint, authFile["managementEndpointUrl"])
	return s, nil
}

// FileSettings contains the available authentication settings.
type FileSettings struct {
	Values map[string]string
}

// GetSubscriptionID returns the available subscription ID or an empty string.
func (settings FileSettings) GetSubscriptionID() string {
	return settings.Values[SubscriptionID]
}

// adds the specified value to the Values map if it isn't nil
func (settings FileSettings) setKeyValue(key string, val interface{}) {
	if val != nil {
		settings.Values[key] = val.(string)
	}
}

// returns the specified AAD endpoint or the public cloud endpoint if unspecified
func (settings FileSettings) getAADEndpoint() string {
	if v, ok := settings.Values[ActiveDirectoryEndpoint]; ok {
		return v
	}
	return azure.PublicCloud.ActiveDirectoryEndpoint
}

// ServicePrincipalTokenFromClientCredentials creates a ServicePrincipalToken from the available client credentials.
func (settings FileSettings) ServicePrincipalTokenFromClientCredentials(baseURI string) (*adal.ServicePrincipalToken, error) {
	resource, err := settings.getResourceForToken(baseURI)
	if err != nil {
		return nil, err
	}
	return settings.ServicePrincipalTokenFromClientCredentialsWithResource(resource)
}

// ClientCredentialsAuthorizer creates an authorizer from the available client credentials.
func (settings FileSettings) ClientCredentialsAuthorizer(baseURI string) (autorest.Authorizer, error) {
	resource, err := settings.getResourceForToken(baseURI)
	if err != nil {
		return nil, err
	}
	return settings.ClientCredentialsAuthorizerWithResource(resource)
}

// ServicePrincipalTokenFromClientCredentialsWithResource creates a ServicePrincipalToken
// from the available client credentials and the specified resource.
func (settings FileSettings) ServicePrincipalTokenFromClientCredentialsWithResource(resource string) (*adal.ServicePrincipalToken, error) {
	if _, ok := settings.Values[ClientSecret]; !ok {
		return nil, errors.New("missing client secret")
	}
	config, err := adal.NewOAuthConfig(settings.getAADEndpoint(), settings.Values[TenantID])
	if err != nil {
		return nil, err
	}
	return adal.NewServicePrincipalToken(*config, settings.Values[ClientID], settings.Values[ClientSecret], resource)
}

func (settings FileSettings) clientCertificateConfigWithResource(resource string) (ClientCertificateConfig, error) {
	if _, ok := settings.Values[CertificatePath]; !ok {
		return ClientCertificateConfig{}, errors.New("missing certificate path")
	}
	cfg := NewClientCertificateConfig(settings.Values[CertificatePath], settings.Values[CertificatePassword], settings.Values[ClientID], settings.Values[TenantID])
	cfg.AADEndpoint = settings.getAADEndpoint()
	cfg.Resource = resource
	return cfg, nil
}

// ClientCredentialsAuthorizerWithResource creates an authorizer from the available client credentials and the specified resource.
func (settings FileSettings) ClientCredentialsAuthorizerWithResource(resource string) (autorest.Authorizer, error) {
	spToken, err := settings.ServicePrincipalTokenFromClientCredentialsWithResource(resource)
	if err != nil {
		return nil, err
	}
	return autorest.NewBearerAuthorizer(spToken), nil
}

// ServicePrincipalTokenFromClientCertificate creates a ServicePrincipalToken from the available certificate credentials.
func (settings FileSettings) ServicePrincipalTokenFromClientCertificate(baseURI string) (*adal.ServicePrincipalToken, error) {
	resource, err := settings.getResourceForToken(baseURI)
	if err != nil {
		return nil, err
	}
	return settings.ServicePrincipalTokenFromClientCertificateWithResource(resource)
}

// ClientCertificateAuthorizer creates an authorizer from the available certificate credentials.
func (settings FileSettings) ClientCertificateAuthorizer(baseURI string) (autorest.Authorizer, error) {
	resource, err := settings.getResourceForToken(baseURI)
	if err != nil {
		return nil, err
	}
	return settings.ClientCertificateAuthorizerWithResource(resource)
}

// ServicePrincipalTokenFromClientCertificateWithResource creates a ServicePrincipalToken from the available certificate credentials.
func (settings FileSettings) ServicePrincipalTokenFromClientCertificateWithResource(resource string) (*adal.ServicePrincipalToken, error) {
	cfg, err := settings.clientCertificateConfigWithResource(resource)
	if err != nil {
		return nil, err
	}
	return cfg.ServicePrincipalToken()
}

// ClientCertificateAuthorizerWithResource creates an authorizer from the available certificate credentials and the specified resource.
func (settings FileSettings) ClientCertificateAuthorizerWithResource(resource string) (autorest.Authorizer, error) {
	cfg, err := settings.clientCertificateConfigWithResource(resource)
	if err != nil {
		return nil, err
	}
	return cfg.Authorizer()
}

func decode(b []byte) ([]byte, error) {
	reader, enc := utfbom.Skip(bytes.NewReader(b))

	switch enc {
	case utfbom.UTF16LittleEndian:
		u16 := make([]uint16, (len(b)/2)-1)
		err := binary.Read(reader, binary.LittleEndian, &u16)
		if err != nil {
			return nil, err
		}
		return []byte(string(utf16.Decode(u16))), nil
	case utfbom.UTF16BigEndian:
		u16 := make([]uint16, (len(b)/2)-1)
		err := binary.Read(reader, binary.BigEndian, &u16)
		if err != nil {
			return nil, err
		}
		return []byte(string(utf16.Decode(u16))), nil
	}
	return ioutil.ReadAll(reader)
}

func (settings FileSettings) getResourceForToken(baseURI string) (string, error) {
	// Compare default base URI from the SDK to the endpoints from the public cloud
	// Base URI and token resource are the same string. This func finds the authentication
	// file field that matches the SDK base URI. The SDK defines the public cloud
	// endpoint as its default base URI
	if !strings.HasSuffix(baseURI, "/") {
		baseURI += "/"
	}
	switch baseURI {
	case azure.PublicCloud.ServiceManagementEndpoint:
		return settings.Values[ManagementEndpoint], nil
	case azure.PublicCloud.ResourceManagerEndpoint:
		return settings.Values[ResourceManagerEndpoint], nil
	case azure.PublicCloud.ActiveDirectoryEndpoint:
		return settings.Values[ActiveDirectoryEndpoint], nil
	case azure.PublicCloud.GalleryEndpoint:
		return settings.Values[GalleryEndpoint], nil
	case azure.PublicCloud.GraphEndpoint:
		return settings.Values[GraphResourceID], nil
	}
	return "", fmt.Errorf("auth: base URI not found in endpoints")
}

// NewClientCredentialsConfig creates an AuthorizerConfig object configured to obtain an Authorizer through Client Credentials.
// Defaults to Public Cloud and Resource Manager Endpoint.
func NewClientCredentialsConfig(clientID string, clientSecret string, tenantID string) ClientCredentialsConfig {
	return ClientCredentialsConfig{
		ClientID:     clientID,
		ClientSecret: clientSecret,
		TenantID:     tenantID,
		Resource:     azure.PublicCloud.ResourceManagerEndpoint,
		AADEndpoint:  azure.PublicCloud.ActiveDirectoryEndpoint,
	}
}

// NewClientCertificateConfig creates a ClientCertificateConfig object configured to obtain an Authorizer through client certificate.
// Defaults to Public Cloud and Resource Manager Endpoint.
func NewClientCertificateConfig(certificatePath string, certificatePassword string, clientID string, tenantID string) ClientCertificateConfig {
	return ClientCertificateConfig{
		CertificatePath:     certificatePath,
		CertificatePassword: certificatePassword,
		ClientID:            clientID,
		TenantID:            tenantID,
		Resource:            azure.PublicCloud.ResourceManagerEndpoint,
		AADEndpoint:         azure.PublicCloud.ActiveDirectoryEndpoint,
	}
}

// NewUsernamePasswordConfig creates an UsernamePasswordConfig object configured to obtain an Authorizer through username and password.
// Defaults to Public Cloud and Resource Manager Endpoint.
func NewUsernamePasswordConfig(username string, password string, clientID string, tenantID string) UsernamePasswordConfig {
	return UsernamePasswordConfig{
		Username:    username,
		Password:    password,
		ClientID:    clientID,
		TenantID:    tenantID,
		Resource:    azure.PublicCloud.ResourceManagerEndpoint,
		AADEndpoint: azure.PublicCloud.ActiveDirectoryEndpoint,
	}
}

// NewMSIConfig creates an MSIConfig object configured to obtain an Authorizer through MSI.
func NewMSIConfig() MSIConfig {
	return MSIConfig{
		Resource: azure.PublicCloud.ResourceManagerEndpoint,
	}
}

// NewDeviceFlowConfig creates a DeviceFlowConfig object configured to obtain an Authorizer through device flow.
// Defaults to Public Cloud and Resource Manager Endpoint.
func NewDeviceFlowConfig(clientID string, tenantID string) DeviceFlowConfig {
	return DeviceFlowConfig{
		ClientID:    clientID,
		TenantID:    tenantID,
		Resource:    azure.PublicCloud.ResourceManagerEndpoint,
		AADEndpoint: azure.PublicCloud.ActiveDirectoryEndpoint,
	}
}

// AuthorizerConfig provides an authorizer from the configuration provided.
type AuthorizerConfig interface {
	Authorizer() (autorest.Authorizer, error)
}

// ClientCredentialsConfig provides the options to get a bearer authorizer from client credentials.
type ClientCredentialsConfig struct {
	ClientID     string
	ClientSecret string
	TenantID     string
	AuxTenants   []string
	AADEndpoint  string
	Resource     string
}

// ServicePrincipalToken creates a ServicePrincipalToken from client credentials.
func (ccc ClientCredentialsConfig) ServicePrincipalToken() (*adal.ServicePrincipalToken, error) {
	oauthConfig, err := adal.NewOAuthConfig(ccc.AADEndpoint, ccc.TenantID)
	if err != nil {
		return nil, err
	}
	return adal.NewServicePrincipalToken(*oauthConfig, ccc.ClientID, ccc.ClientSecret, ccc.Resource)
}

// MultiTenantServicePrincipalToken creates a MultiTenantServicePrincipalToken from client credentials.
func (ccc ClientCredentialsConfig) MultiTenantServicePrincipalToken() (*adal.MultiTenantServicePrincipalToken, error) {
	oauthConfig, err := adal.NewMultiTenantOAuthConfig(ccc.AADEndpoint, ccc.TenantID, ccc.AuxTenants, adal.OAuthOptions{})
	if err != nil {
		return nil, err
	}
	return adal.NewMultiTenantServicePrincipalToken(oauthConfig, ccc.ClientID, ccc.ClientSecret, ccc.Resource)
}

// Authorizer gets the authorizer from client credentials.
func (ccc ClientCredentialsConfig) Authorizer() (autorest.Authorizer, error) {
	if len(ccc.AuxTenants) == 0 {
		spToken, err := ccc.ServicePrincipalToken()
		if err != nil {
			return nil, fmt.Errorf("failed to get SPT from client credentials: %v", err)
		}
		return autorest.NewBearerAuthorizer(spToken), nil
	}
	mtSPT, err := ccc.MultiTenantServicePrincipalToken()
	if err != nil {
		return nil, fmt.Errorf("failed to get multitenant SPT from client credentials: %v", err)
	}
	return autorest.NewMultiTenantServicePrincipalTokenAuthorizer(mtSPT), nil
}

// ClientCertificateConfig provides the options to get a bearer authorizer from a client certificate.
type ClientCertificateConfig struct {
	ClientID            string
	CertificatePath     string
	CertificatePassword string
	TenantID            string
	AuxTenants          []string
	AADEndpoint         string
	Resource            string
}

// ServicePrincipalToken creates a ServicePrincipalToken from client certificate.
func (ccc ClientCertificateConfig) ServicePrincipalToken() (*adal.ServicePrincipalToken, error) {
	oauthConfig, err := adal.NewOAuthConfig(ccc.AADEndpoint, ccc.TenantID)
	if err != nil {
		return nil, err
	}
	certData, err := ioutil.ReadFile(ccc.CertificatePath)
	if err != nil {
		return nil, fmt.Errorf("failed to read the certificate file (%s): %v", ccc.CertificatePath, err)
	}
	certificate, rsaPrivateKey, err := adal.DecodePfxCertificateData(certData, ccc.CertificatePassword)
	if err != nil {
		return nil, fmt.Errorf("failed to decode pkcs12 certificate while creating spt: %v", err)
	}
	return adal.NewServicePrincipalTokenFromCertificate(*oauthConfig, ccc.ClientID, certificate, rsaPrivateKey, ccc.Resource)
}

// MultiTenantServicePrincipalToken creates a MultiTenantServicePrincipalToken from client certificate.
func (ccc ClientCertificateConfig) MultiTenantServicePrincipalToken() (*adal.MultiTenantServicePrincipalToken, error) {
	oauthConfig, err := adal.NewMultiTenantOAuthConfig(ccc.AADEndpoint, ccc.TenantID, ccc.AuxTenants, adal.OAuthOptions{})
	if err != nil {
		return nil, err
	}
	certData, err := ioutil.ReadFile(ccc.CertificatePath)
	if err != nil {
		return nil, fmt.Errorf("failed to read the certificate file (%s): %v", ccc.CertificatePath, err)
	}
	certificate, rsaPrivateKey, err := adal.DecodePfxCertificateData(certData, ccc.CertificatePassword)
	if err != nil {
		return nil, fmt.Errorf("failed to decode pkcs12 certificate while creating spt: %v", err)
	}
	return adal.NewMultiTenantServicePrincipalTokenFromCertificate(oauthConfig, ccc.ClientID, certificate, rsaPrivateKey, ccc.Resource)
}

// Authorizer gets an authorizer object from client certificate.
func (ccc ClientCertificateConfig) Authorizer() (autorest.Authorizer, error) {
	if len(ccc.AuxTenants) == 0 {
		spToken, err := ccc.ServicePrincipalToken()
		if err != nil {
			return nil, fmt.Errorf("failed to get oauth token from certificate auth: %v", err)
		}
		return autorest.NewBearerAuthorizer(spToken), nil
	}
	mtSPT, err := ccc.MultiTenantServicePrincipalToken()
	if err != nil {
		return nil, fmt.Errorf("failed to get multitenant SPT from certificate auth: %v", err)
	}
	return autorest.NewMultiTenantServicePrincipalTokenAuthorizer(mtSPT), nil
}

// DeviceFlowConfig provides the options to get a bearer authorizer using device flow authentication.
type DeviceFlowConfig struct {
	ClientID    string
	TenantID    string
	AADEndpoint string
	Resource    string
}

// Authorizer gets the authorizer from device flow.
func (dfc DeviceFlowConfig) Authorizer() (autorest.Authorizer, error) {
	spToken, err := dfc.ServicePrincipalToken()
	if err != nil {
		return nil, fmt.Errorf("failed to get oauth token from device flow: %v", err)
	}
	return autorest.NewBearerAuthorizer(spToken), nil
}

// ServicePrincipalToken gets the service principal token from device flow.
func (dfc DeviceFlowConfig) ServicePrincipalToken() (*adal.ServicePrincipalToken, error) {
	oauthConfig, err := adal.NewOAuthConfig(dfc.AADEndpoint, dfc.TenantID)
	if err != nil {
		return nil, err
	}
	oauthClient := &autorest.Client{}
	deviceCode, err := adal.InitiateDeviceAuth(oauthClient, *oauthConfig, dfc.ClientID, dfc.Resource)
	if err != nil {
		return nil, fmt.Errorf("failed to start device auth flow: %s", err)
	}
	log.Println(*deviceCode.Message)
	token, err := adal.WaitForUserCompletion(oauthClient, deviceCode)
	if err != nil {
		return nil, fmt.Errorf("failed to finish device auth flow: %s", err)
	}
	return adal.NewServicePrincipalTokenFromManualToken(*oauthConfig, dfc.ClientID, dfc.Resource, *token)
}

// UsernamePasswordConfig provides the options to get a bearer authorizer from a username and a password.
type UsernamePasswordConfig struct {
	ClientID    string
	Username    string
	Password    string
	TenantID    string
	AADEndpoint string
	Resource    string
}

// ServicePrincipalToken creates a ServicePrincipalToken from username and password.
func (ups UsernamePasswordConfig) ServicePrincipalToken() (*adal.ServicePrincipalToken, error) {
	oauthConfig, err := adal.NewOAuthConfig(ups.AADEndpoint, ups.TenantID)
	if err != nil {
		return nil, err
	}
	return adal.NewServicePrincipalTokenFromUsernamePassword(*oauthConfig, ups.ClientID, ups.Username, ups.Password, ups.Resource)
}

// Authorizer gets the authorizer from a username and a password.
func (ups UsernamePasswordConfig) Authorizer() (autorest.Authorizer, error) {
	spToken, err := ups.ServicePrincipalToken()
	if err != nil {
		return nil, fmt.Errorf("failed to get oauth token from username and password auth: %v", err)
	}
	return autorest.NewBearerAuthorizer(spToken), nil
}

// MSIConfig provides the options to get a bearer authorizer through MSI.
type MSIConfig struct {
	Resource string
	ClientID string
}

// ServicePrincipalToken creates a ServicePrincipalToken from MSI.
func (mc MSIConfig) ServicePrincipalToken() (*adal.ServicePrincipalToken, error) {
	spToken, err := adal.NewServicePrincipalTokenFromManagedIdentity(mc.Resource, &adal.ManagedIdentityOptions{
		ClientID: mc.ClientID,
	})
	if err != nil {
		return nil, fmt.Errorf("failed to get oauth token from MSI: %v", err)
	}
	return spToken, nil
}

// Authorizer gets the authorizer from MSI.
func (mc MSIConfig) Authorizer() (autorest.Authorizer, error) {
	spToken, err := mc.ServicePrincipalToken()
	if err != nil {
		return nil, err
	}

	return autorest.NewBearerAuthorizer(spToken), nil
}
