In this example we are going to create a One-Time Password (OTP) library in Golang. It will utilise Time-based One-time Password (TOTP) and HMAC-based One-Time Password (HOTP) versions. Both versions respect their IETF standards which are RFC 6238 and RFC 4226. Lastly, although some code parts resemble existing public libraries (if you follow standards, this is inevitable), this example does not use any public libraries.


There are two defaults we are using here. First one is, the OTP length is 6 characters and the second one is, the period is to 30 seconds. Also you should:



A suggestion about HOTP - Given that this is a counter based algorithm, allowing user to scan a QR code to use it with the mobile application makes counter drifts between client and server more inevitable. For instance, user manually refreshes the code in the application but never submits it which increases the counter on the client side but not on the server side. HOTP is ideal if you email or SMS the code to the user so that the counter on the client and server side match. The CreateHOTPCode function is dedicated for such purpose.


qr.go


package otp

import (
"fmt"

"rsc.io/qr"
)

// NewQR creates a new QR PNG from an OTP URI.
func NewQR(uri string) ([]byte, error) {
code, err := qr.Encode(uri, qr.Q)
if err != nil {
return nil, fmt.Errorf("encode: %w", err)
}

return code.PNG(), nil
}

package otp

import (
"testing"
)

func Test_NewQR(t *testing.T) {
qr, err := NewQR("otpauth://type/label?parameters")
if err != nil {
t.Error("an error was not expected")
}
if len(qr) != 1445 {
t.Error("expected 1445 byte slice but got", len(qr))
}
}

secret.go


package otp

import (
"crypto/rand"
"encoding/base32"
"fmt"
)

// REF: https://datatracker.ietf.org/doc/html/rfc3548#section-5
const secretChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"

// NewSecret creates a Base32 encoded arbitrary secret from a fixed length of 16
// byte slice without having a padding sign `=` at the end.
func NewSecret() (string, error) {
bytes := make([]byte, 16)

if _, err := rand.Read(bytes); err != nil {
return "", fmt.Errorf("read: %w", err)
}

for i, b := range bytes {
bytes[i] = secretChars[b%byte(len(secretChars))]
}

return base32.StdEncoding.WithPadding(base32.NoPadding).EncodeToString(bytes), nil
}

package otp

import (
"encoding/base32"
"testing"
)

func Test_NewSecret(t *testing.T) {
// Encoded secret
encSec, err := NewSecret()
if err != nil {
t.Error("an error was not expected while encoding but got", err.Error())
}
if len(encSec) != 26 {
t.Error("expected 26 characters long secret but got", len(encSec))
}
if encSec[:26] == "=" {
t.Error("did not expect padding")
}

// Decodeded secret
decSec, err := base32.StdEncoding.WithPadding(base32.NoPadding).DecodeString(encSec)
if err != nil {
t.Error("an error was not expected while decoding but got", err.Error())
}
if len(decSec) != 16 {
t.Error("expected 16 byte slice but got", len(decSec))
}
}

otp.go


// TOTP: https://en.wikipedia.org/wiki/One-time_password
// https://datatracker.ietf.org/doc/html/rfc6238
// HOTP: https://en.wikipedia.org/wiki/HMAC-based_one-time_password
// https://datatracker.ietf.org/doc/html/rfc4226
// The Google Authenticator: https://github.com/google/google-authenticator/wiki
package otp

import (
"crypto/hmac"
"crypto/sha1"
"encoding/base32"
"encoding/binary"
"fmt"
"time"
)

const (
// length defines the OTP code in character length.
length = 6
// period defines the TTL of a TOTP code in seconds.
period = 30
)

