Skip to content

Commit

Permalink
feat: add 2fa OTP Strategy
Browse files Browse the repository at this point in the history
  • Loading branch information
shaj13 committed Aug 28, 2020
1 parent ece72c3 commit 339f3cb
Show file tree
Hide file tree
Showing 8 changed files with 634 additions and 1 deletion.
1 change: 0 additions & 1 deletion _examples/kubernetes/mock.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
package main

import (
"fmt"
"io/ioutil"
"log"
"net/http"
Expand Down
101 changes: 101 additions & 0 deletions _examples/twofactor/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
// Copyright 2020 The Go-Guardian. All rights reserved.
// Use of this source code is governed by a MIT
// license that can be found in the LICENSE file.

package main

import (
"context"
"fmt"
"log"
"net/http"

"github.com/gorilla/mux"

"github.com/shaj13/go-guardian/auth"
"github.com/shaj13/go-guardian/auth/strategies/basic"
"github.com/shaj13/go-guardian/auth/strategies/twofactor"
"github.com/shaj13/go-guardian/tfa"
)

// Usage:
// curl -k http://127.0.0.1:8080/v1/book/1449311601 -u admin:admin -H "X-Example-OTP: 345515"

var authenticator auth.Authenticator

func main() {
setupGoGuardian()
router := mux.NewRouter()
router.HandleFunc("/v1/book/{id}", middleware(http.HandlerFunc(getBookAuthor))).Methods("GET")
log.Println("server started and listening on http://127.0.0.1:8080")
http.ListenAndServe("127.0.0.1:8080", router)
}

func getBookAuthor(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id := vars["id"]
books := map[string]string{
"1449311601": "Ryan Boyd",
"148425094X": "Yvonne Wilson",
"1484220498": "Prabath Siriwarden",
}
body := fmt.Sprintf("Author: %s \n", books[id])
w.Write([]byte(body))
}

func setupGoGuardian() {
authenticator = auth.New()

basicStrategy := basic.AuthenticateFunc(validateUser)
tfaStrategy := twofactor.Strategy{
Parser: twofactor.XHeaderParser("X-Example-OTP"),
Manager: OTPManager{},
Primary: basicStrategy,
}

authenticator.EnableStrategy(twofactor.StrategyKey, tfaStrategy)
}

func validateUser(ctx context.Context, r *http.Request, userName, password string) (auth.Info, error) {
// here connect to db or any other service to fetch user and validate it.
if userName == "admin" && password == "admin" {
return auth.NewDefaultUser("medium", "1", nil, nil), nil
}

return nil, fmt.Errorf("Invalid credentials")
}

func middleware(next http.Handler) http.HandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Println("Executing Auth Middleware")
user, err := authenticator.Authenticate(r)
if err != nil {
code := http.StatusUnauthorized
http.Error(w, http.StatusText(code), code)
return
}
log.Printf("User %s Authenticated\n", user.UserName())
next.ServeHTTP(w, r)
})
}

type OTPManager struct{}

func (OTPManager) Enabled(_ auth.Info) bool { return true }

func (OTPManager) Load(_ auth.Info) (twofactor.OTP, error) {
// user otp configuration must be loaded from persistent storage
cfg := tfa.OTPConfig{
OTPType: tfa.HOTP,
Label: "LABEL",
Counter: 0,
Secret: "GXNRHI2MFRFWXQGJHWZJFOSYI6E7MEVA",
}
_, otp, err := tfa.NewOTP(&cfg)
return otp, err
}

func (OTPManager) Store(_ auth.Info, otp twofactor.OTP) error {
// persist user otp after verification
return nil
}
57 changes: 57 additions & 0 deletions auth/strategies/twofactor/example_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package twofactor_test

import (
"context"
"fmt"
"net/http"

"github.com/shaj13/go-guardian/auth"
"github.com/shaj13/go-guardian/auth/strategies/basic"
"github.com/shaj13/go-guardian/auth/strategies/twofactor"
"github.com/shaj13/go-guardian/tfa"
)

type OTPManager struct{}

func (OTPManager) Enabled(_ auth.Info) bool { return true }

func (OTPManager) Load(_ auth.Info) (twofactor.OTP, error) {
// user otp configuration must be loaded from persistent storage
cfg := tfa.OTPConfig{
OTPType: tfa.HOTP,
Label: "LABEL",
Counter: 0,
Secret: "GXNRHI2MFRFWXQGJHWZJFOSYI6E7MEVA",
}
_, otp, err := tfa.NewOTP(&cfg)
return otp, err
}

func (OTPManager) Store(_ auth.Info, otp twofactor.OTP) error {
// persist user otp after verification
fmt.Println("Failed: ", otp.(tfa.OTP).Failed())
return nil
}

func Example() {
strategy := twofactor.Strategy{
Parser: twofactor.XHeaderParser("X-Example-OTP"),
Manager: OTPManager{},
Primary: basic.AuthenticateFunc(
func(ctx context.Context, r *http.Request, userName, password string) (auth.Info, error) {
return auth.NewDefaultUser("example", "1", nil, nil), nil
},
),
}

r, _ := http.NewRequest("GET", "/", nil)
r.SetBasicAuth("example", "example")
r.Header.Set("X-Example-OTP", "345515")

info, err := strategy.Authenticate(r.Context(), r)
fmt.Println(info.UserName(), err)

// Output:
// Failed: 0
// example <nil>
}
59 changes: 59 additions & 0 deletions auth/strategies/twofactor/parser.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package twofactor

