Skip to content

Commit 4f242c9

Browse files
pchanvallonldez
andauthored
azuredns: allow oidc authentication (#2036)
Co-authored-by: Fernandez Ludovic <[email protected]>
1 parent c9ff534 commit 4f242c9

File tree

4 files changed

+168
-16
lines changed

4 files changed

+168
-16
lines changed

docs/content/dns/zz_gen_azuredns.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,11 @@ The generated token will be cached by default in the `~/.azure` folder.
210210

211211
This authentication method can be specificaly used by setting the `AZURE_AUTH_METHOD` environment variable to `cli`.
212212

213+
### Open ID Connect
214+
215+
Open ID Connect is a mechanism that establish a trust relationship between a running environment and the Azure AD identity provider.
216+
It can be enabled by setting the `AZURE_AUTH_METHOD` environment variable to `oidc`.
217+
213218

214219

215220

providers/dns/azuredns/azuredns.go

Lines changed: 54 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"context"
77
"errors"
88
"fmt"
9+
"net/http"
910
"strings"
1011
"time"
1112

@@ -31,12 +32,20 @@ const (
3132
EnvClientID = envNamespace + "CLIENT_ID"
3233
EnvClientSecret = envNamespace + "CLIENT_SECRET"
3334

35+
EnvOIDCToken = envNamespace + "OIDC_TOKEN"
36+
EnvOIDCTokenFilePath = envNamespace + "OIDC_TOKEN_FILE_PATH"
37+
EnvOIDCRequestURL = envNamespace + "OIDC_REQUEST_URL"
38+
EnvOIDCRequestToken = envNamespace + "OIDC_REQUEST_TOKEN"
39+
3440
EnvAuthMethod = envNamespace + "AUTH_METHOD"
3541
EnvAuthMSITimeout = envNamespace + "AUTH_MSI_TIMEOUT"
3642

3743
EnvTTL = envNamespace + "TTL"
3844
EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT"
3945
EnvPollingInterval = envNamespace + "POLLING_INTERVAL"
46+
47+
EnvGitHubOIDCRequestURL = "ACTIONS_ID_TOKEN_REQUEST_URL"
48+
EnvGitHubOIDCRequestToken = "ACTIONS_ID_TOKEN_REQUEST_TOKEN"
4049
)
4150

4251
// Config is used to configure the creation of the DNSProvider.
@@ -52,12 +61,18 @@ type Config struct {
5261
ClientSecret string
5362
TenantID string
5463

64+
OIDCToken string
65+
OIDCTokenFilePath string
66+
OIDCRequestURL string
67+
OIDCRequestToken string
68+
5569
AuthMethod string
5670
AuthMSITimeout time.Duration
5771

5872
PropagationTimeout time.Duration
5973
PollingInterval time.Duration
6074
TTL int
75+
HTTPClient *http.Client
6176
}
6277

6378
// NewDefaultConfig returns a default configuration for the DNSProvider.
@@ -103,6 +118,17 @@ func NewDNSProvider() (*DNSProvider, error) {
103118
config.ClientSecret = env.GetOrFile(EnvClientSecret)
104119
config.TenantID = env.GetOrFile(EnvTenantID)
105120

121+
config.OIDCToken = env.GetOrFile(EnvOIDCToken)
122+
config.OIDCTokenFilePath = env.GetOrFile(EnvOIDCTokenFilePath)
123+
124+
oidcValues, _ := env.GetWithFallback(
125+
[]string{EnvOIDCRequestURL, EnvGitHubOIDCRequestURL},
126+
[]string{EnvOIDCRequestToken, EnvGitHubOIDCRequestToken},
127+
)
128+
129+
config.OIDCRequestURL = oidcValues[EnvOIDCRequestURL]
130+
config.OIDCRequestToken = oidcValues[EnvOIDCRequestToken]
131+
106132
config.AuthMethod = env.GetOrFile(EnvAuthMethod)
107133
config.AuthMSITimeout = env.GetOrDefaultSecond(EnvAuthMSITimeout, 2*time.Second)
108134

@@ -115,6 +141,10 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
115141
return nil, errors.New("azuredns: the configuration of the DNS provider is nil")
116142
}
117143

144+
if config.HTTPClient == nil {
145+
config.HTTPClient = &http.Client{Timeout: 5 * time.Second}
146+
}
147+
118148
credentials, err := getCredentials(config)
119149
if err != nil {
120150
return nil, fmt.Errorf("azuredns: Unable to retrieve valid credentials: %w", err)
@@ -144,6 +174,22 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
144174
return &DNSProvider{provider: dnsProvider}, nil
145175
}
146176

177+
// Timeout returns the timeout and interval to use when checking for DNS propagation.
178+
// Adjusting here to cope with spikes in propagation times.
179+
func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
180+
return d.provider.Timeout()
181+
}
182+
183+
// Present creates a TXT record to fulfill the dns-01 challenge.
184+
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
185+
return d.provider.Present(domain, token, keyAuth)
186+
}
187+
188+
// CleanUp removes the TXT record matching the specified parameters.
189+
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
190+
return d.provider.CleanUp(domain, token, keyAuth)
191+
}
192+
147193
func getCredentials(config *Config) (azcore.TokenCredential, error) {
148194
clientOptions := azcore.ClientOptions{Cloud: config.Environment}
149195

@@ -170,27 +216,19 @@ func getCredentials(config *Config) (azcore.TokenCredential, error) {
170216
case "cli":
171217
return azidentity.NewAzureCLICredential(nil)
172218

219+
case "oidc":
220+
err := checkOIDCConfig(config)
221+
if err != nil {
222+
return nil, err
223+
}
224+
225+
return azidentity.NewClientAssertionCredential(config.TenantID, config.ClientID, getOIDCAssertion(config), &azidentity.ClientAssertionCredentialOptions{ClientOptions: clientOptions})
226+
173227
default:
174228
return azidentity.NewDefaultAzureCredential(&azidentity.DefaultAzureCredentialOptions{ClientOptions: clientOptions})
175229
}
176230
}
177231