type OTP struct {
// Issuer represents the service provider. It is you! e.g. your service,
// your application, your organisation so on.
Issuer string
// Account represents the service user. It is the user! e.g. username, email
// address so on.
Account string
// Secret is an arbitrary key value encoded in Base32 and belongs to the
// service user.
Secret string
// Window is used for time (TOTP) and counter (HOTP) synchronization. Given
// that the possible time and counter drifts between client and server, this
// parameter helps overcome such issue. TOTP uses backward and forward time
// window whereas HOTP uses look-ahead counter window that depends on the
// Counter parameter.
// Resynchronisation is an official recommended practise, however the
// lower the better.
// 0 = not recommended as synchronization is disabled
// TOTP: current time
// HOTP: current counter
// 1 = recommended option
// TOTP: previous - current - next
// HOTP: current counter - next counter
// 2 = being overcautious
// TOTP: previous,previous - current - next,next
// HOTP: current counter - next counter - next counter
// * = Higher numbers may cause denial-of-service attacks.
// REF: https://datatracker.ietf.org/doc/html/rfc6238#page-7
// REF: https://datatracker.ietf.org/doc/html/rfc4226#page-11
Window int
// Counter is required for HOTP only and used for provisioning the code. Set
// it to 0 if you with to use TOTP. Start from 1 for HOTP then fetch and use
// the one in the persistent storage. The server counter is incremented only
// after a successful code verification, however the counter on the code is
// incremented every time a new code is requested by the user which causes
// counters being out of sync. For that reason, time-synchronization should
// be enabled.
// REF: https://datatracker.ietf.org/doc/html/rfc4226#page-11
Counter int
}

// CreateURI builds the authentication URI which is used to create a QR code.
// If the counter is set to 0, the algorithm is assumed to be TOTP, otherwise
// HOTP.
// REF: https://github.com/google/google-authenticator/wiki/Key-Uri-Format
func (o *OTP) CreateURI() string {
algorithm := "totp"
counter := ""
if o.Counter != 0 {
algorithm = "hotp"
counter = fmt.Sprintf("&counter=%d", o.Counter)
}

return fmt.Sprintf("otpauth://%s/%s:%s?secret=%s&issuer=%s%s",
algorithm,
o.Issuer,
o.Account,
o.Secret,
o.Issuer,
counter,
)
}

// CreateHOTPCode creates a new HOTP with a specific counter. This method is
// ideal if you are planning to send manually created code via email, SMS etc.
// The user should not be present a QR code for this option otherwise there is
// a high posibility that the client and server counters will be out of sync,
// unless the user will be forced to rescan a newly generaed QR with up to date
// counter value.
func (o *OTP) CreateHOTPCode(counter int) (string, error) {
val, err := o.createCode(counter)
if err != nil {
return "", fmt.Errorf("create code: %w", err)
}

o.Counter = counter
return val, nil
}

// VerifyCode talks to an algorithm specific validator to verify the integrity
// of the code. If the counter is set to 0, the algorithm is assumed to be TOTP,
// otherwise HOTP.
func (o *OTP) VerifyCode(code string) (bool, error) {
if len(code) != length {
return false, fmt.Errorf("invalid length")
}

if o.Counter != 0 {
ok, err := o.verifyHOTP(code)
if err != nil {
return false, fmt.Errorf("verify HOTP: %w", err)
}
if !ok {
return false, nil
}
return true, nil
}

ok, err := o.verifyTOTP(code)
if err != nil {
return false, fmt.Errorf("verify TOTP: %w", err)
}
if !ok {
return false, nil
}

return true, nil
}

// Depending on the given windows size, we handle clock resynchronisation. If
// the window size is set to 0, resynchronisation is disabled and we just use
// the current time. Otherwise, backward and forward window is taken into
// account as well.
func (o *OTP) verifyTOTP(code string) (bool, error) {
curr := int(time.Now().UTC().Unix() / period)
back := curr
forw := curr
if o.Window != 0 {
back -= o.Window
forw += o.Window
}

for i := back; i <= forw; i++ {
val, err := o.createCode(i)
if err != nil {
return false, fmt.Errorf("create code: %w", err)
}
if val == code {
return true, nil
}
}

return false, nil
}

