Skip to content

Commit

Permalink
Merge pull request #9 from jltorresm/feature/qr
Browse files Browse the repository at this point in the history
Add QR support
  • Loading branch information
jltorresm committed Sep 2, 2020
2 parents f4f7714 + 56f67df commit d461d73
Show file tree
Hide file tree
Showing 9 changed files with 92 additions and 10 deletions.
13 changes: 11 additions & 2 deletions README.md
Expand Up @@ -27,9 +27,9 @@ Implements [RFC 4226][rfc4226] and [RFC 6238][rfc6238].
- Generate HOTP and TOTP codes.
- Verify HOTP an TOTP codes.
- Export OTP config as a [Google Authenticator URI][googleURI].
- Export OTP config as a QR code image (used to register secrets in authenticator apps).

## Planned Functionality
- Export OTP config as a QR code image (used to register secrets in authenticator apps).
- Export OTP config as a JSON.

## Reading Material
Expand Down Expand Up @@ -118,7 +118,16 @@ The former being the preferred way because of the ease of use and the avoidance
of human error.

#### QR Code
TODO
To generate the QR code just get the `KeyUri` and call the `QRCode` method:
```go
otp := otpgo.TOTP{}
base64EncodedQRImage, _ := otp.
KeyUri("john.doe@example.org", "A Company").
QRCode()

// Then use base64EncodedQRImage however you like
// e.g.: send it to the client to display as an image
```

#### Manual registration
TODO
Expand Down
26 changes: 26 additions & 0 deletions authenticator/authenticator.go
@@ -1,8 +1,15 @@
package authenticator

import (
"encoding/base64"
"fmt"
"net/url"

"github.com/skip2/go-qrcode"
)

const (
QRSize = 256
)

// The KeyUri type holds all the config necessary to generate a valid
Expand All @@ -22,6 +29,25 @@ func (ku *KeyUri) String() string {
return uri.String()
}

// QRCode will encode the value returned by KeyUri.String into a base64
// encoded image containing a QR code that can be displayed and then scanned by
// the user. The return value is the base64 encoded image data.
func (ku *KeyUri) QRCode() (string, error) {
uri := ku.String()

qr, err := qrcode.New(uri, qrcode.Medium)
if err != nil {
return "", err
}

bytes, err := qr.PNG(QRSize)
if err != nil {
return "", err
}

return "data:image/png;base64," + base64.StdEncoding.EncodeToString(bytes), nil
}

// The Label is used to identify which account a key is associated with.
type Label struct {
AccountName string // Should be a username, email, etc.
Expand Down
39 changes: 39 additions & 0 deletions authenticator/authenticator_test.go
Expand Up @@ -71,3 +71,42 @@ func TestKeyUri_String(t *testing.T) {
}
}
}