178-
// Timeout returns the timeout and interval to use when checking for DNS propagation.
179-
// Adjusting here to cope with spikes in propagation times.
180-
func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
181-
return d.provider.Timeout()
182-
}
183-
184-
// Present creates a TXT record to fulfill the dns-01 challenge.
185-
func (d *DNSProvider) Present(domain, token, keyAuth string) error {
186-
return d.provider.Present(domain, token, keyAuth)
187-
}
188-
189-
// CleanUp removes the TXT record matching the specified parameters.
190-
func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
191-
return d.provider.CleanUp(domain, token, keyAuth)
192-
}
193-
194232
// timeoutTokenCredential wraps a TokenCredential to add a timeout.
195233
type timeoutTokenCredential struct {
196234
cred azcore.TokenCredential

providers/dns/azuredns/azuredns.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,11 @@ The generated token will be cached by default in the `~/.azure` folder.
156156
157157
This authentication method can be specificaly used by setting the `AZURE_AUTH_METHOD` environment variable to `cli`.
158158
159+
### Open ID Connect
160+
161+
Open ID Connect is a mechanism that establish a trust relationship between a running environment and the Azure AD identity provider.
162+
It can be enabled by setting the `AZURE_AUTH_METHOD` environment variable to `oidc`.
163+
159164
'''
160165

161166
[Configuration]

providers/dns/azuredns/oidc.go

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
package azuredns
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
"io"
8+
"net/http"
9+
"net/url"
10+
"os"
11+
"strings"
12+
)
13+
14+
func checkOIDCConfig(config *Config) error {
15+
if config.TenantID == "" {
16+
return fmt.Errorf("azuredns: TenantID is missing")
17+
}
18+
19+
if config.ClientID == "" {
20+
return fmt.Errorf("azuredns: ClientID is missing")
21+
}
22+
23+
if config.OIDCToken == "" && config.OIDCTokenFilePath == "" && (config.OIDCRequestURL == "" || config.OIDCRequestToken == "") {
24+
return fmt.Errorf("azuredns: OIDCToken, OIDCTokenFilePath or OIDCRequestURL and OIDCRequestToken must be set")
25+
}
26+
27+
return nil
28+
}
29+
30+
func getOIDCAssertion(config *Config) func(ctx context.Context) (string, error) {
31+
return func(ctx context.Context) (string, error) {
32+
var token string
33+
if config.OIDCToken != "" {
34+
token = strings.TrimSpace(config.OIDCToken)
35+
}
36+
37+
if config.OIDCTokenFilePath != "" {
38+
fileTokenRaw, err := os.ReadFile(config.OIDCTokenFilePath)
39+
if err != nil {
40+
return "", fmt.Errorf("azuredns: error retrieving token file with path %s: %w", config.OIDCTokenFilePath, err)
41+
}
42+
43+
fileToken := strings.TrimSpace(string(fileTokenRaw))
44+
if config.OIDCToken != fileToken {
45+
return "", fmt.Errorf("azuredns: token file with path %s does not match token from environment variable", config.OIDCTokenFilePath)
46+
}
47+
48+
token = fileToken
49+
}
50+
51+
if token == "" && config.OIDCRequestURL != "" && config.OIDCRequestToken != "" {
52+
return getOIDCToken(config)
53+
}
54+
55+
return token, nil
56+
}
57+
}
58+
59+
func getOIDCToken(config *Config) (string, error) {
60+
req, err := http.NewRequest(http.MethodGet, config.OIDCRequestURL, http.NoBody)
61+
if err != nil {
62+
return "", fmt.Errorf("azuredns: failed to build OIDC request: %w", err)
63+
}
64+
65+
query, err := url.ParseQuery(req.URL.RawQuery)
66+
if err != nil {
67+
return "", fmt.Errorf("azuredns: cannot parse OIDC request URL query")
68+
}
69+
70+
if query.Get("audience") == "" {
71+
query.Set("audience", "api://AzureADTokenExchange")
72+
req.URL.RawQuery = query.Encode()
73+
}
74+
75+
req.Header.Set("Accept", "application/json")
76+
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", config.OIDCRequestToken))
77+
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
78+
79+
resp, err := config.HTTPClient.Do(req)
80+
if err != nil {
81+
return "", fmt.Errorf("azuredns: cannot request OIDC token: %w", err)
82+
}
83+
84+
defer func() { _ = resp.Body.Close() }()
85+
86+
body, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
87+
if err != nil {
88+
return "", fmt.Errorf("azuredns: cannot parse OIDC token response: %w", err)
89+
}
90+
91+
if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusNoContent {
92+
return "", fmt.Errorf("azuredns: OIDC token request received HTTP status %d with response: %s", resp.StatusCode, body)
93+
}
94+
95+
var returnedToken struct {
96+
Count int `json:"count"`
97+
Value string `json:"value"`
98+
}
99+
if err := json.Unmarshal(body, &returnedToken); err != nil {
100+
return "", fmt.Errorf("azuredns: cannot unmarshal OIDC token response: %w", err)
101+
}
102+
103+
return returnedToken.Value, nil
104+
}

0 commit comments

Comments
 (0)