// Depending on the given windows size, we handle counter resynchronisation. If
// the window size is set to 0, resynchronisation is disabled and we just use
// the current counter. Otherwise, look-ahead counter window is used. When the
// look-ahead window is used, we calculate the next codes and determine if there
// is a match by utilising counter resynchronisation.
func (o *OTP) verifyHOTP(code string) (bool, error) {
size := 0
if o.Window != 0 {
size = o.Window
}

for i := 0; i <= size; i++ {
val, err := o.createCode(o.Counter + i)
if err != nil {
return false, fmt.Errorf("create code: %w", err)
}
if val == code {
o.Counter += i + 1
return true, nil
}
}

o.Counter++
return false, nil
}

// createCode creates a new OTP code based on either a time or counter interval.
// The time is used for TOTP and the counter is used for HOTP algorithm.
func (o *OTP) createCode(interval int) (string, error) {
sec, err := base32.StdEncoding.WithPadding(base32.NoPadding).DecodeString(o.Secret)
if err != nil {
return "", fmt.Errorf("decode string: %w", err)
}

hash := hmac.New(sha1.New, sec)
if err := binary.Write(hash, binary.BigEndian, int64(interval)); err != nil {
return "", fmt.Errorf("binary write: %w", err)
}
sign := hash.Sum(nil)

offset := sign[19] & 15
trunc := binary.BigEndian.Uint32(sign[offset : offset+4])

return fmt.Sprintf("%0*d", length, (trunc&0x7fffffff)%1000000), nil
}

package otp

import (
"testing"
"time"
)

func Test_OTP_CreateURI(t *testing.T) {
twoFA := &OTP{
Issuer: "issuer",
Account: "account",
Secret: "secret",
Window: 0,
Counter: 0,
}

t.Run("totp uri", func(t *testing.T) {
uri := twoFA.CreateURI()
if uri != "otpauth://totp/issuer:account?secret=secret&issuer=issuer" {
t.Error("expected otpauth://totp/issuer:account?secret=secret&issuer=issuer but got", uri)
}
})

t.Run("hotp uri", func(t *testing.T) {
twoFA.Counter = 1
uri := twoFA.CreateURI()
if uri != "otpauth://hotp/issuer:account?secret=secret&issuer=issuer&counter=1" {
t.Error("expected otpauth://hotp/issuer:account?secret=secret&issuer=issuer&counter=1 but got", uri)
}
})
}

func Test_OTP_CreateHOTPCode(t *testing.T) {
twoFA := &OTP{
Issuer: "issuer",
Account: "account",
Window: 0,
Counter: 1,
}

t.Run("error if secret is illegal base32 data", func(t *testing.T) {
twoFA.Secret = "secret"
_, err := twoFA.CreateHOTPCode(2)
if err == nil {
t.Error("expected an error but got nil")
}
if err.Error() != "create code: decode string: illegal base32 data at input byte 0" {
t.Error("expected create code: decode string: illegal base32 data at input byte 0 but got", err.Error())
}
if twoFA.Counter != 1 {
t.Error("counter should have been 1 but got", twoFA.Counter)
}
})

t.Run("successful code creation and counter increase", func(t *testing.T) {
twoFA.Secret = "GNFE2UCWJRCEOMZSLBHUMVCWKM"
code, err := twoFA.CreateHOTPCode(2)
if err != nil {
t.Error("an error was not expected but got", err.Error())
}
if code != "501363" {
t.Error("expected 501363 but got", code)
}
if twoFA.Counter != 2 {
t.Error("counter should have been 2 but got", twoFA.Counter)
}
})
}

