diff --git a/_vagrant/Vagrantfile b/_vagrant/Vagrantfile index 15c639f..a540a0e 100644 --- a/_vagrant/Vagrantfile +++ b/_vagrant/Vagrantfile @@ -39,6 +39,7 @@ Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| docker.vm.provider "virtualbox" do |v| v.customize ["modifyvm", :id, "--natdnshostresolver1", "on"] v.customize ["modifyvm", :id, "--natdnsproxy1", "on"] + v.memory = 2048 end docker.vm.provision "docker" do |d| diff --git a/circle.yml b/circle.yml index e0897b4..ad8a647 100644 --- a/circle.yml +++ b/circle.yml @@ -3,14 +3,15 @@ machine: Europe/Berlin dependencies: + cache_directories: + - /home/ubuntu/api/redis-2.8.18 pre: - source /etc/lsb-release && echo "deb http://download.rethinkdb.com/apt $DISTRIB_CODENAME main" | sudo tee /etc/apt/sources.list.d/rethinkdb.list - wget -qO- http://download.rethinkdb.com/apt/pubkey.gpg | sudo apt-key add - - sudo apt-get update - sudo apt-get install rethinkdb - - wget http://download.redis.io/releases/redis-2.8.18.tar.gz - - tar xvzf redis-2.8.18.tar.gz - - cd redis-2.8.18 && make + - if [[ ! -e redis-2.8.18/src/redis-server ]]; then wget http://download.redis.io/releases/redis-2.8.18.tar.gz && tar xvzf redis-2.8.18.tar.gz; fi + - if [[ ! -e redis-2.8.18/src/redis-server ]]; then cd redis-2.8.18 && make; fi - go get github.com/apcera/gnatsd post: - rethinkdb --bind all: diff --git a/models/account.go b/models/account.go index 6e5f6f9..a4ec101 100644 --- a/models/account.go +++ b/models/account.go @@ -3,6 +3,7 @@ package models import ( "github.com/gyepisam/mcf" _ "github.com/gyepisam/mcf/scrypt" // Required to have mcf hash the password into scrypt + "github.com/lavab/api/factor" ) // Account stores essential data for a Lavaboom user, and is thus not encrypted. @@ -81,6 +82,26 @@ func (a *Account) VerifyPassword(password string) (bool, bool, error) { return true, false, nil } +// Verify2FA verifies the 2FA token with the account settings. +// Returns verified, challenge, error +func (a *Account) Verify2FA(factor factor.Factor, token string) (bool, string, error) { + if token == "" { + req, err := factor.Request(a.ID) + if err != nil { + return false, "", err + } + + return false, req, nil + } + + ok, err := factor.Verify(a.FactorValue, token) + if err != nil { + return false, "", err + } + + return ok, "", nil +} + // SettingsData TODO type SettingsData struct { } diff --git a/models/key.go b/models/key.go index b114ab8..c8adc1c 100644 --- a/models/key.go +++ b/models/key.go @@ -7,7 +7,6 @@ type Key struct { //Body []byte `json:"body" gorethink:"body"` // Raw key contents Headers map[string]string `json:"headers" gorethink:"headers"` // Headers passed with the key - Email string `json:"email" gorethink:"email"` // Address associated with the key Algorithm string `json:"algorithm" gorethink:"algorithm"` // Algorithm of the key Length uint16 `json:"length" gorethink:"length"` // Length of the key Key string `json:"key" gorethink:"key"` // Armor-encoded key diff --git a/routes/accounts.go b/routes/accounts.go index ff50815..824c30d 100644 --- a/routes/accounts.go +++ b/routes/accounts.go @@ -2,6 +2,7 @@ package routes import ( "net/http" + "time" "github.com/Sirupsen/logrus" "github.com/zenazn/goji/web" @@ -82,6 +83,24 @@ func AccountsCreate(w http.ResponseWriter, r *http.Request) { return } + if input.Username != "" { + if used, err := env.Reservations.IsUsernameUsed(input.Username); err != nil || used { + utils.JSONResponse(w, 400, &AccountsCreateResponse{ + Success: false, + Message: "Username already reserved", + }) + return + } + + if used, err := env.Accounts.IsUsernameUsed(input.Username); err != nil || used { + utils.JSONResponse(w, 400, &AccountsCreateResponse{ + Success: false, + Message: "Username already used", + }) + return + } + } + // Adding to [beta] queue if requestType[:5] == "queue" { if requestType[6:] == "reserve" { @@ -93,22 +112,6 @@ func AccountsCreate(w http.ResponseWriter, r *http.Request) { }) return } - - if used, err := env.Reservations.IsUsernameUsed(input.Username); err != nil || used { - utils.JSONResponse(w, 400, &AccountsCreateResponse{ - Success: false, - Message: "Username already reserved", - }) - return - } - - if used, err := env.Accounts.IsUsernameUsed(input.Username); err != nil || used { - utils.JSONResponse(w, 400, &AccountsCreateResponse{ - Success: false, - Message: "Username already used", - }) - return - } } // Ensure that the email is not already used to reserve/register @@ -205,15 +208,6 @@ func AccountsCreate(w http.ResponseWriter, r *http.Request) { // TODO: sanitize user name (i.e. remove caps, periods) - // Both invited and classic require an unique username, so ensure that the user with requested username isn't already used - if _, err := env.Accounts.FindAccountByName(input.Username); err == nil { - utils.JSONResponse(w, 409, &AccountsCreateResponse{ - Success: false, - Message: "Username already exists", - }) - return - } - // Both username and password are filled, so we can create a new account. account := &models.Account{ Resource: models.MakeResource("", input.Username), @@ -332,16 +326,21 @@ func AccountsGet(c web.C, w http.ResponseWriter, r *http.Request) { // AccountsUpdateRequest contains the input for the AccountsUpdate endpoint. type AccountsUpdateRequest struct { - AltEmail string `json:"alt_email" schema:"alt_email"` - CurrentPassword string `json:"current_password" schema:"current_password"` - NewPassword string `json:"new_password" schema:"new_password"` + AltEmail string `json:"alt_email" schema:"alt_email"` + CurrentPassword string `json:"current_password" schema:"current_password"` + NewPassword string `json:"new_password" schema:"new_password"` + FactorType string `json:"factor_type" schema:"factor_type"` + FactorValue []string `json:"factor_value" schema:"factor_value"` + Token string `json:"token" schema:"token"` } // AccountsUpdateResponse contains the result of the AccountsUpdate request. type AccountsUpdateResponse struct { - Success bool `json:"success"` - Message string `json:"message"` - Account *models.Account `json:"account"` + Success bool `json:"success"` + Message string `json:"message,omitempty"` + Account *models.Account `json:"account,omitempty"` + FactorType string `json:"factor_type,omitempty"` + FactorChallenge string `json:"factor_challenge,omitempty"` } // AccountsUpdate allows changing the account's information (password etc.) @@ -354,7 +353,7 @@ func AccountsUpdate(c web.C, w http.ResponseWriter, r *http.Request) { "error": err, }).Warn("Unable to decode a request") - utils.JSONResponse(w, 409, &AccountsUpdateResponse{ + utils.JSONResponse(w, 400, &AccountsUpdateResponse{ Success: false, Message: "Invalid input format", }) @@ -387,13 +386,63 @@ func AccountsUpdate(c web.C, w http.ResponseWriter, r *http.Request) { } if valid, _, err := user.VerifyPassword(input.CurrentPassword); err != nil || !valid { - utils.JSONResponse(w, 409, &AccountsUpdateResponse{ + utils.JSONResponse(w, 403, &AccountsUpdateResponse{ Success: false, Message: "Invalid current password", }) return } + // Check for 2nd factor + if user.FactorType != "" { + factor, ok := env.Factors[user.FactorType] + if ok { + // Verify the 2FA + verified, challenge, err := user.Verify2FA(factor, input.Token) + if err != nil { + utils.JSONResponse(w, 500, &AccountsUpdateResponse{ + Success: false, + Message: "Internal 2FA error", + }) + + env.Log.WithFields(logrus.Fields{ + "err": err.Error(), + "factor": user.FactorType, + }).Warn("2FA authentication error") + return + } + + // Token was probably empty. Return the challenge. + if !verified && challenge != "" { + utils.JSONResponse(w, 403, &AccountsUpdateResponse{ + Success: false, + Message: "2FA token was not passed", + FactorType: user.FactorType, + FactorChallenge: challenge, + }) + return + } + + // Token was incorrect + if !verified { + utils.JSONResponse(w, 403, &AccountsUpdateResponse{ + Success: false, + Message: "Invalid token passed", + FactorType: user.FactorType, + }) + return + } + } + } + + if input.NewPassword != "" && !utils.IsPasswordSecure(input.NewPassword) { + utils.JSONResponse(w, 400, &AccountsUpdateResponse{ + Success: false, + Message: "Weak new password", + }) + return + } + if input.NewPassword != "" { err = user.SetPassword(input.NewPassword) if err != nil { @@ -413,6 +462,25 @@ func AccountsUpdate(c web.C, w http.ResponseWriter, r *http.Request) { user.AltEmail = input.AltEmail } + if input.FactorType != "" { + // Check if such factor exists + if _, exists := env.Factors[input.FactorType]; !exists { + utils.JSONResponse(w, 400, &AccountsUpdateResponse{ + Success: false, + Message: "Invalid new 2FA type", + }) + return + } + + user.FactorType = input.FactorType + } + + if input.FactorValue != nil && len(input.FactorValue) > 0 { + user.FactorValue = input.FactorValue + } + + user.DateModified = time.Now() + err = env.Accounts.UpdateID(session.Owner, user) if err != nil { env.Log.WithFields(logrus.Fields{ diff --git a/routes/accounts_test.go b/routes/accounts_test.go index 240d34a..de7637d 100644 --- a/routes/accounts_test.go +++ b/routes/accounts_test.go @@ -81,8 +81,8 @@ func TestAccountsCreateInvited(t *testing.T) { require.Nil(t, err) // Check the result's contents - require.True(t, response1.Success) require.Equal(t, "A new account was successfully created", response1.Message) + require.True(t, response1.Success) require.NotEmpty(t, response1.Account.ID) accountID = response1.Account.ID @@ -146,12 +146,12 @@ func TestAccountsCreateInvitedExisting(t *testing.T) { // Check the result's contents require.False(t, response.Success) - require.Equal(t, "Username already exists", response.Message) + require.Equal(t, "Username already used", response.Message) } func TestAccountsCreateInvitedWeakPassword(t *testing.T) { const ( - username = "jeremy" + username = "jeremylicious" password = "c0067d4af4e87f00dbac63b6156828237059172d1bbeac67427345d6a9fda484" ) diff --git a/routes/avatars.go b/routes/avatars.go new file mode 100644 index 0000000..c050f6b --- /dev/null +++ b/routes/avatars.go @@ -0,0 +1,86 @@ +package routes + +import ( + "crypto/md5" + "encoding/hex" + "image/color" + "image/png" + "net/http" + "strconv" + + "github.com/lavab/api/utils" + + "github.com/cupcake/sigil/gen" + "github.com/zenazn/goji/web" +) + +var avatarConfig = gen.Sigil{ + Rows: 5, + Foreground: []color.NRGBA{ + color.NRGBA{45, 79, 255, 255}, + color.NRGBA{254, 180, 44, 255}, + color.NRGBA{226, 121, 234, 255}, + color.NRGBA{30, 179, 253, 255}, + color.NRGBA{232, 77, 65, 255}, + color.NRGBA{49, 203, 115, 255}, + color.NRGBA{141, 69, 170, 255}, + }, + Background: color.NRGBA{224, 224, 224, 255}, +} + +func Avatars(c web.C, w http.ResponseWriter, r *http.Request) { + // Parse the query params + query := r.URL.Query() + + // Settings + var ( + widthString = query.Get("width") + width int + ) + + // Read width + if widthString == "" { + width = 100 + } else { + var err error + width, err = strconv.Atoi(widthString) + if err != nil { + utils.JSONResponse(w, 400, map[string]interface{}{ + "message": "Invalid width", + }) + return + } + } + + hash := c.URLParams["hash"] + ext := c.URLParams["ext"] + + if ext != "png" && ext != "svg" { + ext = "png" + } + + // data to parse + var data []byte + + // md5 hash + if len(hash) == 32 { + data, _ = hex.DecodeString(hash) + } + + // not md5 + if data == nil { + hashed := md5.Sum([]byte(hash)) + data = hashed[:] + } + + // if svg + if ext == "svg" { + w.Header().Set("Content-Type", "image/svg+xml") + avatarConfig.MakeSVG(w, width, false, data) + return + } + + // generate the png + w.Header().Set("Content-Type", "image/png") + png.Encode(w, avatarConfig.Make(width, false, data)) +} diff --git a/routes/contacts.go b/routes/contacts.go index 0e46ef4..6118540 100644 --- a/routes/contacts.go +++ b/routes/contacts.go @@ -45,11 +45,12 @@ func ContactsList(c web.C, w http.ResponseWriter, r *http.Request) { // ContactsCreateRequest is the payload that user should pass to POST /contacts type ContactsCreateRequest struct { - Data string `json:"data" schema:"data"` - Name string `json:"name" schema:"name"` - Encoding string `json:"encoding" schema:"encoding"` - VersionMajor int `json:"version_major" schema:"version_major"` - VersionMinor int `json:"version_minor" schema:"version_minor"` + Data string `json:"data" schema:"data"` + Name string `json:"name" schema:"name"` + Encoding string `json:"encoding" schema:"encoding"` + VersionMajor int `json:"version_major" schema:"version_major"` + VersionMinor int `json:"version_minor" schema:"version_minor"` + PGPFingerprints []string `json:"pgp_fingerprints" schema:"pgp_fingerprints"` } // ContactsCreateResponse contains the result of the ContactsCreate request. @@ -80,7 +81,8 @@ func ContactsCreate(c web.C, w http.ResponseWriter, r *http.Request) { session := c.Env["token"].(*models.Token) // Ensure that the input data isn't empty - if input.Data == "" || input.Name == "" || input.Encoding == "" { + if input.Data == "" || input.Name == "" || input.Encoding == "" || + input.PGPFingerprints == nil || len(input.PGPFingerprints) == 0 { utils.JSONResponse(w, 400, &ContactsCreateResponse{ Success: false, Message: "Invalid request", @@ -91,11 +93,12 @@ func ContactsCreate(c web.C, w http.ResponseWriter, r *http.Request) { // Create a new contact struct contact := &models.Contact{ Encrypted: models.Encrypted{ - Encoding: input.Encoding, - Data: input.Data, - Schema: "contact", - VersionMajor: input.VersionMajor, - VersionMinor: input.VersionMinor, + Encoding: input.Encoding, + Data: input.Data, + Schema: "contact", + VersionMajor: input.VersionMajor, + VersionMinor: input.VersionMinor, + PGPFingerprints: input.PGPFingerprints, }, Resource: models.MakeResource(session.Owner, input.Name), } @@ -160,11 +163,12 @@ func ContactsGet(c web.C, w http.ResponseWriter, r *http.Request) { // ContactsUpdateRequest is the payload passed to PUT /contacts/:id type ContactsUpdateRequest struct { - Data string `json:"data" schema:"data"` - Name string `json:"name" schema:"name"` - Encoding string `json:"encoding" schema:"encoding"` - VersionMajor int `json:"version_major" schema:"version_major"` - VersionMinor int `json:"version_minor" schema:"version_minor"` + Data string `json:"data" schema:"data"` + Name string `json:"name" schema:"name"` + Encoding string `json:"encoding" schema:"encoding"` + VersionMajor *int `json:"version_major" schema:"version_major"` + VersionMinor *int `json:"version_minor" schema:"version_minor"` + PGPFingerprints []string `json:"pgp_fingerprints" schema:"pgp_fingerprints"` } // ContactsUpdateResponse contains the result of the ContactsUpdate request. @@ -213,14 +217,32 @@ func ContactsUpdate(c web.C, w http.ResponseWriter, r *http.Request) { return } + if input.Data != "" { + contact.Data = input.Data + } + + if input.Name != "" { + contact.Name = input.Name + } + + if input.Encoding != "" { + contact.Encoding = input.Encoding + } + + if input.VersionMajor != nil { + contact.VersionMajor = *input.VersionMajor + } + + if input.VersionMinor != nil { + contact.VersionMinor = *input.VersionMinor + } + + if input.PGPFingerprints != nil { + contact.PGPFingerprints = input.PGPFingerprints + } + // Perform the update - err = env.Contacts.UpdateID(c.URLParams["id"], map[string]interface{}{ - "data": input.Data, - "name": input.Name, - "encoding": input.Encoding, - "version_major": input.VersionMajor, - "version_minor": input.VersionMinor, - }) + err = env.Contacts.UpdateID(c.URLParams["id"], input) if err != nil { env.Log.WithFields(logrus.Fields{ "error": err, @@ -234,13 +256,6 @@ func ContactsUpdate(c web.C, w http.ResponseWriter, r *http.Request) { return } - // Update the original struct for the response - contact.Data = input.Data - contact.Name = input.Name - contact.Encoding = input.Encoding - contact.VersionMajor = input.VersionMajor - contact.VersionMinor = input.VersionMinor - // Write the contact to the response utils.JSONResponse(w, 200, &ContactsUpdateResponse{ Success: true, diff --git a/routes/contacts_test.go b/routes/contacts_test.go index e2a5ee7..6468423 100644 --- a/routes/contacts_test.go +++ b/routes/contacts_test.go @@ -82,11 +82,12 @@ func TestContactsCreate(t *testing.T) { Uri: server.URL + "/contacts", ContentType: "application/json", Body: routes.ContactsCreateRequest{ - Data: "random stuff", - Name: "John Doe", - Encoding: "json", - VersionMajor: 1, - VersionMinor: 0, + Data: "random stuff", + Name: "John Doe", + Encoding: "json", + VersionMajor: 1, + VersionMinor: 0, + PGPFingerprints: []string{"that's totally a fingerprint!"}, }, } request.AddHeader("Authorization", "Bearer "+authToken) @@ -233,6 +234,9 @@ func TestContactsGetWrongID(t *testing.T) { } func TestContactsUpdate(t *testing.T) { + int1 := 1 + int0 := 0 + request := goreq.Request{ Method: "PUT", Uri: server.URL + "/contacts/" + contactID, @@ -241,8 +245,8 @@ func TestContactsUpdate(t *testing.T) { Data: "random stuff2", Name: "John Doez", Encoding: "json", - VersionMajor: 1, - VersionMinor: 0, + VersionMajor: &int1, + VersionMinor: &int0, }, } request.AddHeader("Authorization", "Bearer "+authToken) @@ -277,6 +281,9 @@ func TestContactsUpdateInvalid(t *testing.T) { } func TestContactsUpdateNotOwned(t *testing.T) { + int1 := 1 + int0 := 0 + request := goreq.Request{ Method: "PUT", Uri: server.URL + "/contacts/" + notOwnedContactID, @@ -285,8 +292,8 @@ func TestContactsUpdateNotOwned(t *testing.T) { Data: "random stuff2", Name: "John Doez", Encoding: "json", - VersionMajor: 1, - VersionMinor: 0, + VersionMajor: &int1, + VersionMinor: &int0, }, } request.AddHeader("Authorization", "Bearer "+authToken) @@ -302,6 +309,9 @@ func TestContactsUpdateNotOwned(t *testing.T) { } func TestContactsUpdateNotExisting(t *testing.T) { + int1 := 1 + int0 := 0 + request := goreq.Request{ Method: "PUT", Uri: server.URL + "/contacts/gibberish", @@ -310,8 +320,8 @@ func TestContactsUpdateNotExisting(t *testing.T) { Data: "random stuff2", Name: "John Doez", Encoding: "json", - VersionMajor: 1, - VersionMinor: 0, + VersionMajor: &int1, + VersionMinor: &int0, }, } request.AddHeader("Authorization", "Bearer "+authToken) diff --git a/routes/emails.go b/routes/emails.go index 612324b..8f0222b 100644 --- a/routes/emails.go +++ b/routes/emails.go @@ -121,15 +121,20 @@ func EmailsList(c web.C, w http.ResponseWriter, r *http.Request) { } type EmailsCreateRequest struct { - To []string `json:"to"` - BCC []string `json:"bcc"` - ReplyTo string `json:"reply_to"` - ThreadID string `json:"thread_id"` - Subject string `json:"title"` - Body string `json:"body"` - Preview string `json:"preview"` - Attachments []string `json:"attachments"` - PGPFingerprints []string `json:"pgp_fingerprints"` + To []string `json:"to"` + BCC []string `json:"bcc"` + ReplyTo string `json:"reply_to"` + ThreadID string `json:"thread_id"` + Subject string `json:"title"` + Body string `json:"body"` + BodyVersionMajor int `json:"body_version_major"` + BodyVersionMinor int `json:"body_version_minor"` + Preview string `json:"preview"` + PreviewVersionMajor int `json:"preview_version_major"` + PreviewVersionMinor int `json:"preview_version_minor"` + Encoding string `json:"encoding"` + Attachments []string `json:"attachments"` + PGPFingerprints []string `json:"pgp_fingerprints"` } // EmailsCreateResponse contains the result of the EmailsCreate request. @@ -179,16 +184,16 @@ func EmailsCreate(c web.C, w http.ResponseWriter, r *http.Request) { PGPFingerprints: input.PGPFingerprints, Data: input.Body, Schema: "email_body", - VersionMajor: 1, - VersionMinor: 0, + VersionMajor: input.BodyVersionMajor, + VersionMinor: input.BodyVersionMinor, }, Preview: models.Encrypted{ Encoding: "json", PGPFingerprints: input.PGPFingerprints, Data: input.Preview, Schema: "email_preview", - VersionMajor: 1, - VersionMinor: 0, + VersionMajor: input.PreviewVersionMajor, + VersionMinor: input.PreviewVersionMinor, }, ThreadID: input.ThreadID, Status: "queued", diff --git a/routes/keys.go b/routes/keys.go index 4328583..b86ff27 100644 --- a/routes/keys.go +++ b/routes/keys.go @@ -18,9 +18,9 @@ import ( // KeysListResponse contains the result of the KeysList request type KeysListResponse struct { - Success bool `json:"success"` - Message string `json:"message,omitempty"` - Keys *[]string `json:"keys,omitempty"` + Success bool `json:"success"` + Message string `json:"message,omitempty"` + Keys *[]*models.Key `json:"keys,omitempty"` } // KeysList responds with the list of keys assigned to the spiecified email @@ -54,16 +54,10 @@ func KeysList(w http.ResponseWriter, r *http.Request) { return } - // Equivalent of _.keys(keys) in JavaScript with underscore.js - keyIDs := []string{} - for _, key := range keys { - keyIDs = append(keyIDs, key.ID) - } - // Respond with list of keys utils.JSONResponse(w, 200, &KeysListResponse{ Success: true, - Keys: &keyIDs, + Keys: &keys, }) } @@ -206,21 +200,94 @@ type KeysGetResponse struct { // KeysGet does *something* - TODO func KeysGet(c web.C, w http.ResponseWriter, r *http.Request) { + // Initialize vars + var ( + key *models.Key + ) + // Get ID from the passed URL params id := c.URLParams["id"] - // Fetch the requested key from the database - key, err := env.Keys.FindByFingerprint(id) - if err != nil { - env.Log.WithFields(logrus.Fields{ - "error": err, - }).Warn("Unable to fetch the requested key from the database") - - utils.JSONResponse(w, 404, &KeysGetResponse{ - Success: false, - Message: "Requested key does not exist on our server", - }) - return + // Check if ID is an email or a fingerprint. + // Fingerprints can't contain @, right? + if strings.Contains(id, "@") { + // Who cares about the second part? I don't! + username := strings.Split(id, "@")[0] + + // Resolve account + account, err := env.Accounts.FindAccountByName(username) + if err != nil { + env.Log.WithFields(logrus.Fields{ + "error": err, + "name": username, + }).Warn("Unable to fetch the requested account from the database") + + utils.JSONResponse(w, 404, &KeysGetResponse{ + Success: false, + Message: "No such user", + }) + return + } + + // Does the user have a default PGP key set? + if account.PGPKey != "" { + // Fetch the requested key from the database + key2, err := env.Keys.FindByFingerprint(account.PGPKey) + if err != nil { + env.Log.WithFields(logrus.Fields{ + "error": err, + }).Warn("Unable to fetch the requested key from the database") + + utils.JSONResponse(w, 500, &KeysGetResponse{ + Success: false, + Message: "Invalid user public key ID", + }) + return + } + + key = key2 + } else { + keys, err := env.Keys.FindByOwner(account.ID) + if err != nil { + env.Log.WithFields(logrus.Fields{ + "error": err, + "owner": account.ID, + }).Warn("Unable to fetch user's keys from the database") + + utils.JSONResponse(w, 500, &KeysGetResponse{ + Success: false, + Message: "Cannot find keys assigned to the account", + }) + return + } + + if len(keys) == 0 { + utils.JSONResponse(w, 500, &KeysGetResponse{ + Success: false, + Message: "Account has no keys assigned to itself", + }) + return + } + + // i should probably sort them? + key = keys[0] + } + } else { + // Fetch the requested key from the database + key2, err := env.Keys.FindByFingerprint(id) + if err != nil { + env.Log.WithFields(logrus.Fields{ + "error": err, + }).Warn("Unable to fetch the requested key from the database") + + utils.JSONResponse(w, 404, &KeysGetResponse{ + Success: false, + Message: "Requested key does not exist on our server", + }) + return + } + + key = key2 } // Return the requested key diff --git a/routes/tokens.go b/routes/tokens.go index 87d24b8..0bcc760 100644 --- a/routes/tokens.go +++ b/routes/tokens.go @@ -14,22 +14,43 @@ import ( // TokensGetResponse contains the result of the TokensGet request. type TokensGetResponse struct { - Success bool `json:"success"` - Message string `json:"message,omitempty"` - Created *time.Time `json:"created,omitempty"` - Expires *time.Time `json:"expires,omitempty"` + Success bool `json:"success"` + Message string `json:"message,omitempty"` + Token *models.Token `json:"token,omitempty"` } // TokensGet returns information about the current token. func TokensGet(c web.C, w http.ResponseWriter, r *http.Request) { - // Fetch the current session from the database - session := c.Env["token"].(*models.Token) + // Initialize + var ( + token *models.Token + err error + ) + + id, ok := c.URLParams["id"] + if !ok || id == "" { + // Get the token from the middleware + token = c.Env["token"].(*models.Token) + } else { + token, err = env.Tokens.GetToken(id) + if err != nil { + env.Log.WithFields(logrus.Fields{ + "error": err, + "id": id, + }).Warn("Unable to find the token") + + utils.JSONResponse(w, 404, &TokensGetResponse{ + Success: false, + Message: "Invalid token ID", + }) + return + } + } // Respond with the token information utils.JSONResponse(w, 200, &TokensGetResponse{ Success: true, - Created: &session.DateCreated, - Expires: &session.ExpiryDate, + Token: token, }) } @@ -43,11 +64,11 @@ type TokensCreateRequest struct { // 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"` - FactorType string `json:"factor_type,omitempty"` - FactorRequest string `json:"factor_request,omitempty"` + Success bool `json:"success"` + Message string `json:"message,omitempty"` + Token *models.Token `json:"token,omitempty"` + FactorType string `json:"factor_type,omitempty"` + FactorChallenge string `json:"factor_challenge,omitempty"` } // TokensCreate allows logging in to an account. @@ -98,12 +119,15 @@ func TokensCreate(w http.ResponseWriter, r *http.Request) { // Update the user if password was updated if updated { + user.DateModified = time.Now() err := env.Accounts.UpdateID(user.ID, user) if err != nil { env.Log.WithFields(logrus.Fields{ "user": user.Name, "error": err, }).Error("Could not update user") + + // DO NOT RETURN! } } @@ -111,27 +135,40 @@ func TokensCreate(w http.ResponseWriter, r *http.Request) { 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 - } + // Verify the 2FA + verified, challenge, err := user.Verify2FA(factor, input.Token) + if err != nil { + utils.JSONResponse(w, 500, &TokensCreateResponse{ + Success: false, + Message: "Internal 2FA error", + }) + + env.Log.WithFields(logrus.Fields{ + "err": err.Error(), + "factor": user.FactorType, + }).Warn("2FA authentication error") + return + } + + // Token was probably empty. Return the challenge. + if !verified && challenge != "" { + utils.JSONResponse(w, 403, &TokensCreateResponse{ + Success: false, + Message: "2FA token was not passed", + FactorType: user.FactorType, + FactorChallenge: challenge, + }) + return + } + + // Token was incorrect + if !verified { + utils.JSONResponse(w, 403, &TokensCreateResponse{ + Success: false, + Message: "Invalid token passed", + FactorType: user.FactorType, + }) + return } } } diff --git a/routes/tokens_test.go b/routes/tokens_test.go index 6fcf270..487de7e 100644 --- a/routes/tokens_test.go +++ b/routes/tokens_test.go @@ -181,7 +181,7 @@ func TestTokensGet(t *testing.T) { require.Nil(t, err) require.True(t, response.Success) - require.True(t, response.Expires.After(time.Now().UTC())) + require.True(t, response.Token.ExpiryDate.After(time.Now().UTC())) } func TestTokensDeleteById(t *testing.T) { diff --git a/setup/setup.go b/setup/setup.go index 06219d0..44ec365 100644 --- a/setup/setup.go +++ b/setup/setup.go @@ -90,6 +90,21 @@ func PrepareMux(flags *env.Flags) *web.Mux { // Put the RethinkDB session into the environment package env.Rethink = rethinkSession + // 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 + // Initialize the tables env.Tokens = &db.TokensTable{ RethinkCRUD: db.NewCRUDTable( @@ -235,21 +250,6 @@ func PrepareMux(flags *env.Flags) *web.Mux { env.NATS = c - // 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() @@ -281,8 +281,12 @@ func PrepareMux(flags *env.Flags) *web.Mux { auth.Delete("/accounts/:id", routes.AccountsDelete) auth.Post("/accounts/:id/wipe-data", routes.AccountsWipeData) + // Avatars + mux.Get("/avatars/:hash.:ext", routes.Avatars) + // Tokens auth.Get("/tokens", routes.TokensGet) + auth.Get("/tokens/:id", routes.TokensGet) mux.Post("/tokens", routes.TokensCreate) auth.Delete("/tokens", routes.TokensDelete) auth.Delete("/tokens/:id", routes.TokensDelete)