Repository: go-acme/lego (language: go)

Issue:
INWX: Issue when using 2FA - Same 2FA code is used twice when Present returns non-error
### Welcome

- [X] Yes, I'm using a binary release within 2 latest releases.
- [X] Yes, I've searched similar issues on GitHub and didn't find any.
- [X] Yes, I've included all information below (version, config, etc).

### What did you expect to see?

i should've seen that lego is presenting letsencrypt the dns challenge and that the challenge gets removed.

The removal does not work though which results in not downloading the certificate.

### What did you see instead?

Output:
```
cleaning up failed: inwx: (2200) Authentication error. Reason: (INVALID) The confirmation code is invalid.
```
Unless you are lucky and your `Present` is called in one 30s timeframe and your `CleanUp` is called in the next 30s timeframe the same 2FA code gets used twice (which is not a valid case for 2FA and most systems seem to implement it correctly).

### How do you use lego?

Docker image

### Reproduction steps

* have a inwx account
* enable 2fa
* try to get a wildcard certificate:
```
docker run -v certs:/lego -w /lego -e INWX_USERNAME=a -e INWX_PASSWORD=b -e INWX_SHARED_SECRET=c  goacme/lego:v4.6.0 -m your-mail@gmail.com --dns inwx -d example.de -d '*.example.de' -a --path /lego --pem run
```

### Version of lego

```console
4.6.0
```


### Logs

<details>

```console
2022/03/20 17:34:21 [INFO] [example.de, *.example.de] acme: Obtaining bundled SAN certificate
2022/03/20 17:34:22 [INFO] [*.example.de] AuthURL: https://acme-v02.api.letsencrypt.org/acme/authz-v3/12345
2022/03/20 17:34:22 [INFO] [example.de] AuthURL: https://acme-v02.api.letsencrypt.org/acme/authz-v3/12345
2022/03/20 17:34:22 [INFO] [example.de] acme: authorization already valid; skipping challenge
2022/03/20 17:34:22 [INFO] [*.example.de] acme: use dns-01 solver
2022/03/20 17:34:22 [INFO] [*.example.de] acme: Preparing to solve DNS-01
2022/03/20 17:34:26 [INFO] [*.example.de] acme: Cleaning DNS-01 challenge
2022/03/20 17:34:29 [WARN] [*.example.de] acme: cleaning up failed: inwx: (2200) Authentication error. Reason: (INVALID) The confirmation code is invalid.
2022/03/20 17:34:30 [INFO] Skipping deactivating of valid auth: https://acme-v02.api.letsencrypt.org/acme/authz-v3/12345
2022/03/20 17:34:30 [INFO] Deactivating auth: https://acme-v02.api.letsencrypt.org/acme/authz-v3/12345
2022/03/20 17:34:30 Could not obtain certificates:
	error: one or more domains had a problem:
[*.example.de] [*.example.de] acme: error presenting token: inwx: (2200) Authentication error. Reason: (INVALID) The confirmation code is invalid.
```

</details>

### Go environment (if applicable)

_No response_


Discussion / hints:
I have the same issue.  Using traefik 2.7.1.
I have the same problem. Temporarily disabling 2FA support was the only solution for me...
inwx responded to my request and the only solution on their side is to create a second account that has access to the domains dns entries and where 2fa is disabled.

Currently (information is from 30th of march 2022) they do not even plan to implement a different way to authenticate, so that regular api driven applications can do actions on the users behalf.
Just moved my NS servers to cloudflare so I can have 2FA and DNS auth for LE. I’m not willing to have my main domain access only authenticated via one factor. Thanks for asking! 

`they do not even plan to implement a different way to authenticate, so that regular api driven applications can do actions on the users behalf. `

this also sounds like total bs to me, how does this break existing APIs? 
I am also affected (Lego 4.14.2 - the current version).  I have solved this through "a lot of retries" from my side in the past, but that has usually taken three or four attempts.

Even if the INWX token reuse detection is unusual, I am having trouble seeing why Lego needs to be affected by this?

https://github.com/go-acme/lego/blob/master/providers/dns/inwx/inwx.go

