From 0747039b18048dc865ebb782635c8ed6cd9eab50 Mon Sep 17 00:00:00 2001 From: Piotr Zduniak Date: Mon, 15 Dec 2014 23:18:22 +0100 Subject: [PATCH] YubiCloud support added-ish --- .gitignore | 1 + env/config.go | 3 +++ env/env.go | 3 +++ factor/authenticator.go | 38 ++++++++++++++++++++++++++++++++ factor/method.go | 7 ++++++ factor/yubicloud.go | 48 +++++++++++++++++++++++++++++++++++++++++ main.go | 10 ++++++++- models/account.go | 3 +++ routes/tokens.go | 40 ++++++++++++++++++++++++++++++---- setup/setup.go | 16 ++++++++++++++ 10 files changed, 164 insertions(+), 5 deletions(-) create mode 100644 factor/authenticator.go create mode 100644 factor/method.go create mode 100644 factor/yubicloud.go diff --git a/.gitignore b/.gitignore index 1e6f0b7..4140b23 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ *~ *.exe _vagrant/.vagrant +.config.conf diff --git a/env/config.go b/env/config.go index 69b1ff9..c07ee6a 100644 --- a/env/config.go +++ b/env/config.go @@ -18,4 +18,7 @@ type Flags struct { RethinkDBAddress string RethinkDBKey string RethinkDBDatabase string + + YubiCloudID string + YubiCloudKey string } diff --git a/env/env.go b/env/env.go index bfdac13..e7f5ec5 100644 --- a/env/env.go +++ b/env/env.go @@ -6,6 +6,7 @@ import ( "github.com/lavab/api/cache" "github.com/lavab/api/db" + "github.com/lavab/api/factor" ) var ( @@ -27,4 +28,6 @@ var ( Contacts *db.ContactsTable // Reservations is the global instance of ReservationsTable Reservations *db.ReservationsTable + // Factors contains all currently registered factors + Factors map[string]factor.Factor ) diff --git a/factor/authenticator.go b/factor/authenticator.go new file mode 100644 index 0000000..0c5b42e --- /dev/null +++ b/factor/authenticator.go @@ -0,0 +1,38 @@ +package factor + +import ( + "github.com/gokyle/hotp" +) + +type Authenticator struct { + length int +} + +func NewAuthenticator(length int) *Authenticator { + return &Authenticator{ + length: length, + } +} + +func (a *Authenticator) Type() string { + return "authenticator" +} + +func (a *Authenticator) Request(data string) (string, error) { + otp, err := hotp.GenerateHOTP(a.length, false) + if err != nil { + return "", err + } + + return otp.URL(data), nil +} + +func (a *Authenticator) Verify(data string, input string) (bool, error) { + // obviously broken + hotp, err := hotp.Unmarshal([]byte(data)) + if err != nil { + return false, err + } + + return hotp.Check(input), nil +} diff --git a/factor/method.go b/factor/method.go new file mode 100644 index 0000000..e13f320 --- /dev/null +++ b/factor/method.go @@ -0,0 +1,7 @@ +package factor + +type Factor interface { + Type() string + Request(data string) (string, error) + Verify(data string, input string) (bool, error) +} diff --git a/factor/yubicloud.go b/factor/yubicloud.go new file mode 100644 index 0000000..9f37334 --- /dev/null +++ b/factor/yubicloud.go @@ -0,0 +1,48 @@ +package factor + +import "github.com/GeertJohan/yubigo" + +// YubiCloud is an implementation of Factor to authenticate with YubiCloud +type YubiCloud struct { + client *yubigo.YubiAuth +} + +// NewYubiCloud set ups a new Factor that supports authing using YubiCloud +func NewYubiCloud(id string, key string) (*YubiCloud, error) { + client, err := yubigo.NewYubiAuth(id, key) + if err != nil { + return nil, err + } + + return &YubiCloud{ + client: client, + }, nil +} + +// Type returns factor's type +func (y *YubiCloud) Type() string { + return "yubicloud" +} + +// Request does nothing in this driver +func (y *YubiCloud) Request(data string) (string, error) { + return "", nil +} + +// Verify checks if the token is valid +func (y *YubiCloud) Verify(data string, input string) (bool, error) { + if input[:12] != data { + return false, nil + } + + _, ok, err := y.client.Verify(input) + if err != nil { + return false, err + } + + if !ok { + return false, nil + } + + return true, nil +} diff --git a/main.go b/main.go index e7393c0..7eb6bbd 100644 --- a/main.go +++ b/main.go @@ -18,9 +18,11 @@ import ( // https://github.com/unrolled/secure var ( + // Enable namsral/flag functionality + configFlag = flag.String("config", "", "config file to load") // General flags bindAddress = flag.String("bind", ":5000", "Network address used to bind") - apiVersion = flag.String("version", "v0", "Shown API version") + apiVersion = flag.String("api_version", "v0", "Shown API version") logFormatterType = flag.String("log", "text", "Log formatter type. Either \"json\" or \"text\"") forceColors = flag.Bool("force_colors", false, "Force colored prompt?") // Registration settings @@ -53,6 +55,9 @@ var ( } return database }(), "Database name on the RethinkDB server") + // YubiCloud params + yubiCloudID = flag.String("yubicloud_id", "", "YubiCloud API id") + yubiCloudKey = flag.String("yubicloud_key", "", "YubiCloud API key") ) func main() { @@ -77,6 +82,9 @@ func main() { RethinkDBAddress: *rethinkdbAddress, RethinkDBKey: *rethinkdbKey, RethinkDBDatabase: *rethinkdbDatabase, + + YubiCloudID: *yubiCloudID, + YubiCloudKey: *yubiCloudKey, } // Generate a mux diff --git a/models/account.go b/models/account.go index b2cbef5..ed77e33 100644 --- a/models/account.go +++ b/models/account.go @@ -34,6 +34,9 @@ type Account struct { AltEmail string `json:"alt_email" gorethink:"alt_email"` + FactorType string `json:"-" gorethink:"factor_type"` + FactorValue string `json:"-" gorethink:"factor_value"` + Status string `json:"status" gorethink:"status"` } diff --git a/routes/tokens.go b/routes/tokens.go index 3b11567..87d24b8 100644 --- a/routes/tokens.go +++ b/routes/tokens.go @@ -38,13 +38,16 @@ type TokensCreateRequest struct { Username string `json:"username" schema:"username"` Password string `json:"password" schema:"password"` Type string `json:"type" schema:"type"` + Token string `json:"token" schema:"token"` } // TokensCreateResponse contains the result of the TokensCreate request. type TokensCreateResponse struct { - Success bool `json:"success"` - Message string `json:"message,omitempty"` - Token *models.Token `json:"token,omitempty"` + Success bool `json:"success"` + Message string `json:"message,omitempty"` + Token *models.Token `json:"token,omitempty"` + FactorType string `json:"factor_type,omitempty"` + FactorRequest string `json:"factor_request,omitempty"` } // TokensCreate allows logging in to an account. @@ -104,12 +107,41 @@ func TokensCreate(w http.ResponseWriter, r *http.Request) { } } + // Check for 2nd factor + if user.FactorType != "" { + factor, ok := env.Factors[user.FactorType] + if ok { + if input.Token == "" { + req, err := factor.Request(user.ID) + if err == nil { + utils.JSONResponse(w, 403, &TokensCreateResponse{ + Success: false, + Message: "Factor token was not passed", + FactorType: user.FactorType, + FactorRequest: req, + }) + return + } + } else { + ok, err := factor.Verify(user.FactorValue, input.Token) + if !ok || err != nil { + utils.JSONResponse(w, 403, &TokensCreateResponse{ + Success: false, + Message: "Invalid token passed", + FactorType: user.FactorType, + }) + return + } + } + } + } + // Calculate the expiry date expDate := time.Now().Add(time.Hour * time.Duration(env.Config.SessionDuration)) // Create a new token token := &models.Token{ - Expiring: models.Expiring{expDate}, + Expiring: models.Expiring{ExpiryDate: expDate}, Resource: models.MakeResource(user.ID, "Auth token expiring on "+expDate.Format(time.RFC3339)), Type: input.Type, } diff --git a/setup/setup.go b/setup/setup.go index 07bb0fc..eabe49d 100644 --- a/setup/setup.go +++ b/setup/setup.go @@ -11,6 +11,7 @@ import ( "github.com/lavab/api/cache" "github.com/lavab/api/db" "github.com/lavab/api/env" + "github.com/lavab/api/factor" "github.com/lavab/api/routes" "github.com/lavab/glogrus" ) @@ -111,6 +112,21 @@ func PrepareMux(flags *env.Flags) *web.Mux { ), } + // Initialize factors + env.Factors = make(map[string]factor.Factor) + if flags.YubiCloudID != "" { + yubicloud, err := factor.NewYubiCloud(flags.YubiCloudID, flags.YubiCloudKey) + if err != nil { + env.Log.WithFields(logrus.Fields{ + "error": err, + }).Fatal("Unable to initiate YubiCloud") + } + env.Factors[yubicloud.Type()] = yubicloud + } + + authenticator := factor.NewAuthenticator(6) + env.Factors[authenticator.Type()] = authenticator + // Create a new goji mux mux := web.New()