func Test_OTP_VerifyCode(t *testing.T) {
t.Run("error while using invalid code with length", func(t *testing.T) {
twoFA := &OTP{}

ok, err := twoFA.VerifyCode("00000") // Not equal to 6
if err == nil {
t.Error("expected an error but got nil")
}
if err.Error() != "invalid length" {
t.Error("expected invalid length but got", err.Error())
}
if ok {
t.Error("expected false but got true")
}
})

t.Run("failed verification for hotp code with invalid secret", func(t *testing.T) {
twoFA := &OTP{
Secret: "secret",
Counter: 1,
}
ok, err := twoFA.VerifyCode("000000") // Random code
if err == nil {
t.Error("expected an error but got nil")
}
if err.Error() != "verify HOTP: create code: decode string: illegal base32 data at input byte 0" {
t.Error("expected verify HOTP: create code: decode string: illegal base32 data at input byte 0 but got", err.Error())
}
if ok {
t.Error("expected false but got true")
}
})

t.Run("failed verification for hotp code", func(t *testing.T) {
twoFA := &OTP{
Secret: "GNFE2UCWJRCEOMZSLBHUMVCWKM",
Counter: 1,
}
ok, err := twoFA.VerifyCode("000000") // Random code
if err != nil {
t.Error("an error was not expected but got", err.Error())
}
if ok {
t.Error("expected false but got true")
}
})

t.Run("successful verification for valid hotp code", func(t *testing.T) {
twoFA := &OTP{
Secret: "GNFE2UCWJRCEOMZSLBHUMVCWKM",
Counter: 1,
}
ok, err := twoFA.VerifyCode("204727") // Code for current counter
if err != nil {
t.Error("an error was not expected but got", err.Error())
}
if !ok {
t.Error("expected true but got false")
}
})

t.Run("failed verification for totp code with invalid secret", func(t *testing.T) {
twoFA := &OTP{Secret: "secret"}
ok, err := twoFA.VerifyCode("000000") // Random code
if err == nil {
t.Error("expected an error but got nil")
}
if err.Error() != "verify TOTP: create code: decode string: illegal base32 data at input byte 0" {
t.Error("expected verify TOTP: create code: decode string: illegal base32 data at input byte 0 but got", err.Error())
}
if ok {
t.Error("expected false but got true")
}
})

t.Run("failed verification for totp code", func(t *testing.T) {
twoFA := &OTP{Secret: "GNFE2UCWJRCEOMZSLBHUMVCWKM"}
ok, err := twoFA.VerifyCode("000000") // Random code
if err != nil {
t.Error("an error was not expected but got", err.Error())
}
if ok {
t.Error("expected false but got true")
}
})

t.Run("successful verification for valid totp code", func(t *testing.T) {
twoFA := &OTP{Secret: "GNFE2UCWJRCEOMZSLBHUMVCWKM"}

code, err := twoFA.createCode(int(time.Now().UTC().Unix() / period)) // Code from current time
if err != nil {
t.Error("an error was not expected but got", err.Error())
}

ok, err := twoFA.VerifyCode(code)
if err != nil {
t.Error("an error was not expected but got", err.Error())
}
if !ok {
t.Error("expected true but got false")
}
})
}

