auth
auth/profile.go
Add an endpoint to validate and register profiles. Add a newProfileRequest object that defines the user-specified properties of a new Profile. Add a helper that validates a newProfileRequest and modifies it for sanitization, mostly just removing leading and trailing whitespace. Add MaxNameLength, MaxUsernameLength, and MaxEmailLength constants to hold the maximum length for those properties. Add errors to be returned when a users attempts to log in with a profile that is compromised or locked. Add the bare bones of a CreateProfileHandler that validates a profile registration request adn uses it to create a Profile and at least one Login. Create a requestError struct that is used for returning API errors, along with constants for the slugs we'll use to signal those errors.
1.1 --- a/profile.go Sun Dec 14 12:05:38 2014 -0500 1.2 +++ b/profile.go Sun Dec 14 12:09:56 2014 -0500 1.3 @@ -1,10 +1,16 @@ 1.4 package auth 1.5 1.6 import ( 1.7 + "encoding/json" 1.8 "errors" 1.9 + "net/http" 1.10 + "regexp" 1.11 + "strings" 1.12 "time" 1.13 1.14 "code.secondbit.org/uuid" 1.15 + 1.16 + "github.com/extemporalgenome/slug" 1.17 ) 1.18 1.19 const ( 1.20 @@ -14,6 +20,12 @@ 1.21 MaxPassphraseLength = 64 1.22 // CurPassphraseScheme is the current passphrase scheme. Incrememnt it when we use a different passphrase scheme 1.23 CurPassphraseScheme = 1 1.24 + // MaxNameLength is the maximum length, in bytes, of a name, exclusive. 1.25 + MaxNameLength = 64 1.26 + // MaxUsernameLength is the maximum length, in bytes, of a username, exclusive. 1.27 + MaxUsernameLength = 16 1.28 + // MaxEmailLength is the maximum length, in bytes, of an email address, exclusive. 1.29 + MaxEmailLength = 64 1.30 ) 1.31 1.32 var ( 1.33 @@ -45,6 +57,13 @@ 1.34 // ErrPassphraseTooLong is returned when a ProfileChange is validated and contains a Passphrase, 1.35 // but the Passphrase is longer than MaxPassphraseLength. 1.36 ErrPassphraseTooLong = errors.New("passphrase too long") 1.37 + 1.38 + // ErrProfileCompromised is returned when a user tries to log in with a profile that is suspected 1.39 + // of being compromised. 1.40 + ErrProfileCompromised = errors.New("profile compromised") 1.41 + // ErrProfileLocked is returned when a user tries to log in with a profile that is locked for a certain 1.42 + // duration, to prevent brute force attacks. 1.43 + ErrProfileLocked = errors.New("profile locked") 1.44 ) 1.45 1.46 // Profile represents a single user of the service, 1.47 @@ -183,6 +202,64 @@ 1.48 LastUsed time.Time 1.49 } 1.50 1.51 +type newProfileRequest struct { 1.52 + Username string `json:"username"` 1.53 + Email string `json:"email"` 1.54 + Passphrase string `json:"passphrase"` 1.55 + Name string `json:"name"` 1.56 +} 1.57 + 1.58 +func validateNewProfileRequest(req *newProfileRequest) []requestError { 1.59 + errors := []requestError{} 1.60 + req.Name = strings.TrimSpace(req.Name) 1.61 + req.Email = strings.TrimSpace(req.Email) 1.62 + req.Username = slug.SlugAscii(strings.TrimSpace(req.Username)) 1.63 + if len(req.Passphrase) < MinPassphraseLength { 1.64 + errors = append(errors, requestError{ 1.65 + Slug: requestErrInsufficient, 1.66 + Field: "/passphrase", 1.67 + }) 1.68 + } 1.69 + if len(req.Passphrase) > MaxPassphraseLength { 1.70 + errors = append(errors, requestError{ 1.71 + Slug: requestErrOverflow, 1.72 + Field: "/passphrase", 1.73 + }) 1.74 + } 1.75 + if len(req.Name) > MaxNameLength { 1.76 + errors = append(errors, requestError{ 1.77 + Slug: requestErrOverflow, 1.78 + Field: "/name", 1.79 + }) 1.80 + } 1.81 + if len(req.Username) > MaxUsernameLength { 1.82 + errors = append(errors, requestError{ 1.83 + Slug: requestErrOverflow, 1.84 + Field: "/username", 1.85 + }) 1.86 + } 1.87 + if req.Email == "" { 1.88 + errors = append(errors, requestError{ 1.89 + Slug: requestErrMissing, 1.90 + Field: "/email", 1.91 + }) 1.92 + } 1.93 + if len(req.Email) > MaxEmailLength { 1.94 + errors = append(errors, requestError{ 1.95 + Slug: requestErrOverflow, 1.96 + Field: "/email", 1.97 + }) 1.98 + } 1.99 + re := regexp.MustCompile(".+@.+\\..+") 1.100 + if !re.Match([]byte(req.Email)) { 1.101 + errors = append(errors, requestError{ 1.102 + Slug: requestErrInvalidValue, 1.103 + Field: "/email", 1.104 + }) 1.105 + } 1.106 + return errors 1.107 +} 1.108 + 1.109 type profileStore interface { 1.110 getProfileByID(id uuid.ID) (Profile, error) 1.111 getProfileByLogin(value string) (Profile, error) 1.112 @@ -344,3 +421,71 @@ 1.113 } 1.114 return logins, nil 1.115 } 1.116 + 1.117 +// CreateProfileHandler is an HTTP handler for registering new profiles. 1.118 +func CreateProfileHandler(w http.ResponseWriter, r *http.Request, context Context) { 1.119 + scheme, ok := passphraseSchemes[CurPassphraseScheme] 1.120 + if !ok { 1.121 + // TODO(paddy): write error 1.122 + return 1.123 + } 1.124 + var req newProfileRequest 1.125 + errors := []requestError{} 1.126 + decoder := json.NewDecoder(r.Body) 1.127 + err := decoder.Decode(&req) 1.128 + if err != nil { 1.129 + // TODO(paddy): write error 1.130 + return 1.131 + } 1.132 + errors = append(errors, validateNewProfileRequest(&req)...) 1.133 + if len(errors) > 0 { 1.134 + //TODO(paddy): return errors 1.135 + return 1.136 + } 1.137 + passphrase, salt, err := scheme.create(req.Passphrase, context.config.iterations) 1.138 + if err != nil { 1.139 + // TODO(paddy): write error 1.140 + return 1.141 + } 1.142 + profile := Profile{ 1.143 + ID: uuid.NewID(), 1.144 + Name: req.Name, 1.145 + Passphrase: string(passphrase), 1.146 + Iterations: context.config.iterations, 1.147 + Salt: string(salt), 1.148 + PassphraseScheme: CurPassphraseScheme, 1.149 + Created: time.Now(), 1.150 + LastSeen: time.Now(), 1.151 + } 1.152 + err = context.SaveProfile(profile) 1.153 + if err != nil { 1.154 + // TODO(paddy): write error 1.155 + return 1.156 + } 1.157 + logins := []Login{} 1.158 + login := Login{ 1.159 + Type: "email", 1.160 + Value: req.Email, 1.161 + Created: profile.Created, 1.162 + LastUsed: profile.Created, 1.163 + ProfileID: profile.ID, 1.164 + } 1.165 + err = context.AddLogin(login) 1.166 + if err != nil { 1.167 + // TODO(paddy): write error 1.168 + return 1.169 + } 1.170 + logins = append(logins, login) 1.171 + if req.Username != "" { 1.172 + login.Type = "username" 1.173 + login.Value = req.Username 1.174 + err = context.AddLogin(login) 1.175 + if err != nil { 1.176 + // TODO(paddy): write error 1.177 + return 1.178 + } 1.179 + logins = append(logins, login) 1.180 + } 1.181 + // TODO(paddy): respond with login(s) and profile that were created 1.182 + // TODO(paddy): should we kick off the email validation flow? 1.183 +}