Skip to content

Commit 9d4c60e

Browse files
gnoackldez
andauthored
inwx: wait before generating new TOTP TANs (go-acme#2084)
Co-authored-by: Fernandez Ludovic <[email protected]>
1 parent c17f659 commit 9d4c60e

File tree

2 files changed

+70
-2
lines changed

2 files changed

+70
-2
lines changed

providers/dns/inwx/inwx.go

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,9 @@ func NewDefaultConfig() *Config {
5151

5252
// DNSProvider implements the challenge.Provider interface.
5353
type DNSProvider struct {
54-
config *Config
55-
client *goinwx.Client
54+
config *Config
55+
client *goinwx.Client
56+
previousUnlock time.Time
5657
}
5758

5859
// NewDNSProvider returns a DNSProvider instance configured for Dyn DNS.
@@ -202,10 +203,33 @@ func (d *DNSProvider) twoFactorAuth(info *goinwx.LoginResponse) error {
202203
return errors.New("two-factor authentication but no shared secret is given")
203204
}
204205

206+
// INWX forbids re-authentication with a previously used TAN.
207+
// To avoid using the same TAN twice, we wait until the next TOTP period.
208+
sleep := d.computeSleep(time.Now())
209+
if sleep != 0 {
210+
log.Infof("inwx: waiting %s for next TOTP token", sleep)
211+
time.Sleep(sleep)
212+
}
213+
205214
tan, err := totp.GenerateCode(d.config.SharedSecret, time.Now())
206215
if err != nil {
207216
return err
208217
}
209218

219+
d.previousUnlock = time.Now()
220+
210221
return d.client.Account.Unlock(tan)
211222
}
223+
224+
func (d *DNSProvider) computeSleep(now time.Time) time.Duration {
225+
if d.previousUnlock.IsZero() {
226+
return 0 * time.Second
227+
}
228+
229+
endPeriod := d.previousUnlock.Add(30 * time.Second)
230+
if endPeriod.After(now) {
231+
return endPeriod.Sub(now)
232+
}
233+
234+
return 0 * time.Second
235+
}

providers/dns/inwx/inwx_test.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@ package inwx
22

33
import (
44
"testing"
5+
"time"
56

67
"github.com/go-acme/lego/v4/platform/tester"
8+
"github.com/stretchr/testify/assert"
79
"github.com/stretchr/testify/require"
810
)
911

@@ -141,3 +143,45 @@ func TestLivePresentAndCleanup(t *testing.T) {
141143
err = provider.CleanUp(envTest.GetDomain(), "", "123d==")
142144
require.NoError(t, err)
143145
}
146+
147+
func Test_computeSleep(t *testing.T) {
148+
testCases := []struct {
149+
desc string
150+
previous string
151+
expected time.Duration
152+
}{
153+
{
154+
desc: "after 30s",
155+
previous: "2024-01-01T06:29:20Z",
156+
expected: 0 * time.Second,
157+
},
158+
{
159+
desc: "0s",
160+
previous: "2024-01-01T06:29:30Z",
161+
expected: 0 * time.Second,
162+
},
163+
{
164+
desc: "before 30s",
165+
previous: "2024-01-01T06:29:50Z", // 10 s
166+
expected: 20 * time.Second,
167+
},
168+
}
169+
170+
now, err := time.Parse(time.RFC3339, "2024-01-01T06:30:00Z")
171+
require.NoError(t, err)
172+
173+
for _, test := range testCases {
174+
test := test
175+
t.Run(test.desc, func(t *testing.T) {
176+
t.Parallel()
177+
178+
previous, err := time.Parse(time.RFC3339, test.previous)
179+
require.NoError(t, err)
180+
181+
d := &DNSProvider{previousUnlock: previous}
182+
183+
sleep := d.computeSleep(now)
184+
assert.Equal(t, test.expected, sleep)
185+
})
186+
}
187+
}

0 commit comments

Comments
 (0)