Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
*~
*.exe
_vagrant/.vagrant
.config.conf
3 changes: 3 additions & 0 deletions env/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,7 @@ type Flags struct {
RethinkDBAddress string
RethinkDBKey string
RethinkDBDatabase string

YubiCloudID string
YubiCloudKey string
}
3 changes: 3 additions & 0 deletions env/env.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (

"github.com/lavab/api/cache"
"github.com/lavab/api/db"
"github.com/lavab/api/factor"
)

var (
Expand All @@ -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
)
38 changes: 38 additions & 0 deletions factor/authenticator.go
Original file line number Diff line number Diff line change
@@ -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
}
7 changes: 7 additions & 0 deletions factor/method.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package factor

type Factor interface {
Type() string
Request(data string) (string, error)
Verify(data string, input string) (bool, error)
}
48 changes: 48 additions & 0 deletions factor/yubicloud.go
Original file line number Diff line number Diff line change
@@ -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
}
10 changes: 9 additions & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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() {
Expand All @@ -77,6 +82,9 @@ func main() {
RethinkDBAddress: *rethinkdbAddress,
RethinkDBKey: *rethinkdbKey,
RethinkDBDatabase: *rethinkdbDatabase,

YubiCloudID: *yubiCloudID,
YubiCloudKey: *yubiCloudKey,
}

// Generate a mux
Expand Down
3 changes: 3 additions & 0 deletions models/account.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
}

Expand Down
40 changes: 36 additions & 4 deletions routes/tokens.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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,
}
Expand Down
16 changes: 16 additions & 0 deletions setup/setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down Expand Up @@ -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()

Expand Down