func Test_OTP_verifyTOTP(t *testing.T) {
t.Run("error while decoding illegal secret", func(t *testing.T) {
twoFA := &OTP{Secret: "secret"}
ok, err := twoFA.verifyTOTP("000000") // Random code
if err == nil {
t.Error("expected an error but got nil")
}
if err.Error() != "create code: decode string: illegal base32 data at input byte 0" {
t.Error("expected create code: decode string: illegal base32 data at input byte 0 but got", err.Error())
}
if ok {
t.Error("expected false but got true")
}
})

t.Run("failed verification for invalid code with resynchronisation disabled", func(t *testing.T) {
twoFA := &OTP{
Secret: "GNFE2UCWJRCEOMZSLBHUMVCWKM",
Window: 0, // resynchronisation disabled
}
ok, err := twoFA.verifyTOTP("000000") // Random code
if err != nil {
t.Error("an error was not expected but got", err.Error())
}
if ok {
t.Error("expected false but got true")
}
})

t.Run("failed verification for invalid code with resynchronisation enabled", func(t *testing.T) {
twoFA := &OTP{
Secret: "GNFE2UCWJRCEOMZSLBHUMVCWKM",
Window: 1, // resynchronisation enabled
}
ok, err := twoFA.verifyTOTP("000000") // Random code
if err != nil {
t.Error("an error was not expected but got", err.Error())
}
if ok {
t.Error("expected false but got true")
}
})

t.Run("successful verification with resynchronisation disabled and without time drift", func(t *testing.T) {
twoFA := &OTP{
Secret: "GNFE2UCWJRCEOMZSLBHUMVCWKM",
Window: 0, // resynchronisation disabled
}

code, err := twoFA.createCode(int(time.Now().UTC().Unix() / period)) // Code from current window (no time drift)
if err != nil {
t.Error("an error was not expected but got", err.Error())
}

ok, err := twoFA.verifyTOTP(code)
if err != nil {
t.Error("an error was not expected but got", err.Error())
}
if !ok {
t.Error("expected true but got false")
}
})

t.Run("successful verification with resynchronisation enabled and with time drift of previous window", func(t *testing.T) {
twoFA := &OTP{
Secret: "GNFE2UCWJRCEOMZSLBHUMVCWKM",
Window: 1, // resynchronisation enabled
}

code, err := twoFA.createCode(int(time.Now().UTC().Unix()/period) - 1) // Code from previous window (time drift)
if err != nil {
t.Error("an error was not expected but got", err.Error())
}

ok, err := twoFA.verifyTOTP(code)
if err != nil {
t.Error("an error was not expected but got", err.Error())
}
if !ok {
t.Error("expected true but got false")
}
})

t.Run("failed verification with resynchronisation disabled and with time drift of previous window", func(t *testing.T) {
twoFA := &OTP{
Secret: "GNFE2UCWJRCEOMZSLBHUMVCWKM",
Window: 0, // resynchronisation disabled
}

code, err := twoFA.createCode(int(time.Now().UTC().Unix()/period) - 1) // Code from previous window (time drift)
if err != nil {
t.Error("an error was not expected but got", err.Error())
}

ok, err := twoFA.verifyTOTP(code)
if err != nil {
t.Error("an error was not expected but got", err.Error())
}
if ok {
t.Error("expected false but got true")
}
})

t.Run("successful verification with resynchronisation enabled and with time drift of next window", func(t *testing.T) {
twoFA := &OTP{
Secret: "GNFE2UCWJRCEOMZSLBHUMVCWKM",
Window: 1, // resynchronisation enabled
}

code, err := twoFA.createCode(int(time.Now().UTC().Unix()/period) + 1) // Code from next window (time drift)
if err != nil {
t.Error("an error was not expected but got", err.Error())
}

ok, err := twoFA.verifyTOTP(code)
if err != nil {
t.Error("an error was not expected but got", err.Error())
}
if !ok {
t.Error("expected true but got false")
}
})

t.Run("failed verification with resynchronisation disabled and with time drift of next window", func(t *testing.T) {
twoFA := &OTP{
Secret: "GNFE2UCWJRCEOMZSLBHUMVCWKM",
Window: 0, // resynchronisation disabled
}

code, err := twoFA.createCode(int(time.Now().UTC().Unix()/period) + 1) // Code from next window (time drift)
if err != nil {
t.Error("an error was not expected but got", err.Error())
}

ok, err := twoFA.verifyTOTP(code)
if err != nil {
t.Error("an error was not expected but got", err.Error())
}
if ok {
t.Error("expected false but got true")
}
})
}

