components-contrib/internal/authentication/azure/auth.go

503 lines
16 KiB
Go

/*
Copyright 2021 The Dapr Authors
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.
*/
package azure
import (
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"errors"
"fmt"
"os"
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/cloud"
"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
"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/auth"
"golang.org/x/crypto/pkcs12"
"github.com/dapr/components-contrib/metadata"
)
// NewEnvironmentSettings returns a new EnvironmentSettings configured for a given Azure resource.
// TODO: Remove resourceName when "track1" SDK support is dropped.
func NewEnvironmentSettings(resourceName string, values map[string]string) (EnvironmentSettings, error) {
es := EnvironmentSettings{
Values: values,
}
azureEnv, err := es.GetAzureEnvironment()
if err != nil {
return es, err
}
es.AzureEnvironment = azureEnv
switch resourceName {
case "azure":
// Azure Resource Manager (management plane)
es.Resource = azureEnv.TokenAudience
case "keyvault":
// Azure Key Vault (data plane)
es.Resource = azureEnv.ResourceIdentifiers.KeyVault
case "storage":
// Azure Storage (data plane)
es.Resource = azureEnv.ResourceIdentifiers.Storage
case "cosmosdb":
// Azure Cosmos DB (data plane)
es.Resource = azureEnv.ResourceIdentifiers.CosmosDB
case "servicebus":
es.Resource = azureEnv.ResourceIdentifiers.ServiceBus
case "eventhubs":
// Azure EventHubs (data plane)
// For documentation https://docs.microsoft.com/en-us/azure/event-hubs/authorize-access-azure-active-directory#overview
// The resource name to request a token is https://eventhubs.azure.net/, and it's the same for all clouds/tenants.
// Kafka connection does not factor in here.
es.Resource = "https://eventhubs.azure.net"
case "signalr":
// Azure SignalR (data plane)
es.Resource = "https://signalr.azure.com"
case "appconfig":
// Azure App Configuration (data plane)
// For documentation https://docs.microsoft.com/en-us/azure/azure-app-configuration/rest-api-authentication-azure-ad#audience
// The resource name to request a token is https://azconfig.io
es.Resource = "https://azconfig.io"
default:
return es, errors.New("invalid resource name: " + resourceName)
}
return es, nil
}
// EnvironmentSettings hold settings to authenticate with Azure.
type EnvironmentSettings struct {
Values map[string]string
Resource string
AzureEnvironment *azure.Environment
}
// GetAzureEnvironment returns the Azure environment for a given name.
func (s EnvironmentSettings) GetAzureEnvironment() (*azure.Environment, error) {
envName, ok := s.GetEnvironment("AzureEnvironment")
if !ok || envName == "" {
envName = DefaultAzureEnvironment
}
env, err := azure.EnvironmentFromName(envName)
if err != nil {
return nil, err
}
return &env, err
}
// GetTokenCredential returns an azcore.TokenCredential retrieved from, in order:
// 1. Client credentials
// 2. Client certificate
// 3. MSI
// This is used by the newer ("track 2") Azure SDKs.
func (s EnvironmentSettings) GetTokenCredential() (azcore.TokenCredential, error) {
// Create a chain
var creds []azcore.TokenCredential
errMsg := ""
// 1. Client credentials
if c, e := s.GetClientCredentials(); e == nil {
cred, err := c.GetTokenCredential()
if err == nil {
creds = append(creds, cred)
} else {
errMsg += err.Error() + "\n"
}
}
// 2. Client certificate
if c, e := s.GetClientCert(); e == nil {
cred, err := c.GetTokenCredential()
if err == nil {
creds = append(creds, cred)
} else {
errMsg += err.Error() + "\n"
}
}
// 3. MSI
{
c := s.GetMSI()
cred, err := c.GetTokenCredential()
if err == nil {
creds = append(creds, cred)
} else {
errMsg += err.Error() + "\n"
}
}
if len(creds) == 0 {
return nil, fmt.Errorf("no suitable token provider for Azure AD; errors: %v", errMsg)
}
return azidentity.NewChainedTokenCredential(creds, nil)
}
// GetAuthorizer creates an Authorizer retrieved from, in order:
// 1. Client credentials
// 2. Client certificate
// 3. MSI
// This is used by the older Azure SDKs.
func (s EnvironmentSettings) GetAuthorizer() (autorest.Authorizer, error) {
spt, err := s.GetServicePrincipalToken()
if err != nil {
return nil, err
}
return autorest.NewBearerAuthorizer(spt), nil
}
// GetServicePrincipalToken returns a Service Principal Token retrieved from, in order:
// 1. Client credentials
// 2. Client certificate
// 3. MSI
// This is used by the older Azure SDKs.
func (s EnvironmentSettings) GetServicePrincipalToken() (*adal.ServicePrincipalToken, error) {
// 1. Client credentials
if c, e := s.GetClientCredentials(); e == nil {
return c.ServicePrincipalToken()
}
// 2. Client Certificate
if c, e := s.GetClientCert(); e == nil {
return c.ServicePrincipalToken()
}
// 3. MSI
return s.GetMSI().ServicePrincipalToken()
}
// GetClientCredentials creates a config object from the available client credentials.
// An error is returned if no certificate credentials are available.
func (s EnvironmentSettings) GetClientCredentials() (CredentialsConfig, error) {
azureEnv, err := s.GetAzureEnvironment()
if err != nil {
return CredentialsConfig{}, err
}
clientID, _ := s.GetEnvironment("ClientID")
clientSecret, _ := s.GetEnvironment("ClientSecret")
tenantID, _ := s.GetEnvironment("TenantID")
if clientID == "" || clientSecret == "" || tenantID == "" {
return CredentialsConfig{}, errors.New("parameters clientId, clientSecret, and tenantId must all be present")
}
authorizer := NewCredentialsConfig(clientID, tenantID, clientSecret, s.Resource, azureEnv)
return authorizer, nil
}
// GetClientCert creates a config object from the available certificate credentials.
// An error is returned if no certificate credentials are available.
func (s EnvironmentSettings) GetClientCert() (CertConfig, error) {
azureEnv, err := s.GetAzureEnvironment()
if err != nil {
return CertConfig{}, err
}
certFilePath, certFilePathPresent := s.GetEnvironment("CertificateFile")
certBytes, certBytesPresent := s.GetEnvironment("Certificate")
certPassword, _ := s.GetEnvironment("CertificatePassword")
clientID, _ := s.GetEnvironment("ClientID")
tenantID, _ := s.GetEnvironment("TenantID")
if !certFilePathPresent && !certBytesPresent {
return CertConfig{}, fmt.Errorf("missing client certificate")
}
authorizer := NewCertConfig(clientID, tenantID, certFilePath, []byte(certBytes), certPassword, s.Resource, azureEnv)
return authorizer, nil
}
// GetMSI creates a MSI config object from the available client ID.
func (s EnvironmentSettings) GetMSI() MSIConfig {
config := NewMSIConfig(s.Resource)
// This is optional and it's ok if value is empty
config.ClientID, _ = s.GetEnvironment("ClientID")
return config
}
// CredentialsConfig provides the options to get a bearer authorizer from client credentials.
type CredentialsConfig struct {
*auth.ClientCredentialsConfig
}
// NewCredentialsConfig creates an CredentialsConfig object configured to obtain an Authorizer through Client Credentials.
func NewCredentialsConfig(clientID string, tenantID string, clientSecret string, resource string, env *azure.Environment) CredentialsConfig {
return CredentialsConfig{
&auth.ClientCredentialsConfig{
ClientSecret: clientSecret,
ClientID: clientID,
TenantID: tenantID,
Resource: resource,
AADEndpoint: env.ActiveDirectoryEndpoint,
},
}
}
// ServicePrincipalToken gets a ServicePrincipalToken object from the credentials.
func (c CredentialsConfig) ServicePrincipalToken() (*adal.ServicePrincipalToken, error) {
oauthConfig, err := adal.NewOAuthConfig(c.AADEndpoint, c.TenantID)
if err != nil {
return nil, err
}
return adal.NewServicePrincipalToken(*oauthConfig, c.ClientID, c.ClientSecret, c.Resource)
}
// GetTokenCredential returns the azcore.TokenCredential object from the credentials.
func (c CredentialsConfig) GetTokenCredential() (token azcore.TokenCredential, err error) {
return azidentity.NewClientSecretCredential(c.TenantID, c.ClientID, c.ClientSecret, &azidentity.ClientSecretCredentialOptions{
ClientOptions: azcore.ClientOptions{
Cloud: cloud.Configuration{
ActiveDirectoryAuthorityHost: c.AADEndpoint,
},
},
})
}
// CertConfig provides the options to get a bearer authorizer from a client certificate.
type CertConfig struct {
*auth.ClientCertificateConfig
CertificateData []byte
}
// NewCertConfig creates an CertConfig object configured to obtain an Authorizer through Client Credentials, using a certificate.
func NewCertConfig(clientID string, tenantID string, certificatePath string, certificateBytes []byte, certificatePassword string, resource string, env *azure.Environment) CertConfig {
return CertConfig{
&auth.ClientCertificateConfig{
CertificatePath: certificatePath,
CertificatePassword: certificatePassword,
ClientID: clientID,
TenantID: tenantID,
Resource: resource,
AADEndpoint: env.ActiveDirectoryEndpoint,
},
certificateBytes,
}
}
// ServicePrincipalToken gets a ServicePrincipalToken object from client certificate.
func (c CertConfig) ServicePrincipalToken() (*adal.ServicePrincipalToken, error) {
if c.ClientCertificateConfig.CertificatePath != "" {
// in standalone mode, component yaml will pass cert path
return c.ClientCertificateConfig.ServicePrincipalToken()
} else if len(c.CertificateData) > 0 {
// in kubernetes mode, runtime will get the secret from K8S secret store and pass byte array
return c.ServicePrincipalTokenByCertBytes()
}
return nil, fmt.Errorf("certificate is not given")
}
// ServicePrincipalTokenByCertBytes gets the service principal token by CertificateBytes.
func (c CertConfig) ServicePrincipalTokenByCertBytes() (*adal.ServicePrincipalToken, error) {
oauthConfig, err := adal.NewOAuthConfig(c.AADEndpoint, c.TenantID)
if err != nil {
return nil, err
}
certificate, rsaPrivateKey, err := c.decodeCertificate(c.CertificateData, c.CertificatePassword)
if err != nil {
return nil, fmt.Errorf("failed to decode pkcs12 certificate while creating spt: %v", err)
}
return adal.NewServicePrincipalTokenFromCertificate(*oauthConfig, c.ClientID, certificate, rsaPrivateKey, c.Resource)
}
// GetTokenCredential returns the azcore.TokenCredential object from client certificate.
func (c CertConfig) GetTokenCredential() (token azcore.TokenCredential, err error) {
ccc := c.ClientCertificateConfig
// Certificate data - may be empty here
data := c.CertificateData
// If we have a certificate path, load it
if c.ClientCertificateConfig.CertificatePath != "" {
var errB error
data, errB = os.ReadFile(ccc.CertificatePath)
if errB != nil {
return nil, fmt.Errorf("failed to read the certificate file (%s): %v", ccc.CertificatePath, errB)
}
}
if len(data) == 0 {
return nil, fmt.Errorf("certificate is not given")
}
// Decode the certificate
cert, key, err := c.decodeCertificate(data, c.CertificatePassword)
if err != nil || cert == nil {
return nil, fmt.Errorf("failed to decode pkcs12 certificate while creating spt: %v", err)
}
// Create the azcore.TokenCredential object
certs := []*x509.Certificate{cert}
opts := &azidentity.ClientCertificateCredentialOptions{
ClientOptions: azcore.ClientOptions{
Cloud: cloud.Configuration{
ActiveDirectoryAuthorityHost: c.AADEndpoint,
},
},
}
return azidentity.NewClientCertificateCredential(c.TenantID, c.ClientID, certs, key, opts)
}
// Decode a certificate, either as a PKCS#12 (PFX) bundle, or as a single file with both certificate and key encoded in PEM blocks.
// The password is only used for PFX (and could be empty).
func (c CertConfig) decodeCertificate(data []byte, password string) (certificate *x509.Certificate, privateKey *rsa.PrivateKey, err error) {
// First, try to decode the certificate as PKCS#12
certificate, privateKey, err = c.decodePkcs12(data, password)
if err == nil && certificate != nil {
return certificate, privateKey, nil
}
// If it failed, try decoding as PEM
certificate, privateKey, err = c.decodePEM(data)
if err == nil && certificate != nil {
return certificate, privateKey, nil
}
return nil, nil, errors.New("certificate is not valid")
}
func (c CertConfig) decodePkcs12(pkcs []byte, password string) (*x509.Certificate, *rsa.PrivateKey, error) {
privateKey, certificate, err := pkcs12.Decode(pkcs, password)
if err != nil {
return nil, nil, err
}
rsaPrivateKey, isRsaKey := privateKey.(*rsa.PrivateKey)
if !isRsaKey {
return nil, nil, fmt.Errorf("PKCS#12 certificate must contain an RSA private key")
}
return certificate, rsaPrivateKey, nil
}
func (c CertConfig) decodePEM(data []byte) (certificate *x509.Certificate, privateKey *rsa.PrivateKey, err error) {
// We should have 2 PEM blocks: a certificate and a key
var (
block *pem.Block
parsedKey any
ok bool
)
for i := 0; i < 2; i++ {
block, data = pem.Decode(data)
if block == nil {
break
}
switch block.Type {
case "CERTIFICATE":
// If we already have a certificate decoded, return an error
if certificate != nil {
return nil, nil, errors.New("invalid certificate")
}
certificate, err = x509.ParseCertificate(block.Bytes)
if err != nil {
return nil, nil, err
}
case "PRIVATE KEY": // PKCS#8
// If we already have a key decoded, return an error
if privateKey != nil {
return nil, nil, errors.New("invalid certificate")
}
parsedKey, err = x509.ParsePKCS8PrivateKey(block.Bytes)
if err != nil {
return nil, nil, err
}
privateKey, ok = parsedKey.(*rsa.PrivateKey)
if !ok || privateKey == nil {
return nil, nil, fmt.Errorf("certificate must contain an RSA private key")
}
case "RSA PRIVATE KEY": // PKCS#1
// If we already have a key decoded, return an error
if privateKey != nil {
return nil, nil, errors.New("invalid certificate")
}
parsedKey, err = x509.ParsePKCS1PrivateKey(block.Bytes)
if err != nil {
return nil, nil, err
}
privateKey, ok = parsedKey.(*rsa.PrivateKey)
if !ok || privateKey == nil {
return nil, nil, fmt.Errorf("certificate must contain an RSA private key")
}
}
}
// We should have both a private key and a certificate
if privateKey == nil || certificate == nil {
return nil, nil, errors.New("invalid certificate")
}
return certificate, privateKey, nil
}
// MSIConfig provides the options to get a bearer authorizer through MSI.
type MSIConfig struct {
Resource string
ClientID string
}
// NewMSIConfig creates an MSIConfig object configured to obtain an Authorizer through MSI.
func NewMSIConfig(resource string) MSIConfig {
return MSIConfig{
Resource: resource,
}
}
// ServicePrincipalToken gets the ServicePrincipalToken object from MSI.
func (c MSIConfig) ServicePrincipalToken() (*adal.ServicePrincipalToken, error) {
msiEndpoint, err := adal.GetMSIEndpoint()
if err != nil {
return nil, err
}
var spToken *adal.ServicePrincipalToken
if c.ClientID == "" {
spToken, err = adal.NewServicePrincipalTokenFromMSI(msiEndpoint, c.Resource)
if err != nil {
return nil, fmt.Errorf("failed to get oauth token from MSI: %v", err)
}
} else {
spToken, err = adal.NewServicePrincipalTokenFromMSIWithUserAssignedID(msiEndpoint, c.Resource, c.ClientID)
if err != nil {
return nil, fmt.Errorf("failed to get oauth token from MSI for user assigned identity: %v", err)
}
}
return spToken, nil
}
// GetTokenCredential returns the azcore.TokenCredential object from MSI.
func (c MSIConfig) GetTokenCredential() (token azcore.TokenCredential, err error) {
opts := &azidentity.ManagedIdentityCredentialOptions{}
if c.ClientID != "" {
opts.ID = azidentity.ClientID(c.ClientID)
}
return azidentity.NewManagedIdentityCredential(opts)
}
// GetAzureEnvironment returns the Azure environment for a given name, supporting aliases too.
func (s EnvironmentSettings) GetEnvironment(key string) (val string, ok bool) {
return metadata.GetMetadataProperty(s.Values, MetadataKeys[key]...)
}