auth

Paddy 2014-11-11 Parent:752c2fb9731c Child:c8b0208c9e5d

69:42bc3e44f4fe Go to Latest

auth/http.go

Stub out sessions. Stop using the Login type when getting profile by Login, removing Logins, or recording Login use. The Login value has to be unique, anyways, and we don't actually know the Login type when getting a profile by Login. That's sort of the point. Create the concept of Sessions and a sessionStore type to manage our authentication sessions with the server. As per OWASP, we're basically just going to use a transparent, SHA256-generated random string as an ID, and store it client-side and server-side and just pass it back and forth. Add the ProfileID to the Grant type, because we need to remember who granted access. That's sort of important. Set a defaultGrantExpiration constant to an hour, so we have that one constant when creating new Grants. Create a helper that pulls the session ID out of an auth cookie, checks it against the sessionStore, and returns the Session if it's valid. Create a helper that pulls the username and password out of a basic auth header. Create a helper that authenticates a user's login and passphrase, checking them against the profileStore securely. Stub out how the cookie checking is going to work for getting grant approval. Fix the stored Grant RedirectURI to be the passed in redirect URI, not the RedirectURI that we ultimately redirect to. This is in accordance with the spec. Store the profile ID from our session in the created Grant. Stub out a GetTokenHandler that will allow users to exchange a Grant for a Token. Set a constant for the current passphrase scheme, which we will increment for each revision to the passphrase scheme, for backwards compatibility. Change the Profile iterations property to an int, not an int64, to match the code.secondbit.org/pass library (which is matching the PBKDF2 library).

