auth

Paddy 2014-12-14 Parent:42bc3e44f4fe Child:3a1fe5ee17f5

99:5bccbed6631b Go to Latest

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.

History
     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 +}