Repository: go-acme/lego
Pull request #2173 (2024-05-05 18:34:32)

ovh: add OAuth2 authentication
## Description

The latest go-ovh version allows authentication using OAuth2 client_id and client_secret. Which in turn can be provisioned using the OVH terraform provider thus enabling everything to be configured as code.

## Note

I'm not usually writing code in Go so I'm not sure if everything is idiomatic. Please advice if something needs to be changed to be more in line with the rest of the code.


Patch:
diff --git a/cmd/zz_gen_cmd_dnshelp.go b/cmd/zz_gen_cmd_dnshelp.go
index d28400fd..fda2a845 100644
--- a/cmd/zz_gen_cmd_dnshelp.go
+++ b/cmd/zz_gen_cmd_dnshelp.go
@@ -2099,9 +2099,11 @@ func displayDNSHelp(w io.Writer, name string) error {
 		ew.writeln()
 
 		ew.writeln(`Credentials:`)
-		ew.writeln(`	- "OVH_APPLICATION_KEY":	Application key`)
-		ew.writeln(`	- "OVH_APPLICATION_SECRET":	Application secret`)
-		ew.writeln(`	- "OVH_CONSUMER_KEY":	Consumer key`)
+		ew.writeln(`	- "OVH_APPLICATION_KEY":	Application key (Application Key authentication)`)
+		ew.writeln(`	- "OVH_APPLICATION_SECRET":	Application secret (Application Key authentication)`)
+		ew.writeln(`	- "OVH_CLIENT_ID":	Client ID (OAuth2)`)
+		ew.writeln(`	- "OVH_CLIENT_SECRET":	Client secret (OAuth2)`)
+		ew.writeln(`	- "OVH_CONSUMER_KEY":	Consumer key (Application Key authentication)`)
 		ew.writeln(`	- "OVH_ENDPOINT":	Endpoint URL (ovh-eu or ovh-ca)`)
 		ew.writeln()
 
diff --git a/docs/content/dns/zz_gen_ovh.md b/docs/content/dns/zz_gen_ovh.md
index 5d7e2317..724fb0cb 100644
--- a/docs/content/dns/zz_gen_ovh.md
+++ b/docs/content/dns/zz_gen_ovh.md
@@ -26,11 +26,20 @@ Configuration for [OVH](https://www.ovh.com/).
 Here is an example bash command using the OVH provider:
 
 ```bash
+# Application Key authentication:
+
 OVH_APPLICATION_KEY=1234567898765432 \
 OVH_APPLICATION_SECRET=b9841238feb177a84330febba8a832089 \
 OVH_CONSUMER_KEY=256vfsd347245sdfg \
 OVH_ENDPOINT=ovh-eu \
 lego --email you@example.com --dns ovh --domains my.example.org run
+
+# Or OAuth2:
+
+OVH_CLIENT_ID=yyy \
+OVH_CLIENT_SECRET=xxx \
+OVH_ENDPOINT=ovh-eu \
+lego --email you@example.com --dns ovh --domains my.example.org run
 ```
 
 
@@ -40,9 +49,11 @@ lego --email you@example.com --dns ovh --domains my.example.org run
 
 | Environment Variable Name | Description |
 |-----------------------|-------------|
-| `OVH_APPLICATION_KEY` | Application key |
-| `OVH_APPLICATION_SECRET` | Application secret |
-| `OVH_CONSUMER_KEY` | Consumer key |
+| `OVH_APPLICATION_KEY` | Application key (Application Key authentication) |
+| `OVH_APPLICATION_SECRET` | Application secret (Application Key authentication) |
+| `OVH_CLIENT_ID` | Client ID (OAuth2) |
+| `OVH_CLIENT_SECRET` | Client secret (OAuth2) |
+| `OVH_CONSUMER_KEY` | Consumer key (Application Key authentication) |
 | `OVH_ENDPOINT` | Endpoint URL (ovh-eu or ovh-ca) |
 
 The environment variable names can be suffixed by `_FILE` to reference a file instead of a value.
@@ -82,6 +93,22 @@ When requesting the consumer key, the following configuration can be used to def
 }
 ```
 
+## OAuth2 Client Credentials
+
+Another method for authentication is by using OAuth2 client credentials.
+
+An IAM policy and service account can be created by following the [OVH guide](https://help.ovhcloud.com/csm/en-manage-service-account?id=kb_article_view&sysparm_article=KB0059343).
+
+Following IAM policies need to be authorized for the affected domain:
+
+* dnsZone:apiovh:record/create
+* dnsZone:apiovh:record/delete
+* dnsZone:apiovh:refresh
+
+## Important Note
+
+Both authentication methods cannot be used at the same time.
+
 
 
 ## More information
diff --git a/go.mod b/go.mod
index cbfea1bc..f770fe42 100644
--- a/go.mod
+++ b/go.mod
@@ -59,7 +59,7 @@ require (
 	github.com/nrdcg/porkbun v0.3.0
 	github.com/nzdjb/go-metaname v1.0.0
 	github.com/oracle/oci-go-sdk/v65 v65.63.1
-	github.com/ovh/go-ovh v1.4.3
+	github.com/ovh/go-ovh v1.5.1
 	github.com/pquerna/otp v1.4.0
 	github.com/rainycape/memcache v0.0.0-20150622160815-1031fa0ce2f2
 	github.com/sacloud/api-client-go v0.2.10
diff --git a/go.sum b/go.sum
index 3f8cdfa0..277c8f8e 100644
--- a/go.sum
+++ b/go.sum
@@ -524,8 +524,8 @@ github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b h1:Ff
 github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b/go.mod h1:AC62GU6hc0BrNm+9RK9VSiwa/EUe1bkIeFORAMcHvJU=
 github.com/oracle/oci-go-sdk/v65 v65.63.1 h1:dYL7sk9L1+C9LCmoq+zjPMNteuJJfk54YExq/4pV9xQ=
 github.com/oracle/oci-go-sdk/v65 v65.63.1/go.mod h1:IBEV9l1qBzUpo7zgGaRUhbB05BVfcDGYRFBCPlTcPp0=
-github.com/ovh/go-ovh v1.4.3 h1:Gs3V823zwTFpzgGLZNI6ILS4rmxZgJwJCz54Er9LwD0=
-github.com/ovh/go-ovh v1.4.3/go.mod h1:AkPXVtgwB6xlKblMjRKJJmjRp+ogrE7fz2lVgcQY8SY=
+github.com/ovh/go-ovh v1.5.1 h1:P8O+7H+NQuFK9P/j4sFW5C0fvSS2DnHYGPwdVCp45wI=
+github.com/ovh/go-ovh v1.5.1/go.mod h1:cTVDnl94z4tl8pP1uZ/8jlVxntjSIf09bNcQ5TJSC7c=
 github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
 github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
 github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
diff --git a/providers/dns/ovh/ovh.go b/providers/dns/ovh/ovh.go
index a98c2b0d..edc3e6e3 100644
--- a/providers/dns/ovh/ovh.go
+++ b/providers/dns/ovh/ovh.go
@@ -14,16 +14,14 @@ import (
 )
 
 // OVH API reference:       https://eu.api.ovh.com/
-// Create a Token:					https://eu.api.ovh.com/createToken/
+// Create a Token:          https://eu.api.ovh.com/createToken/
+// Create a OAuth2 client:   https://eu.api.ovh.com/console-preview/?section=%2Fme&branch=v1#post-/me/api/oauth2/client
 
 // Environment variables names.
 const (
 	envNamespace = "OVH_"
 
-	EnvEndpoint          = envNamespace + "ENDPOINT"
-	EnvApplicationKey    = envNamespace + "APPLICATION_KEY"
-	EnvApplicationSecret = envNamespace + "APPLICATION_SECRET"
-	EnvConsumerKey       = envNamespace + "CONSUMER_KEY"
+	EnvEndpoint = envNamespace + "ENDPOINT"
 
 	EnvTTL                = envNamespace + "TTL"
 	EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT"
@@ -31,6 +29,19 @@ const (
 	EnvHTTPTimeout        = envNamespace + "HTTP_TIMEOUT"
 )
 
+// Authenticate using application key.
+const (
+	EnvApplicationKey    = envNamespace + "APPLICATION_KEY"
+	EnvApplicationSecret = envNamespace + "APPLICATION_SECRET"
+	EnvConsumerKey       = envNamespace + "CONSUMER_KEY"
+)
+
+// Authenticate using OAuth2 client.
+const (
+	EnvClientID     = envNamespace + "CLIENT_ID"
+	EnvClientSecret = envNamespace + "CLIENT_SECRET"
+)
+
 // Record a DNS record.
 type Record struct {
 	ID        int64  `json:"id,omitempty"`
@@ -41,18 +52,32 @@ type Record struct {
 	Zone      string `json:"zone,omitempty"`
 }
 
+// OAuth2Config the OAuth2 specific configuration.
+type OAuth2Config struct {
+	ClientID     string
+	ClientSecret string
+}
+
 // Config is used to configure the creation of the DNSProvider.
 type Config struct {
-	APIEndpoint        string
-	ApplicationKey     string
-	ApplicationSecret  string
-	ConsumerKey        string
+	APIEndpoint string
+
+	ApplicationKey    string
+	ApplicationSecret string
+	ConsumerKey       string
+
+	OAuth2Config *OAuth2Config
+
 	PropagationTimeout time.Duration
 	PollingInterval    time.Duration
 	TTL                int
 	HTTPClient         *http.Client
 }
 
+func (c *Config) hasAppKeyAuth() bool {
+	return c.ApplicationKey != "" || c.ApplicationSecret != "" || c.ConsumerKey != ""
+}
+
 // NewDefaultConfig returns a default configuration for the DNSProvider.
 func NewDefaultConfig() *Config {
 	return &Config{
@@ -77,17 +102,11 @@ type DNSProvider struct {
 // Credentials must be passed in the environment variables:
 // OVH_ENDPOINT (must be either "ovh-eu" or "ovh-ca"), OVH_APPLICATION_KEY, OVH_APPLICATION_SECRET, OVH_CONSUMER_KEY.
 func NewDNSProvider() (*DNSProvider, error) {
-	values, err := env.Get(EnvEndpoint, EnvApplicationKey, EnvApplicationSecret, EnvConsumerKey)
+	config, err := createConfigFromEnvVars()
 	if err != nil {
 		return nil, fmt.Errorf("ovh: %w", err)
 	}
 
-	config := NewDefaultConfig()
-	config.APIEndpoint = values[EnvEndpoint]
-	config.ApplicationKey = values[EnvApplicationKey]
-	config.ApplicationSecret = values[EnvApplicationSecret]
-	config.ConsumerKey = values[EnvConsumerKey]
-
 	return NewDNSProviderConfig(config)
 }
 
@@ -97,16 +116,11 @@ func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
 		return nil, errors.New("ovh: the configuration of the DNS provider is nil")
 	}
 
-	if config.APIEndpoint == "" || config.ApplicationKey == "" || config.ApplicationSecret == "" || config.ConsumerKey == "" {
-		return nil, errors.New("ovh: credentials missing")
+	if config.OAuth2Config != nil && config.hasAppKeyAuth() {
+		return nil, errors.New("ovh: can't use both authentication systems (ApplicationKey and OAuth2)")
 	}
 
-	client, err := ovh.NewClient(
-		config.APIEndpoint,
-		config.ApplicationKey,
-		config.ApplicationSecret,
-		config.ConsumerKey,
-	)
+	client, err := newClient(config)
 	if err != nil {
 		return nil, fmt.Errorf("ovh: %w", err)
 	}
@@ -207,3 +221,95 @@ func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
 func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
 	return d.config.PropagationTimeout, d.config.PollingInterval
 }
+
+func createConfigFromEnvVars() (*Config, error) {
+	firstAppKeyEnvVar := findFirstValuedEnvVar(EnvApplicationKey, EnvApplicationSecret, EnvConsumerKey)
+	firstOAuth2EnvVar := findFirstValuedEnvVar(EnvClientID, EnvClientSecret)
+
+	if firstAppKeyEnvVar != "" && firstOAuth2EnvVar != "" {
+		return nil, fmt.Errorf("can't use both %s and %s at the same time", firstAppKeyEnvVar, firstOAuth2EnvVar)
+	}
+
+	config := NewDefaultConfig()
+
+	if firstOAuth2EnvVar != "" {
+		values, err := env.Get(EnvEndpoint, EnvClientID, EnvClientSecret)
+		if err != nil {
+			return nil, err
+		}
+
+		config.APIEndpoint = values[EnvEndpoint]
+		config.OAuth2Config = &OAuth2Config{
+			ClientID:     values[EnvClientID],
+			ClientSecret: values[EnvClientSecret],
+		}
+
+		return config, nil
+	}
+
+	values, err := env.Get(EnvEndpoint, EnvApplicationKey, EnvApplicationSecret, EnvConsumerKey)
+	if err != nil {
+		return nil, err
+	}
+
+	config.APIEndpoint = values[EnvEndpoint]
+
+	config.ApplicationKey = values[EnvApplicationKey]
+	config.ApplicationSecret = values[EnvApplicationSecret]
+	config.ConsumerKey = values[EnvConsumerKey]
+
+	return config, nil
+}
+
+func findFirstValuedEnvVar(envVars ...string) string {
+	for _, envVar := range envVars {
+		if env.GetOrFile(envVar) != "" {
+			return envVar
+		}
+	}
+
+	return ""
+}
+
+func newClient(config *Config) (*ovh.Client, error) {
+	if config.OAuth2Config == nil {
+		return newClientApplicationKey(config)
+	}
+
+	return newClientOAuth2(config)
+}
+
+func newClientApplicationKey(config *Config) (*ovh.Client, error) {
+	if config.APIEndpoint == "" || config.ApplicationKey == "" || config.ApplicationSecret == "" || config.ConsumerKey == "" {
+		return nil, errors.New("credentials are missing")
+	}
+
+	client, err := ovh.NewClient(
+		config.APIEndpoint,
+		config.ApplicationKey,
+		config.ApplicationSecret,
+		config.ConsumerKey,
+	)
+	if err != nil {
+		return nil, fmt.Errorf("new client: %w", err)
+	}
+
+	return client, nil
+}
+
+func newClientOAuth2(config *Config) (*ovh.Client, error) {
+	if config.APIEndpoint == "" || config.OAuth2Config.ClientID == "" || config.OAuth2Config.ClientSecret == "" {
+		return nil, errors.New("credentials are missing")
+	}
+
+	client, err := ovh.NewOAuth2Client(
+		config.APIEndpoint,
+		config.OAuth2Config.ClientID,
+		config.OAuth2Config.ClientSecret,
+	)
+	if err != nil {
+		return nil, fmt.Errorf("new OAuth2 client: %w", err)
+	}
+
+	return client, nil
+}
diff --git a/providers/dns/ovh/ovh.toml b/providers/dns/ovh/ovh.toml
index ddd51d2c..1597d280 100644
--- a/providers/dns/ovh/ovh.toml
+++ b/providers/dns/ovh/ovh.toml
@@ -5,11 +5,20 @@ Code = "ovh"
 Since = "v0.4.0"
 
 Example = '''
+# Application Key authentication:
+
 OVH_APPLICATION_KEY=1234567898765432 \
 OVH_APPLICATION_SECRET=b9841238feb177a84330febba8a832089 \
 OVH_CONSUMER_KEY=256vfsd347245sdfg \
 OVH_ENDPOINT=ovh-eu \
 lego --email you@example.com --dns ovh --domains my.example.org run
+
+# Or OAuth2:
+
+OVH_CLIENT_ID=yyy \
+OVH_CLIENT_SECRET=xxx \
+OVH_ENDPOINT=ovh-eu \
+lego --email you@example.com --dns ovh --domains my.example.org run
 '''
 
 Additional = '''
@@ -33,14 +42,32 @@ When requesting the consumer key, the following configuration can be used to def
   ]
 }
 ```
+
+## OAuth2 Client Credentials
+
+Another method for authentication is by using OAuth2 client credentials.
+
+An IAM policy and service account can be created by following the [OVH guide](https://help.ovhcloud.com/csm/en-manage-service-account?id=kb_article_view&sysparm_article=KB0059343).
+
+Following IAM policies need to be authorized for the affected domain:
+
+* dnsZone:apiovh:record/create
+* dnsZone:apiovh:record/delete
+* dnsZone:apiovh:refresh
+
+## Important Note
+
+Both authentication methods cannot be used at the same time.
 '''
 
 [Configuration]
   [Configuration.Credentials]
     OVH_ENDPOINT = "Endpoint URL (ovh-eu or ovh-ca)"
-    OVH_APPLICATION_KEY = "Application key"
-    OVH_APPLICATION_SECRET = "Application secret"
-    OVH_CONSUMER_KEY = "Consumer key"
+    OVH_APPLICATION_KEY = "Application key (Application Key authentication)"
+    OVH_APPLICATION_SECRET = "Application secret (Application Key authentication)"
+    OVH_CONSUMER_KEY = "Consumer key (Application Key authentication)"
+    OVH_CLIENT_ID = "Client ID (OAuth2)"
+    OVH_CLIENT_SECRET = "Client secret (OAuth2)"
   [Configuration.Additional]
     OVH_POLLING_INTERVAL = "Time between DNS propagation check"
     OVH_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation"


Test patch:
diff --git a/providers/dns/ovh/ovh_test.go b/providers/dns/ovh/ovh_test.go
index d38de1f2..cac88e90 100644
--- a/providers/dns/ovh/ovh_test.go
+++ b/providers/dns/ovh/ovh_test.go
@@ -14,7 +14,9 @@ var envTest = tester.NewEnvTest(
 	EnvEndpoint,
 	EnvApplicationKey,
 	EnvApplicationSecret,
-	EnvConsumerKey).
+	EnvConsumerKey,
+	EnvClientID,
+	EnvClientSecret).
 	WithDomain(envDomain)
 
 func TestNewDNSProvider(t *testing.T) {
@@ -24,7 +26,7 @@ func TestNewDNSProvider(t *testing.T) {
 		expected string
 	}{
 		{
-			desc: "success",
+			desc: "application key: success",
 			envVars: map[string]string{
 				EnvEndpoint:          "ovh-eu",
 				EnvApplicationKey:    "B",
@@ -33,17 +35,7 @@ func TestNewDNSProvider(t *testing.T) {
 			},
 		},
 		{
-			desc: "missing credentials",
-			envVars: map[string]string{
-				EnvEndpoint:          "",
-				EnvApplicationKey:    "",
-				EnvApplicationSecret: "",
-				EnvConsumerKey:       "",
-			},
-			expected: "ovh: some credentials information are missing: OVH_ENDPOINT,OVH_APPLICATION_KEY,OVH_APPLICATION_SECRET,OVH_CONSUMER_KEY",
-		},
-		{
-			desc: "missing endpoint",
+			desc: "application key: missing endpoint",
 			envVars: map[string]string{
 				EnvEndpoint:          "",
 				EnvApplicationKey:    "B",
@@ -53,17 +45,17 @@ func TestNewDNSProvider(t *testing.T) {
 			expected: "ovh: some credentials information are missing: OVH_ENDPOINT",
 		},
 		{
-			desc: "missing invalid endpoint",
+			desc: "application key: missing invalid endpoint",
 			envVars: map[string]string{
 				EnvEndpoint:          "foobar",
 				EnvApplicationKey:    "B",
 				EnvApplicationSecret: "C",
 				EnvConsumerKey:       "D",
 			},
-			expected: "ovh: unknown endpoint 'foobar', consider checking 'Endpoints' list of using an URL",
+			expected: "ovh: new client: unknown endpoint 'foobar', consider checking 'Endpoints' list or using an URL",
 		},
 		{
-			desc: "missing application key",
+			desc: "application key: missing application key",
 			envVars: map[string]string{
 				EnvEndpoint:          "ovh-eu",
 				EnvApplicationKey:    "",
@@ -73,7 +65,7 @@ func TestNewDNSProvider(t *testing.T) {
 			expected: "ovh: some credentials information are missing: OVH_APPLICATION_KEY",
 		},
 		{
-			desc: "missing application secret",
+			desc: "application key: missing application secret",
 			envVars: map[string]string{
 				EnvEndpoint:          "ovh-eu",
 				EnvApplicationKey:    "B",
@@ -83,7 +75,7 @@ func TestNewDNSProvider(t *testing.T) {
 			expected: "ovh: some credentials information are missing: OVH_APPLICATION_SECRET",
 		},
 		{
-			desc: "missing consumer key",
+			desc: "application key: missing consumer key",
 			envVars: map[string]string{
 				EnvEndpoint:          "ovh-eu",
 				EnvApplicationKey:    "B",
@@ -92,6 +84,56 @@ func TestNewDNSProvider(t *testing.T) {
 			},
 			expected: "ovh: some credentials information are missing: OVH_CONSUMER_KEY",
 		},
+		{
+			desc: "oauth2: success",
+			envVars: map[string]string{
+				EnvEndpoint:     "ovh-eu",
+				EnvClientID:     "E",
+				EnvClientSecret: "F",
+			},
+		},
+		{
+			desc: "oauth2: missing client secret",
+			envVars: map[string]string{
+				EnvEndpoint:     "ovh-eu",
+				EnvClientID:     "E",
+				EnvClientSecret: "",
+			},
+			expected: "ovh: some credentials information are missing: OVH_CLIENT_SECRET",
+		},
+		{
+			desc: "oauth2: missing client ID",
+			envVars: map[string]string{
+				EnvEndpoint:     "ovh-eu",
+				EnvClientID:     "",
+				EnvClientSecret: "F",
+			},
+			expected: "ovh: some credentials information are missing: OVH_CLIENT_ID",
+		},
+		{
+			desc: "missing credentials",
+			envVars: map[string]string{
+				EnvEndpoint:          "",
+				EnvApplicationKey:    "",
+				EnvApplicationSecret: "",
+				EnvConsumerKey:       "",
+				EnvClientID:          "",
+				EnvClientSecret:      "",
+			},
+			expected: "ovh: some credentials information are missing: OVH_ENDPOINT,OVH_APPLICATION_KEY,OVH_APPLICATION_SECRET,OVH_CONSUMER_KEY",
+		},
+		{
+			desc: "mixed auth",
+			envVars: map[string]string{
+				EnvEndpoint:          "ovh-eu",
+				EnvApplicationKey:    "B",
+				EnvApplicationSecret: "C",
+				EnvConsumerKey:       "D",
+				EnvClientID:          "E",
+				EnvClientSecret:      "F",
+			},
+			expected: "ovh: can't use both OVH_APPLICATION_KEY and OVH_CLIENT_ID at the same time",
+		},
 	}
 
 	for _, test := range testCases {
@@ -123,61 +165,111 @@ func TestNewDNSProviderConfig(t *testing.T) {
 		applicationKey    string
 		applicationSecret string
 		consumerKey       string
+		clientID          string
+		clientSecret      string
 		expected          string
 	}{
 		{
-			desc:              "success",
+			desc:              "application key: success",
 			apiEndpoint:       "ovh-eu",
 			applicationKey:    "B",
 			applicationSecret: "C",
 			consumerKey:       "D",
 		},
 		{
-			desc:     "missing credentials",
-			expected: "ovh: credentials missing",
-		},
-		{
-			desc:              "missing api endpoint",
+			desc:              "application key: missing api endpoint",
 			apiEndpoint:       "",
 			applicationKey:    "B",
 			applicationSecret: "C",
 			consumerKey:       "D",
-			expected:          "ovh: credentials missing",
+			expected:          "ovh: credentials are missing",
 		},
 		{
-			desc:              "missing invalid api endpoint",
+			desc:              "application key: invalid api endpoint",
 			apiEndpoint:       "foobar",
 			applicationKey:    "B",
 			applicationSecret: "C",
 			consumerKey:       "D",
-			expected:          "ovh: unknown endpoint 'foobar', consider checking 'Endpoints' list of using an URL",
+			expected:          "ovh: new client: unknown endpoint 'foobar', consider checking 'Endpoints' list or using an URL",
 		},
 		{
-			desc:              "missing application key",
+			desc:              "application key: missing application key",
 			apiEndpoint:       "ovh-eu",
 			applicationKey:    "",
 			applicationSecret: "C",
 			consumerKey:       "D",
-			expected:          "ovh: credentials missing",
+			expected:          "ovh: credentials are missing",
 		},
 		{
-			desc:              "missing application secret",
+			desc:              "application key: missing application secret",
 			apiEndpoint:       "ovh-eu",
 			applicationKey:    "B",
 			applicationSecret: "",
 			consumerKey:       "D",
-			expected:          "ovh: credentials missing",
+			expected:          "ovh: credentials are missing",
 		},
 		{
-			desc:              "missing consumer key",
+			desc:              "application key: missing consumer key",
 			apiEndpoint:       "ovh-eu",
 			applicationKey:    "B",
 			applicationSecret: "C",
 			consumerKey:       "",
-			expected:          "ovh: credentials missing",
+			expected:          "ovh: credentials are missing",
+		},
+		{
+			desc:         "oauth2: success",
+			apiEndpoint:  "ovh-eu",
+			clientID:     "B",
+			clientSecret: "C",
+		},
+		{
+			desc:         "oauth2: missing api endpoint",
+			apiEndpoint:  "",
+			clientID:     "B",
+			clientSecret: "C",
+			expected:     "ovh: credentials are missing",
+		},
+		{
+			desc:         "oauth2: invalid api endpoint",
+			apiEndpoint:  "foobar",
+			clientID:     "B",
+			clientSecret: "C",
+			expected:     "ovh: new OAuth2 client: unknown endpoint 'foobar', consider checking 'Endpoints' list or using an URL",
+		},
+		{
+			desc:         "oauth2: missing client id",
+			apiEndpoint:  "ovh-eu",
+			clientID:     "",
+			clientSecret: "C",
+			expected:     "ovh: credentials are missing",
+		},
+		{
+			desc:         "oauth2: missing client secret",
+			apiEndpoint:  "ovh-eu",
+			clientID:     "B",
+			clientSecret: "",
+			expected:     "ovh: credentials are missing",
+		},
+		{
+			desc:     "missing credentials",
+			expected: "ovh: credentials are missing",
+		},
+		{
+			desc:              "mixed auth",
+			apiEndpoint:       "ovh-eu",
+			applicationKey:    "B",
+			applicationSecret: "C",
+			consumerKey:       "D",
+			clientID:          "B",
+			clientSecret:      "C",
+			expected:          "ovh: can't use both authentication systems (ApplicationKey and OAuth2)",
 		},
 	}
 
+	// The OVH client use the same env vars than lego, so it requires to clean them.
+	defer envTest.RestoreEnv()
+	envTest.ClearEnv()
+
 	for _, test := range testCases {
 		t.Run(test.desc, func(t *testing.T) {
 			config := NewDefaultConfig()
@@ -186,6 +278,13 @@ func TestNewDNSProviderConfig(t *testing.T) {
 			config.ApplicationSecret = test.applicationSecret
 			config.ConsumerKey = test.consumerKey
 
+			if test.clientID != "" || test.clientSecret != "" {
+				config.OAuth2Config = &OAuth2Config{
+					ClientID:     test.clientID,
+					ClientSecret: test.clientSecret,
+				}
+			}
+
 			p, err := NewDNSProviderConfig(config)
 
 			if test.expected == "" {