History
paddy@51 1 package auth
paddy@51 2
paddy@51 3 import (
paddy@69 4 "encoding/base64"
paddy@69 5 "encoding/json"
paddy@69 6 "errors"
paddy@61 7 "html/template"
paddy@51 8 "net/http"
paddy@60 9 "net/url"
paddy@69 10 "strings"
paddy@60 11 "time"
paddy@56 12
paddy@69 13 "crypto/sha256"
paddy@69 14 "code.secondbit.org/pass"
paddy@56 15 "code.secondbit.org/uuid"
paddy@51 16 )
paddy@51 17
paddy@60 18 const (
paddy@69 19 authCookieName = "auth"
paddy@69 20 defaultGrantExpiration = 600 // default to ten minute grant expirations
paddy@60 21 getGrantTemplateName = "get_grant"
paddy@60 22 )
paddy@51 23
paddy@69 24 var (
paddy@69 25 // ErrNoAuth is returned when an Authorization header is not present or is empty.
paddy@69 26 ErrNoAuth = errors.New("no authorization header supplied")
paddy@69 27 // ErrInvalidAuthFormat is returned when an Authorization header is present but not the correct format.
paddy@69 28 ErrInvalidAuthFormat = errors.New("authorization header is not in a valid format")
paddy@69 29 // ErrIncorrectAuth is returned when a user authentication attempt does not match the stored values.
paddy@69 30 ErrIncorrectAuth = errors.New("invalid authentication")
paddy@69 31 // ErrInvalidPassphraseScheme is returned when an undefined passphrase scheme is used.
paddy@69 32 ErrInvalidPassphraseScheme = errors.New("invalid passphrase scheme")
paddy@69 33 // ErrNoSession is returned when no session ID is passed with a request.
paddy@69 34 ErrNoSession = errors.New("no session ID found")
paddy@69 35 )
paddy@69 36
paddy@69 37 type tokenResponse struct {
paddy@69 38 AccessToken string `json:"access_token"`
paddy@69 39 TokenType string `json:"token_type,omitempty"`
paddy@69 40 ExpiresIn int32 `json:"expires_in,omitempty"`
paddy@69 41 RefreshToken string `json:"refresh_token,omitempty"`
paddy@69 42 }
paddy@69 43
paddy@69 44 func getBasicAuth(r *http.Request) (un, pass string, err error) {
paddy@69 45 auth := r.Header.Get("Authorization")
paddy@69 46 if auth == "" {
paddy@69 47 return "", "", ErrNoAuth
paddy@69 48 }
paddy@69 49 pieces := strings.SplitN(auth, " ", 2)
paddy@69 50 if pieces[0] != "Basic" {
paddy@69 51 return "", "", ErrInvalidAuthFormat
paddy@69 52 }
paddy@69 53 decoded, err := base64.StdEncoding.DecodeString(pieces[1])
paddy@69 54 if err != nil {
paddy@69 55 // TODO(paddy): should probably log this...
paddy@69 56 return "", "", ErrInvalidAuthFormat
paddy@69 57 }
paddy@69 58 info := strings.SplitN(string(decoded), ":", 2)
paddy@69 59 return info[0], info[1], nil
paddy@69 60 }
paddy@69 61
paddy@69 62 func checkCookie(r *http.Request, context Context) (Session, error) {
paddy@69 63 cookie, err := r.Cookie(authCookieName)
paddy@69 64 if err != nil {
paddy@69 65 if err == http.ErrNoCookie {
paddy@69 66 return Session{}, ErrNoSession
paddy@69 67 }
paddy@69 68 return Session{}, err
paddy@69 69 }
paddy@69 70 if cookie.Name != authCookieName || !cookie.Expires.After(time.Now()) ||
paddy@69 71 !cookie.Secure || !cookie.HttpOnly {
paddy@69 72 return Session{}, ErrInvalidSession
paddy@69 73 }
paddy@69 74 sess, err := context.GetSession(cookie.Value)
paddy@69 75 if err == ErrSessionNotFound {
paddy@69 76 return Session{}, ErrInvalidSession
paddy@69 77 } else if err != nil {
paddy@69 78 return Session{}, err
paddy@69 79 }
paddy@69 80 if !sess.Active {
paddy@69 81 return Session{}, ErrInvalidSession
paddy@69 82 }
paddy@69 83 return sess, nil
paddy@69 84 }
paddy@69 85
paddy@69 86 func authenticate(user, passphrase string, context Context) (Profile, error) {
paddy@69 87 profile, err := context.GetProfileByLogin(user)
paddy@69 88 if err != nil {
paddy@69 89 if err == ErrProfileNotFound {
paddy@69 90 return Profile{}, ErrIncorrectAuth
paddy@69 91 }
paddy@69 92 return Profile{}, err
paddy@69 93 }
paddy@69 94 switch profile.PassphraseScheme {
paddy@69 95 case 1:
paddy@69 96 candidate := pass.Check(sha256.New, profile.Iterations, []byte(passphrase), []byte(profile.Salt))
paddy@69 97 if !pass.Compare(candidate, []byte(profile.Passphrase)) {
paddy@69 98 return Profile{}, ErrIncorrectAuth
paddy@69 99 }
paddy@69 100 default:
paddy@69 101 // TODO(paddy): return some error
paddy@69 102 return Profile{}, ErrInvalidPassphraseScheme
paddy@69 103 }
paddy@69 104 return profile, nil
paddy@69 105 }
paddy@69 106
paddy@57 107 // GetGrantHandler presents and processes the page for asking a user to grant access
paddy@57 108 // to their data. See RFC 6749, Section 4.1.
paddy@51 109 func GetGrantHandler(w http.ResponseWriter, r *http.Request, context Context) {
paddy@69 110 session, err := checkCookie(r, context)
paddy@69 111 if err != nil {
paddy@69 112 if err == ErrNoSession {
paddy@69 113 // TODO(paddy): redirect to login screen
paddy@69 114 //return
paddy@69 115 }
paddy@69 116 if err == ErrInvalidSession {
paddy@69 117 // TODO(paddy): return an access denied error
paddy@69 118 //return
paddy@69 119 }
paddy@69 120 // TODO(paddy): return a server error
paddy@69 121 //return
paddy@69 122 }
paddy@56 123 if r.URL.Query().Get("client_id") == "" {
paddy@56 124 w.WriteHeader(http.StatusBadRequest)
paddy@56 125 context.Render(w, getGrantTemplateName, map[string]interface{}{
paddy@61 126 "error": template.HTML("Client ID must be specified in the request."),
paddy@56 127 })
paddy@56 128 return
paddy@56 129 }
paddy@56 130 clientID, err := uuid.Parse(r.URL.Query().Get("client_id"))
paddy@56 131 if err != nil {
paddy@56 132 w.WriteHeader(http.StatusBadRequest)
paddy@56 133 context.Render(w, getGrantTemplateName, map[string]interface{}{
paddy@61 134 "error": template.HTML("client_id is not a valid Client ID."),
paddy@56 135 })
paddy@56 136 return
paddy@56 137 }
paddy@64 138 redirectURI := r.URL.Query().Get("redirect_uri")
paddy@64 139 redirectURL, err := url.Parse(redirectURI)
paddy@64 140 if err != nil {
paddy@64 141 w.WriteHeader(http.StatusBadRequest)
paddy@64 142 context.Render(w, getGrantTemplateName, map[string]interface{}{
paddy@64 143 "error": template.HTML("The redirect_uri specified is not valid."),
paddy@64 144 })
paddy@64 145 return
paddy@64 146 }
paddy@56 147 client, err := context.GetClient(clientID)
paddy@56 148 if err != nil {
paddy@59 149 if err == ErrClientNotFound {
paddy@59 150 w.WriteHeader(http.StatusBadRequest)
paddy@59 151 context.Render(w, getGrantTemplateName, map[string]interface{}{
paddy@61 152 "error": template.HTML("The specified Client couldn’t be found."),
paddy@59 153 })
paddy@59 154 } else {
paddy@59 155 w.WriteHeader(http.StatusInternalServerError)
paddy@59 156 context.Render(w, getGrantTemplateName, map[string]interface{}{
paddy@61 157 "internal_error": template.HTML(err.Error()),
paddy@59 158 })
paddy@59 159 }
paddy@56 160 return
paddy@56 161 }
paddy@56 162 // whether a redirect URI is valid or not depends on the number of endpoints
paddy@56 163 // the client has registered
paddy@56 164 numEndpoints, err := context.CountEndpoints(clientID)
paddy@56 165 if err != nil {
paddy@56 166 w.WriteHeader(http.StatusInternalServerError)
paddy@56 167 context.Render(w, getGrantTemplateName, map[string]interface{}{
paddy@61 168 "internal_error": template.HTML(err.Error()),
paddy@56 169 })
paddy@56 170 return
paddy@56 171 }
paddy@56 172 var validURI bool
paddy@58 173 if redirectURI != "" {
paddy@58 174 // BUG(paddy): We really should normalize URIs before trying to compare them.
paddy@58 175 validURI, err = context.CheckEndpoint(clientID, redirectURI)
paddy@56 176 if err != nil {
paddy@56 177 w.WriteHeader(http.StatusInternalServerError)
paddy@56 178 context.Render(w, getGrantTemplateName, map[string]interface{}{
paddy@61 179 "internal_error": template.HTML(err.Error()),
paddy@56 180 })
paddy@56 181 return
paddy@56 182 }
paddy@56 183 } else if redirectURI == "" && numEndpoints == 1 {
paddy@56 184 // if we don't specify the endpoint and there's only one endpoint, the
paddy@56 185 // request is valid, and we're redirecting to that one endpoint
paddy@56 186 validURI = true
paddy@56 187 endpoints, err := context.ListEndpoints(clientID, 1, 0)
paddy@56 188 if err != nil {
paddy@56 189 w.WriteHeader(http.StatusInternalServerError)
paddy@56 190 context.Render(w, getGrantTemplateName, map[string]interface{}{
paddy@61 191 "internal_error": template.HTML(err.Error()),
paddy@56 192 })
paddy@56 193 return
paddy@56 194 }
paddy@56 195 if len(endpoints) != 1 {
paddy@56 196 validURI = false
paddy@56 197 } else {
paddy@66 198 u := endpoints[0].URI // Copy it here to avoid grabbing a pointer to the memstore
paddy@66 199 redirectURI = u.String()
paddy@66 200 redirectURL = &u
paddy@56 201 }
paddy@56 202 } else {
paddy@56 203 validURI = false
paddy@56 204 }
paddy@56 205 if !validURI {
paddy@56 206 w.WriteHeader(http.StatusBadRequest)
paddy@56 207 context.Render(w, getGrantTemplateName, map[string]interface{}{
paddy@61 208 "error": template.HTML("The redirect_uri specified is not valid."),
paddy@56 209 })
paddy@56 210 return
paddy@56 211 }
paddy@60 212 scope := r.URL.Query().Get("scope")
paddy@60 213 state := r.URL.Query().Get("state")
paddy@56 214 if r.URL.Query().Get("response_type") != "code" {
paddy@65 215 q := redirectURL.Query()
paddy@65 216 q.Add("error", "invalid_request")
paddy@65 217 q.Add("state", state)
paddy@65 218 redirectURL.RawQuery = q.Encode()
paddy@60 219 http.Redirect(w, r, redirectURL.String(), http.StatusFound)
paddy@60 220 return
paddy@56 221 }
paddy@56 222 if r.Method == "POST" {
paddy@63 223 // BUG(paddy): We need to implement CSRF protection when obtaining a grant code.
paddy@56 224 if r.PostFormValue("grant") == "approved" {
paddy@60 225 code := uuid.NewID().String()
paddy@60 226 grant := Grant{
paddy@60 227 Code: code,
paddy@60 228 Created: time.Now(),
paddy@60 229 ExpiresIn: defaultGrantExpiration,
paddy@60 230 ClientID: clientID,
paddy@60 231 Scope: scope,
paddy@69 232 RedirectURI: r.URL.Query().Get("redirect_uri"),
paddy@60 233 State: state,
paddy@69 234 ProfileID: session.ProfileID,
paddy@60 235 }
paddy@60 236 err := context.SaveGrant(grant)
paddy@60 237 if err != nil {
paddy@66 238 q := redirectURL.Query()
paddy@66 239 q.Add("error", "server_error")
paddy@66 240 q.Add("state", state)
paddy@66 241 redirectURL.RawQuery = q.Encode()
paddy@60 242 http.Redirect(w, r, redirectURL.String(), http.StatusFound)
paddy@60 243 return
paddy@60 244 }
paddy@66 245 q := redirectURL.Query()
paddy@66 246 q.Add("code", code)
paddy@66 247 q.Add("state", state)
paddy@66 248 redirectURL.RawQuery = q.Encode()
paddy@60 249 http.Redirect(w, r, redirectURL.String(), http.StatusFound)
paddy@60 250 return
paddy@56 251 }
paddy@66 252 q := redirectURL.Query()
paddy@66 253 q.Add("error", "access_denied")
paddy@66 254 q.Add("state", state)
paddy@66 255 redirectURL.RawQuery = q.Encode()
paddy@60 256 http.Redirect(w, r, redirectURL.String(), http.StatusFound)
paddy@60 257 return
paddy@56 258 }
paddy@51 259 w.WriteHeader(http.StatusOK)
paddy@56 260 context.Render(w, getGrantTemplateName, map[string]interface{}{
paddy@56 261 "client": client,
paddy@56 262 })
paddy@51 263 }
paddy@68 264
paddy@69 265 // GetTokenHandler allows a client to exchange an authorization grant for an
paddy@69 266 // access token. See RFC 6749 Section 4.1.3.
paddy@69 267 func GetTokenHandler(w http.ResponseWriter, r *http.Request, context Context) {
paddy@69 268 enc := json.NewEncoder(w)
paddy@69 269 grantType := r.PostFormValue("grant_type")
paddy@69 270 if grantType != "authorization_code" {
paddy@69 271 // TODO(paddy): render invalid request JSON
paddy@69 272 return
paddy@69 273 }
paddy@69 274 code := r.PostFormValue("code")
paddy@69 275 if code == "" {
paddy@69 276 // TODO(paddy): render invalid request JSON
paddy@69 277 return
paddy@69 278 }
paddy@69 279 redirectURI := r.PostFormValue("redirect_uri")
paddy@69 280 clientIDStr, clientSecret, err := getBasicAuth(r)
paddy@69 281 if err != nil {
paddy@69 282 // TODO(paddy): render access denied
paddy@69 283 return
paddy@69 284 }
paddy@69 285 if clientIDStr == "" && err == nil {
paddy@69 286 clientIDStr = r.PostFormValue("client_id")
paddy@69 287 }
paddy@69 288 // TODO(paddy): client ID can also come from Basic auth
paddy@69 289 clientID, err := uuid.Parse(clientIDStr)
paddy@69 290 if err != nil {
paddy@69 291 // TODO(paddy): render invalid request JSON
paddy@69 292 return
paddy@69 293 }
paddy@69 294 client, err := context.GetClient(clientID)
paddy@69 295 if err != nil {
paddy@69 296 if err == ErrClientNotFound {
paddy@69 297 // TODO(paddy): render invalid request JSON
paddy@69 298 } else {
paddy@69 299 // TODO(paddy): render internal server error JSON
paddy@69 300 }
paddy@69 301 return
paddy@69 302 }
paddy@69 303 if client.Secret != clientSecret {
paddy@69 304 // TODO(paddy): render invalid request JSON
paddy@69 305 return
paddy@69 306 }
paddy@69 307 grant, err := context.GetGrant(code)
paddy@69 308 if err != nil {
paddy@69 309 if err == ErrGrantNotFound {
paddy@69 310 // TODO(paddy): return error
paddy@69 311 return
paddy@69 312 }
paddy@69 313 // TODO(paddy): return error
paddy@69 314 }
paddy@69 315 if grant.RedirectURI != redirectURI {
paddy@69 316 // TODO(paddy): return error
paddy@69 317 }
paddy@69 318 if !grant.ClientID.Equal(clientID) {
paddy@69 319 // TODO(paddy): return error
paddy@69 320 }
paddy@69 321 token := Token{
paddy@69 322 AccessToken: uuid.NewID().String(),
paddy@69 323 RefreshToken: uuid.NewID().String(),
paddy@69 324 Created: time.Now(),
paddy@69 325 ExpiresIn: defaultTokenExpiration,
paddy@69 326 TokenType: "", // TODO(paddy): fill in token type
paddy@69 327 Scope: grant.Scope,
paddy@69 328 ProfileID: grant.ProfileID,
paddy@69 329 }
paddy@69 330 err = context.SaveToken(token)
paddy@69 331 if err != nil {
paddy@69 332 // TODO(paddy): return error
paddy@69 333 }
paddy@69 334 resp := tokenResponse{
paddy@69 335 AccessToken: token.AccessToken,
paddy@69 336 RefreshToken: token.RefreshToken,
paddy@69 337 ExpiresIn: token.ExpiresIn,
paddy@69 338 TokenType: token.TokenType,
paddy@69 339 }
paddy@69 340 err = enc.Encode(resp)
paddy@69 341 if err != nil {
paddy@69 342 // TODO(paddy): log this or something
paddy@69 343 return
paddy@69 344 }
paddy@69 345 }
paddy@69 346
paddy@68 347 // TODO(paddy): exchange user credentials for access token
paddy@68 348 // TODO(paddy): exchange client credentials for access token
paddy@68 349 // TODO(paddy): implicit grant for access token
paddy@68 350 // TODO(paddy): exchange refresh token for access token