import (
"errors"
"net/http"

"github.com/shaj13/go-guardian/internal"
)

// ErrMissingPin is returned by Parser,
// When one-time password missing or empty in HTTP request.
var ErrMissingPin = errors.New("strategies/twofactor: One-time password missing or empty")

// Parser parse and extract one-time password from incoming HTTP request.
type Parser interface {
PinCode(r *http.Request) (string, error)
}

type pinFn func(r *http.Request) (string, error)

func (fn pinFn) PinCode(r *http.Request) (string, error) {
return fn(r)
}

// XHeaderParser return a one-time password parser, where pin extracted form "X-" header.
func XHeaderParser(header string) Parser {
fn := func(r *http.Request) (string, error) {
return internal.ParseHeader(header, r, ErrMissingPin)
}

return pinFn(fn)
}

// JSONBodyParser return a one-time password parser, where pin extracted form request body.
func JSONBodyParser(key string) Parser {
fn := func(r *http.Request) (string, error) {
return internal.ParseJSONBody(key, r, ErrMissingPin)
}

return pinFn(fn)
}

// QueryParser return a one-time password parser, where pin extracted form HTTP query string.
func QueryParser(key string) Parser {
fn := func(r *http.Request) (string, error) {
return internal.ParseQuery(key, r, ErrMissingPin)
}

return pinFn(fn)
}

// CookieParser return a one-time password parser, where pin extracted form HTTP Cookie.
func CookieParser(key string) Parser {
fn := func(r *http.Request) (string, error) {
return internal.ParseCookie(key, r, ErrMissingPin)
}

return pinFn(fn)
}
125 changes: 125 additions & 0 deletions auth/strategies/twofactor/parser_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
package twofactor

import (
"net/http"
"strings"
"testing"

"github.com/stretchr/testify/assert"
)

func TestParser(t *testing.T) {
table := []struct {
name string
prepare func() (Parser, *http.Request)
err error
pin string
}{
{
name: "XHeaderParser return error when failed to parse pin",
prepare: func() (Parser, *http.Request) {
req, _ := http.NewRequest("GET", "/", nil)
parser := XHeaderParser("X-OTP")
return parser, req
},
err: ErrMissingPin,
pin: "",
},
{
name: "XHeaderParser return pin",
prepare: func() (Parser, *http.Request) {
req, _ := http.NewRequest("GET", "/", nil)
req.Header.Set("X-OTP", "123456")
parser := XHeaderParser("X-OTP")
return parser, req
},
err: nil,
pin: "123456",
},
{
name: "QueryParser return error when failed to parse pin",
prepare: func() (Parser, *http.Request) {
req, _ := http.NewRequest("GET", "/", nil)
parser := QueryParser("otp")
return parser, req
},
err: ErrMissingPin,
pin: "",
},
{
name: "QueryParser return pin",
prepare: func() (Parser, *http.Request) {
req, _ := http.NewRequest("GET", "/something?otp=123456", nil)
parser := QueryParser("otp")
return parser, req
},
err: nil,
pin: "123456",
},
{
name: "CookieParser return error when failed to find Cookie",
prepare: func() (Parser, *http.Request) {
req, _ := http.NewRequest("GET", "/", nil)
parser := CookieParser("otp")
return parser, req
},
err: http.ErrNoCookie,
pin: "",
},
{
name: "CookieParser return error when failed to parse pin",
prepare: func() (Parser, *http.Request) {
req, _ := http.NewRequest("GET", "/", nil)
cookie := &http.Cookie{Name: "otp", Value: ""}
req.AddCookie(cookie)
parser := CookieParser("otp")
return parser, req
},
err: ErrMissingPin,
pin: "",
},
{
name: "CookieParser return error when failed to parse pin",
prepare: func() (Parser, *http.Request) {
req, _ := http.NewRequest("GET", "/", nil)
cookie := &http.Cookie{Name: "otp", Value: "123456"}
req.AddCookie(cookie)
parser := CookieParser("otp")
return parser, req
},
err: nil,
pin: "123456",
},
{
name: "JSONBodyParser return otp",
prepare: func() (Parser, *http.Request) {
reader := strings.NewReader(`{"otp":"123456"}`)
req, _ := http.NewRequest("GET", "/", reader)
parser := JSONBodyParser("otp")
return parser, req
},
pin: "123456",
},
{
name: "JSONBodyParser return error when otp missing",
prepare: func() (Parser, *http.Request) {
reader := strings.NewReader(`{"pin":"123456"}`)
req, _ := http.NewRequest("GET", "/", reader)
parser := JSONBodyParser("otp")
return parser, req
},
err: ErrMissingPin,
pin: "",
},
}

for _, tt := range table {
t.Run(tt.name, func(t *testing.T) {
p, r := tt.prepare()
pin, err := p.PinCode(r)

assert.Equal(t, tt.pin, pin)
assert.Equal(t, tt.err, err)
})
}
}
Loading

0 comments on commit 339f3cb

Please sign in to comment.