It seems that Lego is trying to authenticate with INWX multiple times?  Couldn't we authenticate only once instead and keep the session open until the entire ACME protocol is done?

(As a fallback, the other approach would be to wait for at least 30 seconds between generating two TOTP tokens.  It would make the challenge slower, but at least it would work reliably.)

Similar discussion with more background: https://github.com/StackExchange/dnscontrol/issues/848

Fix patch:
diff --git a/providers/dns/inwx/inwx.go b/providers/dns/inwx/inwx.go
index ab134b0c8e..ee569c9295 100644
--- a/providers/dns/inwx/inwx.go
+++ b/providers/dns/inwx/inwx.go
@@ -51,8 +51,9 @@ func NewDefaultConfig() *Config {
 
 // DNSProvider implements the challenge.Provider interface.
 type DNSProvider struct {
-	config *Config
-	client *goinwx.Client
+	config         *Config
+	client         *goinwx.Client
+	previousUnlock time.Time
 }
 
 // NewDNSProvider returns a DNSProvider instance configured for Dyn DNS.
@@ -202,10 +203,33 @@ func (d *DNSProvider) twoFactorAuth(info *goinwx.LoginResponse) error {
 		return errors.New("two-factor authentication but no shared secret is given")
 	}
 
+	// INWX forbids re-authentication with a previously used TAN.
+	// To avoid using the same TAN twice, we wait until the next TOTP period.
+	sleep := d.computeSleep(time.Now())
+	if sleep != 0 {
+		log.Infof("inwx: waiting %s for next TOTP token", sleep)
+		time.Sleep(sleep)
+	}
+
 	tan, err := totp.GenerateCode(d.config.SharedSecret, time.Now())
 	if err != nil {
 		return err
 	}
 
+	d.previousUnlock = time.Now()
+
 	return d.client.Account.Unlock(tan)
 }
+
+func (d *DNSProvider) computeSleep(now time.Time) time.Duration {
+	if d.previousUnlock.IsZero() {
+		return 0 * time.Second
+	}
+
+	endPeriod := d.previousUnlock.Add(30 * time.Second)
+	if endPeriod.After(now) {
+		return endPeriod.Sub(now)
+	}
+
+	return 0 * time.Second
+}


Test patch:
diff --git a/providers/dns/inwx/inwx_test.go b/providers/dns/inwx/inwx_test.go
index 14dbd4344c..8053c87919 100644
--- a/providers/dns/inwx/inwx_test.go
+++ b/providers/dns/inwx/inwx_test.go
@@ -2,8 +2,10 @@ package inwx
 
 import (
 	"testing"
+	"time"
 
 	"github.com/go-acme/lego/v4/platform/tester"
+	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 )
 
@@ -141,3 +143,45 @@ func TestLivePresentAndCleanup(t *testing.T) {
 	err = provider.CleanUp(envTest.GetDomain(), "", "123d==")
 	require.NoError(t, err)
 }
+
+func Test_computeSleep(t *testing.T) {
+	testCases := []struct {
+		desc     string
+		previous string
+		expected time.Duration
+	}{
+		{
+			desc:     "after 30s",
+			previous: "2024-01-01T06:29:20Z",
+			expected: 0 * time.Second,
+		},
+		{
+			desc:     "0s",
+			previous: "2024-01-01T06:29:30Z",
+			expected: 0 * time.Second,
+		},
+		{
+			desc:     "before 30s",
+			previous: "2024-01-01T06:29:50Z", // 10 s
+			expected: 20 * time.Second,
+		},
+	}
+
+	now, err := time.Parse(time.RFC3339, "2024-01-01T06:30:00Z")
+	require.NoError(t, err)
+
+	for _, test := range testCases {
+		test := test
+		t.Run(test.desc, func(t *testing.T) {
+			t.Parallel()
+
+			previous, err := time.Parse(time.RFC3339, test.previous)
+			require.NoError(t, err)
+
+			d := &DNSProvider{previousUnlock: previous}
+
+			sleep := d.computeSleep(now)
+			assert.Equal(t, test.expected, sleep)
+		})
+	}
+}