package authenticator import ( "context" "fmt" "net/http" "strings" "time" "git.wxccs.org/iceking2nd/winauth-go/internal/global" "git.wxccs.org/iceking2nd/winauth-go/internal/httpc" ) // GoogleAuthenticator implements the time-sync flavor of TOTP that Google, // Microsoft and Okta all share. The only difference between vendors in the // original C# port is the URL used to learn the server clock. type GoogleAuthenticator struct { Base timeSyncURL string } // NewGoogleAuthenticator returns a Google-flavored TOTP authenticator. func NewGoogleAuthenticator() *GoogleAuthenticator { return newTOTP("https://www.google.com") } // NewMicrosoftAuthenticator is an alias kept for parity with the original // C# class hierarchy. func NewMicrosoftAuthenticator() *GoogleAuthenticator { return newTOTP("https://www.microsoft.com") } // NewOktaVerifyAuthenticator returns a TOTP that syncs against okta.com. func NewOktaVerifyAuthenticator() *GoogleAuthenticator { return newTOTP("https://www.okta.com") } func newTOTP(syncURL string) *GoogleAuthenticator { g := &GoogleAuthenticator{Base: NewBase(), timeSyncURL: syncURL} return g } // Name returns a short identifier used in logs. func (g *GoogleAuthenticator) Name() string { switch g.timeSyncURL { case "https://www.microsoft.com": return "microsoft" case "https://www.okta.com": return "okta" default: return "google" } } // Enroll loads a base32-encoded shared secret and then performs an initial // clock sync against the vendor's HTTP endpoint. func (g *GoogleAuthenticator) Enroll(b32 string) error { const fn = "internal.authenticator.GoogleAuthenticator.Enroll" raw, err := Base32Decode(b32) if err != nil { return err } g.SecretKey = raw global.Log.WithField("func", fn).WithField("len", len(raw)).Debug("enrolled secret") return g.Sync() } // CurrentCode returns the live TOTP for the receiver. func (g *GoogleAuthenticator) CurrentCode() (string, error) { if g.SecretKey == nil { return "", fmt.Errorf("authenticator: no secret loaded") } return g.CalculateTOTP(), nil } // SecretData / SetSecretData delegate to the embedded Base. func (g *GoogleAuthenticator) SecretData() string { return g.EncodeSecretData() } func (g *GoogleAuthenticator) SetSecretData(value string) error { return g.DecodeSecretData(value) } // Sync issues a HEAD request against the configured vendor URL and reads // the response's Date header to derive ServerTimeDiff. Errors are swallowed // in the same way as the original C# implementation — repeated failures // should not block code generation, the local clock is the fallback. func (g *GoogleAuthenticator) Sync() error { const fn = "internal.authenticator.GoogleAuthenticator.Sync" logger := global.Log.WithField("func", fn).WithField("url", g.timeSyncURL) ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() req, err := http.NewRequestWithContext(ctx, http.MethodHead, g.timeSyncURL, nil) if err != nil { logger.WithError(err).Warn("build request failed") return err } resp, err := httpc.New().Do(req) if err != nil { logger.WithError(err).Warn("sync request failed; using local clock") return nil } defer resp.Body.Close() dateStr := strings.TrimSpace(resp.Header.Get("Date")) if dateStr == "" { logger.Warn("response missing Date header") return nil } t, err := http.ParseTime(dateStr) if err != nil { logger.WithError(err).Warn("invalid Date header") return nil } serverMs := t.UnixMilli() g.ServerTimeDiff = serverMs - NowMillis() g.LastServerTime = NowMillis() logger.WithField("offset_ms", g.ServerTimeDiff).Debug("clock synced") return nil }