auth
98:09c47387e455 Browse Files
Move login concerns to session, add login handler. Move all our helpers for authenticating, building a login redirect, and reading a cookie to session.go. Rewrite our passphrase scheme code so that a scheme is just a struct with three functions for checking a passphrase against a profile object, generating a passphrase, and calculating the number of iterations to use when generating a passphrase. Define an implementation of our passphrase scheme (scheme #1) using PBKDF2 and SHA256. Add a CreateSessionHandler function that logs the user in using their login and passphrase. Add a RegisterSessionHandlers function that adds the session-related handlers (right now, just our CreateSessionHandler) to the specified router.
1.1 --- a/oauth2.go Sun Dec 14 12:01:44 2014 -0500 1.2 +++ b/oauth2.go Sun Dec 14 12:05:38 2014 -0500 1.3 @@ -1,8 +1,6 @@ 1.4 package auth 1.5 1.6 import ( 1.7 - "crypto/sha256" 1.8 - "encoding/hex" 1.9 "encoding/json" 1.10 "errors" 1.11 "html/template" 1.12 @@ -12,7 +10,6 @@ 1.13 "sync" 1.14 "time" 1.15 1.16 - "code.secondbit.org/pass" 1.17 "code.secondbit.org/uuid" 1.18 1.19 "github.com/gorilla/mux" 1.20 @@ -129,61 +126,6 @@ 1.21 return true 1.22 } 1.23 1.24 -func checkCookie(r *http.Request, context Context) (Session, error) { 1.25 - cookie, err := r.Cookie(authCookieName) 1.26 - if err == http.ErrNoCookie { 1.27 - return Session{}, ErrNoSession 1.28 - } else if err != nil { 1.29 - log.Println(err) 1.30 - return Session{}, err 1.31 - } 1.32 - sess, err := context.GetSession(cookie.Value) 1.33 - if err == ErrSessionNotFound { 1.34 - return Session{}, ErrInvalidSession 1.35 - } else if err != nil { 1.36 - return Session{}, err 1.37 - } 1.38 - if !sess.Active { 1.39 - return Session{}, ErrInvalidSession 1.40 - } 1.41 - return sess, nil 1.42 -} 1.43 - 1.44 -func buildLoginRedirect(r *http.Request, context Context) string { 1.45 - if context.loginURI == nil { 1.46 - return "" 1.47 - } 1.48 - uri := *context.loginURI 1.49 - q := uri.Query() 1.50 - q.Set("from", r.URL.String()) 1.51 - uri.RawQuery = q.Encode() 1.52 - return uri.String() 1.53 -} 1.54 - 1.55 -func authenticate(user, passphrase string, context Context) (Profile, error) { 1.56 - profile, err := context.GetProfileByLogin(user) 1.57 - if err != nil { 1.58 - if err == ErrProfileNotFound || err == ErrLoginNotFound { 1.59 - return Profile{}, ErrIncorrectAuth 1.60 - } 1.61 - return Profile{}, err 1.62 - } 1.63 - switch profile.PassphraseScheme { 1.64 - case 1: 1.65 - realPass, err := hex.DecodeString(profile.Passphrase) 1.66 - if err != nil { 1.67 - return Profile{}, err 1.68 - } 1.69 - candidate := pass.Check(sha256.New, profile.Iterations, []byte(passphrase), []byte(profile.Salt)) 1.70 - if !pass.Compare(candidate, realPass) { 1.71 - return Profile{}, ErrIncorrectAuth 1.72 - } 1.73 - default: 1.74 - return Profile{}, ErrInvalidPassphraseScheme 1.75 - } 1.76 - return profile, nil 1.77 -} 1.78 - 1.79 func wrap(context Context, f func(w http.ResponseWriter, r *http.Request, context Context)) http.Handler { 1.80 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 1.81 f(w, r, context)
2.1 --- a/session.go Sun Dec 14 12:01:44 2014 -0500 2.2 +++ b/session.go Sun Dec 14 12:05:38 2014 -0500 2.3 @@ -1,11 +1,22 @@ 2.4 package auth 2.5 2.6 import ( 2.7 + "crypto/sha256" 2.8 + "encoding/hex" 2.9 "errors" 2.10 + "log" 2.11 + "net/http" 2.12 "sort" 2.13 "time" 2.14 2.15 + "code.secondbit.org/pass" 2.16 "code.secondbit.org/uuid" 2.17 + 2.18 + "github.com/gorilla/mux" 2.19 +) 2.20 + 2.21 +const ( 2.22 + loginTemplateName = "login" 2.23 ) 2.24 2.25 var ( 2.26 @@ -17,8 +28,22 @@ 2.27 ErrInvalidSession = errors.New("session is not valid") 2.28 // ErrSessionAlreadyExists is returned when a sessionStore tries to store a Session with an ID that already exists in the sessionStore. 2.29 ErrSessionAlreadyExists = errors.New("session already exists") 2.30 + 2.31 + passphraseSchemes = map[int]passphraseScheme{ 2.32 + 1: { 2.33 + check: pbkdf2sha256check, 2.34 + create: pbkdf2sha256create, 2.35 + calculateIterations: pbkdf2sha256calc, 2.36 + }, 2.37 + } 2.38 ) 2.39 2.40 +type passphraseScheme struct { 2.41 + check func(profile Profile, passphrase string) (bool, error) 2.42 + create func(passphrase string, iterations int) (result, salt []byte, err error) 2.43 + calculateIterations func() (int, error) 2.44 +} 2.45 + 2.46 // Session represents a user's authenticated session, associating it with a profile 2.47 // and some audit data. 2.48 type Session struct { 2.49 @@ -26,8 +51,8 @@ 2.50 IP string 2.51 UserAgent string 2.52 ProfileID uuid.ID 2.53 + Login string 2.54 Created time.Time 2.55 - Login string 2.56 Active bool 2.57 } 2.58 2.59 @@ -102,3 +127,137 @@ 2.60 res = []Session(sorted) 2.61 return res, nil 2.62 } 2.63 + 2.64 +// RegisterSessionHandlers adds handlers to the passed router to handle the session endpoints, like login and logout. 2.65 +func RegisterSessionHandlers(r *mux.Router, context Context) { 2.66 + r.Handle("/login", wrap(context, CreateSessionHandler)) 2.67 +} 2.68 + 2.69 +func checkCookie(r *http.Request, context Context) (Session, error) { 2.70 + cookie, err := r.Cookie(authCookieName) 2.71 + if err == http.ErrNoCookie { 2.72 + return Session{}, ErrNoSession 2.73 + } else if err != nil { 2.74 + log.Println(err) 2.75 + return Session{}, err 2.76 + } 2.77 + sess, err := context.GetSession(cookie.Value) 2.78 + if err == ErrSessionNotFound { 2.79 + return Session{}, ErrInvalidSession 2.80 + } else if err != nil { 2.81 + return Session{}, err 2.82 + } 2.83 + if !sess.Active { 2.84 + return Session{}, ErrInvalidSession 2.85 + } 2.86 + return sess, nil 2.87 +} 2.88 + 2.89 +func buildLoginRedirect(r *http.Request, context Context) string { 2.90 + if context.loginURI == nil { 2.91 + return "" 2.92 + } 2.93 + uri := *context.loginURI 2.94 + q := uri.Query() 2.95 + q.Set("from", r.URL.String()) 2.96 + uri.RawQuery = q.Encode() 2.97 + return uri.String() 2.98 +} 2.99 + 2.100 +func pbkdf2sha256check(profile Profile, passphrase string) (bool, error) { 2.101 + realPass, err := hex.DecodeString(profile.Passphrase) 2.102 + if err != nil { 2.103 + return false, err 2.104 + } 2.105 + candidate := pass.Check(sha256.New, profile.Iterations, []byte(passphrase), []byte(profile.Salt)) 2.106 + if !pass.Compare(candidate, realPass) { 2.107 + return false, ErrIncorrectAuth 2.108 + } 2.109 + return true, nil 2.110 +} 2.111 + 2.112 +func pbkdf2sha256create(passphrase string, iters int) (result, salt []byte, err error) { 2.113 + return pass.Create(sha256.New, iters, []byte(passphrase)) 2.114 +} 2.115 + 2.116 +func pbkdf2sha256calc() (int, error) { 2.117 + return pass.CalculateIterations(sha256.New) 2.118 +} 2.119 + 2.120 +func authenticate(user, passphrase string, context Context) (Profile, error) { 2.121 + profile, err := context.GetProfileByLogin(user) 2.122 + if err != nil { 2.123 + if err == ErrProfileNotFound || err == ErrLoginNotFound { 2.124 + return Profile{}, ErrIncorrectAuth 2.125 + } 2.126 + return Profile{}, err 2.127 + } 2.128 + if profile.Compromised { 2.129 + return Profile{}, ErrProfileCompromised 2.130 + } 2.131 + if !profile.LockedUntil.IsZero() && profile.LockedUntil.After(time.Now()) { 2.132 + return profile, ErrProfileLocked 2.133 + } 2.134 + scheme, ok := passphraseSchemes[profile.PassphraseScheme] 2.135 + if !ok { 2.136 + return Profile{}, ErrInvalidPassphraseScheme 2.137 + } 2.138 + result, err := scheme.check(profile, passphrase) 2.139 + if !result { 2.140 + return Profile{}, err 2.141 + } 2.142 + return profile, nil 2.143 +} 2.144 + 2.145 +// CreateSessionHandler allows the user to log into their account and create their session. 2.146 +func CreateSessionHandler(w http.ResponseWriter, r *http.Request, context Context) { 2.147 + // BUG(paddy): Creating a session needs CSRF protection, right? This whole thing should get a security audit 2.148 + errors := []error{} 2.149 + if r.Method == "POST" { 2.150 + profile, err := authenticate(r.PostFormValue("login"), r.PostFormValue("passphrase"), context) 2.151 + if err == nil { 2.152 + ip := r.Header.Get("X-Forwarded-For") 2.153 + if ip == "" { 2.154 + ip = r.RemoteAddr 2.155 + } 2.156 + session := Session{ 2.157 + ID: uuid.NewID().String(), 2.158 + IP: ip, 2.159 + UserAgent: r.UserAgent(), 2.160 + ProfileID: profile.ID, 2.161 + Login: r.PostFormValue("login"), 2.162 + Created: time.Now(), 2.163 + Active: true, 2.164 + } 2.165 + err = context.CreateSession(session) 2.166 + if err != nil { 2.167 + w.WriteHeader(http.StatusInternalServerError) 2.168 + w.Write([]byte(err.Error())) 2.169 + return 2.170 + } 2.171 + // BUG(paddy): really need to do a security audit on our cookie 2.172 + cookie := http.Cookie{ 2.173 + Name: authCookieName, 2.174 + Value: session.ID, 2.175 + Expires: time.Now().Add(24 * 7 * time.Hour), 2.176 + HttpOnly: true, 2.177 + } 2.178 + http.SetCookie(w, &cookie) 2.179 + redirectTo := r.URL.Query().Get("from") 2.180 + if redirectTo == "" { 2.181 + redirectTo = "/" 2.182 + } 2.183 + http.Redirect(w, r, redirectTo, http.StatusFound) 2.184 + return 2.185 + } else if err != ErrIncorrectAuth && err != ErrProfileCompromised && err != ErrProfileLocked { 2.186 + w.WriteHeader(http.StatusInternalServerError) 2.187 + w.Write([]byte(err.Error())) 2.188 + return 2.189 + } else { 2.190 + errors = append(errors, err) 2.191 + } 2.192 + } 2.193 + context.Render(w, loginTemplateName, map[string]interface{}{ 2.194 + "errors": errors, 2.195 + }) 2.196 +}