func Test_OTP_verifyHOTP(t *testing.T) {
t.Run("error while decoding illegal secret", func(t *testing.T) {
twoFA := &OTP{Secret: "secret"}
ok, err := twoFA.verifyHOTP("000000") // Random code
if err == nil {
t.Error("expected an error but got nil")
}
if err.Error() != "create code: decode string: illegal base32 data at input byte 0" {
t.Error("expected create code: decode string: illegal base32 data at input byte 0 but got", err.Error())
}
if ok {
t.Error("expected false but got true")
}
})

t.Run("failed verification for invalid code with resynchronisation disabled", func(t *testing.T) {
twoFA := &OTP{
Secret: "GNFE2UCWJRCEOMZSLBHUMVCWKM",
Window: 0, // resynchronisation disabled
Counter: 1,
}
ok, err := twoFA.verifyHOTP("000000") // Random code
if err != nil {
t.Error("an error was not expected but got", err.Error())
}
if ok {
t.Error("expected false but got true")
}
if twoFA.Counter != 2 {
t.Error("counter should have been 2 but got", twoFA.Counter)
}
})

t.Run("failed verification for invalid code with resynchronisation enabled", func(t *testing.T) {
twoFA := &OTP{
Secret: "GNFE2UCWJRCEOMZSLBHUMVCWKM",
Window: 1, // resynchronisation enabled
Counter: 1,
}
ok, err := twoFA.verifyHOTP("000000") // Random code
if err != nil {
t.Error("an error was not expected but got", err.Error())
}
if ok {
t.Error("expected false but got true")
}
if twoFA.Counter != 2 {
t.Error("counter should have been 2 but got", twoFA.Counter)
}
})

t.Run("successful verification with resynchronisation disabled and without counter drift", func(t *testing.T) {
twoFA := &OTP{
Secret: "GNFE2UCWJRCEOMZSLBHUMVCWKM",
Window: 0, // resynchronisation disabled
Counter: 1,
}
ok, err := twoFA.verifyHOTP("204727") // Code for current counter
if err != nil {
t.Error("an error was not expected but got", err.Error())
}
if !ok {
t.Error("expected true but got false")
}
if twoFA.Counter != 2 {
t.Error("counter should have been 2 but got", twoFA.Counter)
}
})

t.Run("successful verification with resynchronisation enabled and with counter drift", func(t *testing.T) {
twoFA := &OTP{
Secret: "GNFE2UCWJRCEOMZSLBHUMVCWKM",
Window: 1, // resynchronisation enabled
Counter: 1,
}
ok, err := twoFA.verifyHOTP("501363") // 501363 belongs to counter 2 (counter drift)
if err != nil {
t.Error("an error was not expected but got", err.Error())
}
if !ok {
t.Error("expected true but got false")
}
if twoFA.Counter != 3 {
t.Error("counter should have been 3 but got", twoFA.Counter)
}
})

t.Run("failed verification with resynchronisation disabled and with counter drift", func(t *testing.T) {
twoFA := &OTP{
Secret: "GNFE2UCWJRCEOMZSLBHUMVCWKM",
Window: 0, // resynchronisation disabled
Counter: 1,
}
ok, err := twoFA.verifyHOTP("501363") // 501363 belongs to counter 2 (counter drift)
if err != nil {
t.Error("an error was not expected but got", err.Error())
}
if ok {
t.Error("expected false but got true")
}
if twoFA.Counter != 2 {
t.Error("counter should have been 2 but got", twoFA.Counter)
}
})
}

func Test_OTP_createCode(t *testing.T) {
t.Run("error while decoding illegal secret", func(t *testing.T) {
twoFA := &OTP{Secret: "secret"}
code, err := twoFA.createCode(1)
if err == nil {
t.Error("expected an error but got nil")
}
if err.Error() != "decode string: illegal base32 data at input byte 0" {
t.Error("expected decode string: illegal base32 data at input byte 0 but got", err.Error())
}
if code != "" {
t.Error("expected empty code but got", code)
}
})

t.Run("successful code creation", func(t *testing.T) {
twoFA := &OTP{Secret: "GNFE2UCWJRCEOMZSLBHUMVCWKM"}
code, err := twoFA.createCode(1)
if err != nil {
t.Error("an error was not expected but got", err.Error())
}
if code != "204727" {
t.Error("expected 501363 but got", code)
}
})
}

Tests


