auth

Paddy 2014-11-11 Parent:42bc3e44f4fe Child:ade4f4afd898

71:c8b0208c9e5d Go to Latest

auth/http.go

Remove extraneous TODOs. One TODO shouldn't actually be done. Another is already done. Remove both.

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