auth

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

70:8398c3e4b3d9 Go to Latest

auth/http.go

Actually define Sessions. That last commit (42bc3e44f4fe) didn't actually include Sessions. Oops. Add the file and include our Session definition and sessionStore.

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