func TestKeyUri_QRCode(t *testing.T) {
ku := KeyUri{
Type: "mock",
Label: Label{
AccountName: "J0hn@example.com",
Issuer: "Example Co.",
},
Parameters: mockFormatter("$p3c!al C#4rs"),
}

expected := "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAQAAAAEAAQMAAAB" +
"mvDolAAAABlBMVEX///8AAABVwtN+AAACtklEQVR42uyZzY3sIBCEC3HoIyGQiUlstNiax" +
"JhMCIEjB+R66p4f7+oFYCxN35b9DgNuV1VjfOtbFywhyQfJNazwRJQiO5yutgsBFZBHQ9i" +
"arzcgAmR3DfAzAaQ82hJ2ZJKeSABcy7xPBhSk51HXAaAvUwLaDxvJgRFZeurXAwApWMIa1" +
"nivvnZ066A/HXU2YK9eW8LWbvFeR5TSF9sW5wGeFR7tJ4w4MNDRXf9P684FpNo+CnLLZPU" +
"0fVjwEz4iNgFAEokqYysyfRUKWcLnoC8BIGoT88E1sAKsSNxlD2y3eQD9ydYwgK++et0DO" +
"wLb8eqdDgAd2jBh48ANCgCqzEdfzwCYW+xh134wRZNdF5DbdQCpHaL/fluz2sdfvzgfQNR" +
"Mo7uASjGrlO7I9hGQOQAkzYctP/0CPWGRAnfow/mAVCQkdbDAeovKY5EdP2GdB1CFMMfYW" +
"uaAryRcX7Q52pWAnjqCSvHQ8QE9yS7lcL0ZAKE2TChha9rgiMrDcXvr3AyAmpo2DNXFbmD" +
"tSdiBH2AqQHfxaLl5rtHXnmwyy+1agD6N9px7NE9KYTE9eSMTAJYnNZmTrLlaw7jumuPAR" +
"ABST/bDEe8WF2182MMxiJ0OaMiRouMD1HWHnXxXbfYTAbqCoJFxjevrT9nD9lHaKwDCrsO" +
"D44qXNcP8wh3JfAZANOQocrdkbpFXo9m7oyYBNMQ21zzv1DlHeW7HfDEH0JfwkVZNj2prC" +
"AOXAgrVL/RZACO+rkV+Z6DTASuN4mpqGnNeofdXnjwfeN0mAVlTjTmvGUjmTIBdJlsbI2q" +
"m7UmKFOSPPkwB2A1nsVvaWxzRdkE7+6sBOgJxxDWyknx+XhiYCyiv8SHbtR2A5ZcUTwG8L" +
"pPZPHUPtqjj5EzA+9VzZPWV1SYccv/7RWxy4Fvfmqr+BQAA///VaqA18lDV5wAAAABJRU5" +
"ErkJggg=="

qr, err := ku.QRCode()

if err != nil {
t.Errorf("unexpected error: %s", err)
t.FailNow()
}

if expected != qr {
t.Errorf("unexpected qr\nexpected: %s\n actual: %s", expected, qr)
}
}
6 changes: 4 additions & 2 deletions examples/hotp/main.go
Expand Up @@ -54,6 +54,8 @@ func main() {
ku := h.KeyUri(aUsername, anIssuer)

// From here you can get the plain text uri.
msg = "Exporting config for \"%s\" at \"%s\":\n\t- Plain URI --> %s\n"
fmt.Printf(msg, aUsername, anIssuer, ku.String())
msg = "Exporting config for \"%s\" at \"%s\":\n\t- Plain URI --> %s\n\t- QR code image, base 64 encoded (" +
"truncated to save space) --> %s...\n"
qr, _ := ku.QRCode()
fmt.Printf(msg, aUsername, anIssuer, ku.String(), qr[0:200])
}
6 changes: 4 additions & 2 deletions examples/totp/main.go
Expand Up @@ -46,6 +46,8 @@ func main() {
ku := t.KeyUri(aUsername, anIssuer)

// From here you can get the plain text uri.
msg = "Exporting config for \"%s\" at \"%s\":\n\t- Plain URI --> %s\n"
fmt.Printf(msg, aUsername, anIssuer, ku.String())
msg = "Exporting config for \"%s\" at \"%s\":\n\t- Plain URI --> %s\n\t- QR code image, base 64 encoded (" +
"truncated to save space) --> %s...\n"
qr, _ := ku.QRCode()
fmt.Printf(msg, aUsername, anIssuer, ku.String(), qr[0:200])
}
2 changes: 2 additions & 0 deletions go.mod
@@ -1,3 +1,5 @@
module github.com/jltorresm/otpgo

go 1.14

require github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
2 changes: 2 additions & 0 deletions go.sum
@@ -0,0 +1,2 @@
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
4 changes: 2 additions & 2 deletions hotp.go
Expand Up @@ -86,8 +86,8 @@ func (h *HOTP) Validate(token string) (bool, error) {
// KeyUri return an authenticator.KeyUri configured with the current HOTP params.
// - accountName is the username or email of the account
// - issuer is the site or org
func (h *HOTP) KeyUri(accountName, issuer string) authenticator.KeyUri {
return authenticator.KeyUri{
func (h *HOTP) KeyUri(accountName, issuer string) *authenticator.KeyUri {
return &authenticator.KeyUri{
Type: "hotp",
Label: authenticator.Label{
AccountName: accountName,
Expand Down
4 changes: 2 additions & 2 deletions totp.go
Expand Up @@ -91,8 +91,8 @@ func (t *TOTP) Validate(token string) (bool, error) {
// KeyUri return an authenticator.KeyUri configured with the current TOTP params.
// - accountName is the username or email of the account
// - issuer is the site or org
func (t *TOTP) KeyUri(accountName, issuer string) authenticator.KeyUri {
return authenticator.KeyUri{
func (t *TOTP) KeyUri(accountName, issuer string) *authenticator.KeyUri {
return &authenticator.KeyUri{
Type: "totp",
Label: authenticator.Label{
AccountName: accountName,
Expand Down

0 comments on commit d461d73

Please sign in to comment.