$ gotest -v ./...
=== RUN Test_OTP_CreateURI
=== RUN Test_OTP_CreateURI/totp_uri
=== RUN Test_OTP_CreateURI/hotp_uri
--- PASS: Test_OTP_CreateURI (0.00s)
--- PASS: Test_OTP_CreateURI/totp_uri (0.00s)
--- PASS: Test_OTP_CreateURI/hotp_uri (0.00s)
=== RUN Test_OTP_CreateHOTPCode
=== RUN Test_OTP_CreateHOTPCode/error_if_secret_is_illegal_base32_data
=== RUN Test_OTP_CreateHOTPCode/successful_code_creation_and_counter_increase
--- PASS: Test_OTP_CreateHOTPCode (0.00s)
--- PASS: Test_OTP_CreateHOTPCode/error_if_secret_is_illegal_base32_data (0.00s)
--- PASS: Test_OTP_CreateHOTPCode/successful_code_creation_and_counter_increase (0.00s)
=== RUN Test_OTP_VerifyCode
=== RUN Test_OTP_VerifyCode/error_while_using_invalid_code_with_length
=== RUN Test_OTP_VerifyCode/failed_verification_for_hotp_code_with_invalid_secret
=== RUN Test_OTP_VerifyCode/failed_verification_for_hotp_code
=== RUN Test_OTP_VerifyCode/successful_verification_for_valid_hotp_code
=== RUN Test_OTP_VerifyCode/failed_verification_for_totp_code_with_invalid_secret
=== RUN Test_OTP_VerifyCode/failed_verification_for_totp_code
=== RUN Test_OTP_VerifyCode/successful_verification_for_valid_totp_code
--- PASS: Test_OTP_VerifyCode (0.00s)
--- PASS: Test_OTP_VerifyCode/error_while_using_invalid_code_with_length (0.00s)
--- PASS: Test_OTP_VerifyCode/failed_verification_for_hotp_code_with_invalid_secret (0.00s)
--- PASS: Test_OTP_VerifyCode/failed_verification_for_hotp_code (0.00s)
--- PASS: Test_OTP_VerifyCode/successful_verification_for_valid_hotp_code (0.00s)
--- PASS: Test_OTP_VerifyCode/failed_verification_for_totp_code_with_invalid_secret (0.00s)
--- PASS: Test_OTP_VerifyCode/failed_verification_for_totp_code (0.00s)
--- PASS: Test_OTP_VerifyCode/successful_verification_for_valid_totp_code (0.00s)
=== RUN Test_OTP_verifyTOTP
=== RUN Test_OTP_verifyTOTP/error_while_decoding_illegal_secret
=== RUN Test_OTP_verifyTOTP/failed_verification_for_invalid_code_with_resynchronisation_disabled
=== RUN Test_OTP_verifyTOTP/failed_verification_for_invalid_code_with_resynchronisation_enabled
=== RUN Test_OTP_verifyTOTP/successful_verification_with_resynchronisation_disabled_and_without_time_drift
=== RUN Test_OTP_verifyTOTP/successful_verification_with_resynchronisation_enabled_and_with_time_drift_of_previous_window
=== RUN Test_OTP_verifyTOTP/failed_verification_with_resynchronisation_disabled_and_with_time_drift_of_previous_window
=== RUN Test_OTP_verifyTOTP/successful_verification_with_resynchronisation_enabled_and_with_time_drift_of_next_window
=== RUN Test_OTP_verifyTOTP/failed_verification_with_resynchronisation_disabled_and_with_time_drift_of_next_window
--- PASS: Test_OTP_verifyTOTP (0.00s)
--- PASS: Test_OTP_verifyTOTP/error_while_decoding_illegal_secret (0.00s)
--- PASS: Test_OTP_verifyTOTP/failed_verification_for_invalid_code_with_resynchronisation_disabled (0.00s)
--- PASS: Test_OTP_verifyTOTP/failed_verification_for_invalid_code_with_resynchronisation_enabled (0.00s)
--- PASS: Test_OTP_verifyTOTP/successful_verification_with_resynchronisation_disabled_and_without_time_drift (0.00s)
--- PASS: Test_OTP_verifyTOTP/successful_verification_with_resynchronisation_enabled_and_with_time_drift_of_previous_window (0.00s)
--- PASS: Test_OTP_verifyTOTP/failed_verification_with_resynchronisation_disabled_and_with_time_drift_of_previous_window (0.00s)
--- PASS: Test_OTP_verifyTOTP/successful_verification_with_resynchronisation_enabled_and_with_time_drift_of_next_window (0.00s)
--- PASS: Test_OTP_verifyTOTP/failed_verification_with_resynchronisation_disabled_and_with_time_drift_of_next_window (0.00s)
=== RUN Test_OTP_verifyHOTP
=== RUN Test_OTP_verifyHOTP/error_while_decoding_illegal_secret
=== RUN Test_OTP_verifyHOTP/failed_verification_for_invalid_code_with_resynchronisation_disabled
=== RUN Test_OTP_verifyHOTP/failed_verification_for_invalid_code_with_resynchronisation_enabled
=== RUN Test_OTP_verifyHOTP/successful_verification_with_resynchronisation_disabled_and_without_counter_drift
=== RUN Test_OTP_verifyHOTP/successful_verification_with_resynchronisation_enabled_and_with_counter_drift
=== RUN Test_OTP_verifyHOTP/failed_verification_with_resynchronisation_disabled_and_with_counter_drift
--- PASS: Test_OTP_verifyHOTP (0.00s)
--- PASS: Test_OTP_verifyHOTP/error_while_decoding_illegal_secret (0.00s)
--- PASS: Test_OTP_verifyHOTP/failed_verification_for_invalid_code_with_resynchronisation_disabled (0.00s)
--- PASS: Test_OTP_verifyHOTP/failed_verification_for_invalid_code_with_resynchronisation_enabled (0.00s)
--- PASS: Test_OTP_verifyHOTP/successful_verification_with_resynchronisation_disabled_and_without_counter_drift (0.00s)
--- PASS: Test_OTP_verifyHOTP/successful_verification_with_resynchronisation_enabled_and_with_counter_drift (0.00s)
--- PASS: Test_OTP_verifyHOTP/failed_verification_with_resynchronisation_disabled_and_with_counter_drift (0.00s)
=== RUN Test_OTP_createCode
=== RUN Test_OTP_createCode/error_while_decoding_illegal_secret
=== RUN Test_OTP_createCode/successful_code_creation
--- PASS: Test_OTP_createCode (0.00s)
--- PASS: Test_OTP_createCode/error_while_decoding_illegal_secret (0.00s)
--- PASS: Test_OTP_createCode/successful_code_creation (0.00s)
=== RUN Test_NewQR
--- PASS: Test_NewQR (0.00s)
=== RUN Test_NewSecret
--- PASS: Test_NewSecret (0.00s)
PASS
ok github.com/you/otp 0.065s

Usage


// Create secret
sec, err := otp.NewSecret()
if err != nil {
log.Fatalln(err)
}

iss := "Inanzzz"
acc := "you@example.com"

// Use TOTP
twoFA := &otp.OTP{
Issuer: iss,
Account: acc,
Secret: sec,
Window: 0, // Without time synchronization (prefer 1 to enable synchronization)
}

// ctr := 1
//
// Use HOTP
// twoFA := &otp.OTP{
// Issuer: iss,
// Account: acc,
// Secret: sec,
// Window: 0, // Without counter synchronization (prefer 1 to enable synchronization)
// Counter: ctr,
// }

// Create HOTP manually
// code, err := twoFA.CreateHOTPCode(ctr)
// if err != nil {
// log.Fatalln(err)
// }

// Create and save QR image
qr, err := otp.NewQR(twoFA.CreateURI())
if err != nil {
log.Fatalln(err)
}
err = ioutil.WriteFile("qr.png", qr, 0600)
if err != nil {
log.Fatalln(err)
}

code := "891329"

// Verify TOP code
ok, err := twoFA.VerifyCode(code)
if err != nil {
log.Fatalln(err)
}
if !ok {
log.Println("INVALID")
} else {
log.Println("VALID")
}