1
0
mirror of https://github.com/go-acme/lego.git synced 2024-12-26 03:09:37 +02:00

azuredns: allow oidc authentication (#2036)

Co-authored-by: Fernandez Ludovic <ldez@users.noreply.github.com>
This commit is contained in:
pchanvallon 2023-10-30 18:26:35 +01:00 committed by GitHub
parent c9ff534e39
commit 4f242c93e6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 168 additions and 16 deletions

View File

@ -210,6 +210,11 @@ The generated token will be cached by default in the `~/.azure` folder.
This authentication method can be specificaly used by setting the `AZURE_AUTH_METHOD` environment variable to `cli`. This authentication method can be specificaly used by setting the `AZURE_AUTH_METHOD` environment variable to `cli`.
### Open ID Connect
Open ID Connect is a mechanism that establish a trust relationship between a running environment and the Azure AD identity provider.
It can be enabled by setting the `AZURE_AUTH_METHOD` environment variable to `oidc`.

View File

@ -6,6 +6,7 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"net/http"
"strings" "strings"
"time" "time"
@ -31,12 +32,20 @@ const (
EnvClientID = envNamespace + "CLIENT_ID" EnvClientID = envNamespace + "CLIENT_ID"
EnvClientSecret = envNamespace + "CLIENT_SECRET" EnvClientSecret = envNamespace + "CLIENT_SECRET"
EnvOIDCToken = envNamespace + "OIDC_TOKEN"
EnvOIDCTokenFilePath = envNamespace + "OIDC_TOKEN_FILE_PATH"
EnvOIDCRequestURL = envNamespace + "OIDC_REQUEST_URL"
EnvOIDCRequestToken = envNamespace + "OIDC_REQUEST_TOKEN"
EnvAuthMethod = envNamespace + "AUTH_METHOD" EnvAuthMethod = envNamespace + "AUTH_METHOD"
EnvAuthMSITimeout = envNamespace + "AUTH_MSI_TIMEOUT" EnvAuthMSITimeout = envNamespace + "AUTH_MSI_TIMEOUT"
EnvTTL = envNamespace + "TTL" EnvTTL = envNamespace + "TTL"
EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT"
EnvPollingInterval = envNamespace + "POLLING_INTERVAL" EnvPollingInterval = envNamespace + "POLLING_INTERVAL"
EnvGitHubOIDCRequestURL = "ACTIONS_ID_TOKEN_REQUEST_URL"
EnvGitHubOIDCRequestToken = "ACTIONS_ID_TOKEN_REQUEST_TOKEN"
) )
// Config is used to configure the creation of the DNSProvider. // Config is used to configure the creation of the DNSProvider.
@ -52,12 +61,18 @@ type Config struct {
ClientSecret string ClientSecret string
TenantID string TenantID string
OIDCToken string
OIDCTokenFilePath string
OIDCRequestURL string
OIDCRequestToken string
AuthMethod string AuthMethod string
AuthMSITimeout time.Duration AuthMSITimeout time.Duration
PropagationTimeout time.Duration PropagationTimeout time.Duration
PollingInterval time.Duration PollingInterval time.Duration
TTL int TTL int
HTTPClient *http.Client
} }
// NewDefaultConfig returns a default configuration for the DNSProvider. // NewDefaultConfig returns a default configuration for the DNSProvider.
@ -103,6 +118,17 @@ func NewDNSProvider() (*DNSProvider, error) {
config.ClientSecret = env.GetOrFile(EnvClientSecret) config.ClientSecret = env.GetOrFile(EnvClientSecret)
config.TenantID = env.GetOrFile(EnvTenantID) config.TenantID = env.GetOrFile(EnvTenantID)
config.OIDCToken = env.GetOrFile(EnvOIDCToken)
config.OIDCTokenFilePath = env.GetOrFile(EnvOIDCTokenFilePath)
oidcValues, _ := env.GetWithFallback(
[]string{EnvOIDCRequestURL, EnvGitHubOIDCRequestURL},
[]string{EnvOIDCRequestToken, EnvGitHubOIDCRequestToken},
)
config.OIDCRequestURL = oidcValues[EnvOIDCRequestURL]
config.OIDCRequestToken = oidcValues[EnvOIDCRequestToken]
config.AuthMethod = env.GetOrFile(EnvAuthMethod) config.AuthMethod = env.GetOrFile(EnvAuthMethod)
config.AuthMSITimeout = env.GetOrDefaultSecond(EnvAuthMSITimeout, 2*time.Second) config.AuthMSITimeout = env.GetOrDefaultSecond(EnvAuthMSITimeout, 2*time.Second)
@ -115,6 +141,10 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
return nil, errors.New("azuredns: the configuration of the DNS provider is nil") return nil, errors.New("azuredns: the configuration of the DNS provider is nil")
} }
if config.HTTPClient == nil {
config.HTTPClient = &http.Client{Timeout: 5 * time.Second}
}
credentials, err := getCredentials(config) credentials, err := getCredentials(config)
if err != nil { if err != nil {
return nil, fmt.Errorf("azuredns: Unable to retrieve valid credentials: %w", err) return nil, fmt.Errorf("azuredns: Unable to retrieve valid credentials: %w", err)
@ -144,6 +174,22 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
return &DNSProvider{provider: dnsProvider}, nil return &DNSProvider{provider: dnsProvider}, nil
} }
// Timeout returns the timeout and interval to use when checking for DNS propagation.
// Adjusting here to cope with spikes in propagation times.
func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
return d.provider.Timeout()
}
// Present creates a TXT record to fulfill the dns-01 challenge.
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
return d.provider.Present(domain, token, keyAuth)
}
// CleanUp removes the TXT record matching the specified parameters.
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
return d.provider.CleanUp(domain, token, keyAuth)
}
func getCredentials(config *Config) (azcore.TokenCredential, error) { func getCredentials(config *Config) (azcore.TokenCredential, error) {
clientOptions := azcore.ClientOptions{Cloud: config.Environment} clientOptions := azcore.ClientOptions{Cloud: config.Environment}
@ -170,27 +216,19 @@ func getCredentials(config *Config) (azcore.TokenCredential, error) {
case "cli": case "cli":
return azidentity.NewAzureCLICredential(nil) return azidentity.NewAzureCLICredential(nil)
case "oidc":
err := checkOIDCConfig(config)
if err != nil {
return nil, err
}
return azidentity.NewClientAssertionCredential(config.TenantID, config.ClientID, getOIDCAssertion(config), &azidentity.ClientAssertionCredentialOptions{ClientOptions: clientOptions})
default: default:
return azidentity.NewDefaultAzureCredential(&azidentity.DefaultAzureCredentialOptions{ClientOptions: clientOptions}) return azidentity.NewDefaultAzureCredential(&azidentity.DefaultAzureCredentialOptions{ClientOptions: clientOptions})
} }
} }
// Timeout returns the timeout and interval to use when checking for DNS propagation.
// Adjusting here to cope with spikes in propagation times.
func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
return d.provider.Timeout()
}
// Present creates a TXT record to fulfill the dns-01 challenge.
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
return d.provider.Present(domain, token, keyAuth)
}
// CleanUp removes the TXT record matching the specified parameters.
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
return d.provider.CleanUp(domain, token, keyAuth)
}
// timeoutTokenCredential wraps a TokenCredential to add a timeout. // timeoutTokenCredential wraps a TokenCredential to add a timeout.
type timeoutTokenCredential struct { type timeoutTokenCredential struct {
cred azcore.TokenCredential cred azcore.TokenCredential

View File

@ -156,6 +156,11 @@ The generated token will be cached by default in the `~/.azure` folder.
This authentication method can be specificaly used by setting the `AZURE_AUTH_METHOD` environment variable to `cli`. This authentication method can be specificaly used by setting the `AZURE_AUTH_METHOD` environment variable to `cli`.
### Open ID Connect
Open ID Connect is a mechanism that establish a trust relationship between a running environment and the Azure AD identity provider.
It can be enabled by setting the `AZURE_AUTH_METHOD` environment variable to `oidc`.
''' '''
[Configuration] [Configuration]

View File

@ -0,0 +1,104 @@
package azuredns
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"os"
"strings"
)
func checkOIDCConfig(config *Config) error {
if config.TenantID == "" {
return fmt.Errorf("azuredns: TenantID is missing")
}
if config.ClientID == "" {
return fmt.Errorf("azuredns: ClientID is missing")
}
if config.OIDCToken == "" && config.OIDCTokenFilePath == "" && (config.OIDCRequestURL == "" || config.OIDCRequestToken == "") {
return fmt.Errorf("azuredns: OIDCToken, OIDCTokenFilePath or OIDCRequestURL and OIDCRequestToken must be set")
}
return nil
}
func getOIDCAssertion(config *Config) func(ctx context.Context) (string, error) {
return func(ctx context.Context) (string, error) {
var token string
if config.OIDCToken != "" {
token = strings.TrimSpace(config.OIDCToken)
}
if config.OIDCTokenFilePath != "" {
fileTokenRaw, err := os.ReadFile(config.OIDCTokenFilePath)
if err != nil {
return "", fmt.Errorf("azuredns: error retrieving token file with path %s: %w", config.OIDCTokenFilePath, err)
}
fileToken := strings.TrimSpace(string(fileTokenRaw))
if config.OIDCToken != fileToken {
return "", fmt.Errorf("azuredns: token file with path %s does not match token from environment variable", config.OIDCTokenFilePath)
}
token = fileToken
}
if token == "" && config.OIDCRequestURL != "" && config.OIDCRequestToken != "" {
return getOIDCToken(config)
}
return token, nil
}
}
func getOIDCToken(config *Config) (string, error) {
req, err := http.NewRequest(http.MethodGet, config.OIDCRequestURL, http.NoBody)
if err != nil {
return "", fmt.Errorf("azuredns: failed to build OIDC request: %w", err)
}
query, err := url.ParseQuery(req.URL.RawQuery)
if err != nil {
return "", fmt.Errorf("azuredns: cannot parse OIDC request URL query")
}
if query.Get("audience") == "" {
query.Set("audience", "api://AzureADTokenExchange")
req.URL.RawQuery = query.Encode()
}
req.Header.Set("Accept", "application/json")
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", config.OIDCRequestToken))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
resp, err := config.HTTPClient.Do(req)
if err != nil {
return "", fmt.Errorf("azuredns: cannot request OIDC token: %w", err)
}
defer func() { _ = resp.Body.Close() }()
body, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
if err != nil {
return "", fmt.Errorf("azuredns: cannot parse OIDC token response: %w", err)
}
if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusNoContent {
return "", fmt.Errorf("azuredns: OIDC token request received HTTP status %d with response: %s", resp.StatusCode, body)
}
var returnedToken struct {
Count int `json:"count"`
Value string `json:"value"`
}
if err := json.Unmarshal(body, &returnedToken); err != nil {
return "", fmt.Errorf("azuredns: cannot unmarshal OIDC token response: %w", err)
}
return returnedToken.Value, nil
}