auth

Paddy 2014-12-07 Parent:1dc4e152e3b0 Child:1fb166575e69

86:7f64033806bb Go to Latest

auth/oauth2.go

Document RenderJSONToken. Document the RenderJSONToken to satisfy golint.

History
paddy@51 1 package auth
paddy@51 2
paddy@51 3 import (
paddy@82 4 "crypto/sha256"
paddy@79 5 "encoding/hex"
paddy@69 6 "encoding/json"
paddy@69 7 "errors"
paddy@61 8 "html/template"
paddy@77 9 "log"
paddy@51 10 "net/http"
paddy@60 11 "net/url"
paddy@84 12 "sync"
paddy@60 13 "time"
paddy@56 14
paddy@69 15 "code.secondbit.org/pass"
paddy@56 16 "code.secondbit.org/uuid"
paddy@82 17
paddy@82 18 "github.com/gorilla/mux"
paddy@51 19 )
paddy@51 20
paddy@60 21 const (
paddy@69 22 authCookieName = "auth"
paddy@69 23 defaultGrantExpiration = 600 // default to ten minute grant expirations
paddy@60 24 getGrantTemplateName = "get_grant"
paddy@60 25 )
paddy@51 26
paddy@69 27 var (
paddy@69 28 // ErrNoAuth is returned when an Authorization header is not present or is empty.
paddy@69 29 ErrNoAuth = errors.New("no authorization header supplied")
paddy@69 30 // ErrInvalidAuthFormat is returned when an Authorization header is present but not the correct format.
paddy@69 31 ErrInvalidAuthFormat = errors.New("authorization header is not in a valid format")
paddy@69 32 // ErrIncorrectAuth is returned when a user authentication attempt does not match the stored values.
paddy@69 33 ErrIncorrectAuth = errors.New("invalid authentication")
paddy@69 34 // ErrInvalidPassphraseScheme is returned when an undefined passphrase scheme is used.
paddy@69 35 ErrInvalidPassphraseScheme = errors.New("invalid passphrase scheme")
paddy@69 36 // ErrNoSession is returned when no session ID is passed with a request.
paddy@69 37 ErrNoSession = errors.New("no session ID found")
paddy@84 38
paddy@84 39 grantTypesMap = grantTypes{types: map[string]GrantType{}}
paddy@69 40 )
paddy@69 41
paddy@84 42 type grantTypes struct {
paddy@84 43 types map[string]GrantType
paddy@84 44 sync.RWMutex
paddy@84 45 }
paddy@84 46
paddy@84 47 // GrantType defines a set of functions and metadata around a specific authorization grant strategy.
paddy@84 48 //
paddy@84 49 // The Validate function will be called when requests are made that match the GrantType, and should write any
paddy@84 50 // errors to the ResponseWriter. It is responsible for determining if the grant is valid and a token should be issued.
paddy@84 51 // It must return the scope the grant was for and the ID of the Profile that issued the grant, as well as if the grant
paddy@84 52 // is valid or not. It must not be nil.
paddy@84 53 //
paddy@84 54 // The Invalidate function will be called when the grant has successfully generated a token and the token has successfully
paddy@84 55 // been conveyed to the user. The Invalidate function is always called asynchronously, outside the request. It should take
paddy@84 56 // care of marking the grant as used, if the GrantType requires grants to be one-time only grants. The Invalidate function
paddy@84 57 // can be nil.
paddy@84 58 //
paddy@84 59 // IssuesRefresh determines whether the GrantType should yield a refresh token as well as an access token. If true, the client
paddy@84 60 // will be issued a refresh token.
paddy@85 61 //
paddy@85 62 // The ReturnToken will be called when a token is created and needs to be returned to the client. If it returns true, the token
paddy@85 63 // was successfully returned and the Invalidate function will be called asynchronously.
paddy@84 64 type GrantType struct {
paddy@84 65 Validate func(w http.ResponseWriter, r *http.Request, context Context) (scope string, profileID uuid.ID, valid bool)
paddy@84 66 Invalidate func(r *http.Request, context Context) bool
paddy@85 67 ReturnToken func(w http.ResponseWriter, r *http.Request, token Token, context Context) bool
paddy@84 68 IssuesRefresh bool
paddy@84 69 }
paddy@84 70
paddy@69 71 type tokenResponse struct {
paddy@69 72 AccessToken string `json:"access_token"`
paddy@69 73 TokenType string `json:"token_type,omitempty"`
paddy@69 74 ExpiresIn int32 `json:"expires_in,omitempty"`
paddy@69 75 RefreshToken string `json:"refresh_token,omitempty"`
paddy@69 76 }
paddy@69 77
paddy@82 78 type errorResponse struct {
paddy@82 79 Error string `json:"error"`
paddy@82 80 Description string `json:"error_description,omitempty"`
paddy@82 81 URI string `json:"error_uri,omitempty"`
paddy@82 82 }
paddy@82 83
paddy@84 84 // RegisterGrantType associates a string with a GrantType. When the string is used as the value for "grant_type" when obtaining
paddy@84 85 // an access token, the associated GrantType's properties will be used.
paddy@84 86 //
paddy@84 87 // RegisterGrantType should be called in the `init()` function of packages, much like database/sql registers drivers. It will panic
paddy@84 88 // if a GrantType tries to register under a string that already has a GrantType registered for it.
paddy@84 89 func RegisterGrantType(name string, g GrantType) {
paddy@84 90 grantTypesMap.Lock()
paddy@84 91 defer grantTypesMap.Unlock()
paddy@84 92 if _, ok := grantTypesMap.types[name]; ok {
paddy@84 93 panic("Duplicate registration of grant_type " + name)
paddy@84 94 }
paddy@84 95 grantTypesMap.types[name] = g
paddy@84 96 }
paddy@84 97
paddy@84 98 func findGrantType(name string) (GrantType, bool) {
paddy@84 99 grantTypesMap.RLock()
paddy@84 100 defer grantTypesMap.RUnlock()
paddy@84 101 t, ok := grantTypesMap.types[name]
paddy@84 102 return t, ok
paddy@84 103 }
paddy@84 104
paddy@82 105 func renderJSONError(enc *json.Encoder, errorType string) {
paddy@82 106 err := enc.Encode(errorResponse{
paddy@82 107 Error: errorType,
paddy@82 108 })
paddy@82 109 if err != nil {
paddy@82 110 // TODO(paddy): log this or something
paddy@69 111 }
paddy@69 112 }
paddy@69 113
paddy@86 114 // RenderJSONToken is an implementation of the ReturnToken function for GrantTypes. It returns the token using JSON
paddy@86 115 // according to the spec. See RFC 6479, Section 4.1.4.
paddy@85 116 func RenderJSONToken(w http.ResponseWriter, r *http.Request, token Token, context Context) bool {
paddy@85 117 enc := json.NewEncoder(w)
paddy@85 118 resp := tokenResponse{
paddy@85 119 AccessToken: token.AccessToken,
paddy@85 120 RefreshToken: token.RefreshToken,
paddy@85 121 ExpiresIn: token.ExpiresIn,
paddy@85 122 TokenType: token.TokenType,
paddy@85 123 }
paddy@85 124 err := enc.Encode(resp)
paddy@85 125 if err != nil {
paddy@85 126 // TODO(paddy): log this or something
paddy@85 127 return false
paddy@85 128 }
paddy@85 129 return true
paddy@85 130 }
paddy@85 131
paddy@69 132 func checkCookie(r *http.Request, context Context) (Session, error) {
paddy@69 133 cookie, err := r.Cookie(authCookieName)
paddy@77 134 if err == http.ErrNoCookie {
paddy@77 135 return Session{}, ErrNoSession
paddy@77 136 } else if err != nil {
paddy@77 137 log.Println(err)
paddy@69 138 return Session{}, err
paddy@69 139 }
paddy@69 140 sess, err := context.GetSession(cookie.Value)
paddy@69 141 if err == ErrSessionNotFound {
paddy@69 142 return Session{}, ErrInvalidSession
paddy@69 143 } else if err != nil {
paddy@69 144 return Session{}, err
paddy@69 145 }
paddy@69 146 if !sess.Active {
paddy@69 147 return Session{}, ErrInvalidSession
paddy@69 148 }
paddy@69 149 return sess, nil
paddy@69 150 }
paddy@69 151
paddy@77 152 func buildLoginRedirect(r *http.Request, context Context) string {
paddy@77 153 if context.loginURI == nil {
paddy@77 154 return ""
paddy@77 155 }
paddy@77 156 uri := *context.loginURI
paddy@77 157 q := uri.Query()
paddy@78 158 q.Set("from", r.URL.String())
paddy@77 159 uri.RawQuery = q.Encode()
paddy@77 160 return uri.String()
paddy@77 161 }
paddy@77 162
paddy@69 163 func authenticate(user, passphrase string, context Context) (Profile, error) {
paddy@69 164 profile, err := context.GetProfileByLogin(user)
paddy@69 165 if err != nil {
paddy@79 166 if err == ErrProfileNotFound || err == ErrLoginNotFound {
paddy@69 167 return Profile{}, ErrIncorrectAuth
paddy@69 168 }
paddy@69 169 return Profile{}, err
paddy@69 170 }
paddy@69 171 switch profile.PassphraseScheme {
paddy@69 172 case 1:
paddy@79 173 realPass, err := hex.DecodeString(profile.Passphrase)
paddy@79 174 if err != nil {
paddy@79 175 return Profile{}, err
paddy@79 176 }
paddy@69 177 candidate := pass.Check(sha256.New, profile.Iterations, []byte(passphrase), []byte(profile.Salt))
paddy@79 178 if !pass.Compare(candidate, realPass) {
paddy@69 179 return Profile{}, ErrIncorrectAuth
paddy@69 180 }
paddy@69 181 default:
paddy@69 182 return Profile{}, ErrInvalidPassphraseScheme
paddy@69 183 }
paddy@69 184 return profile, nil
paddy@69 185 }
paddy@69 186
paddy@77 187 func wrap(context Context, f func(w http.ResponseWriter, r *http.Request, context Context)) http.Handler {
paddy@77 188 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
paddy@77 189 f(w, r, context)
paddy@77 190 })
paddy@77 191 }
paddy@77 192
paddy@77 193 // RegisterOAuth2 adds handlers to the passed router to handle the OAuth2 endpoints.
paddy@77 194 func RegisterOAuth2(r *mux.Router, context Context) {
paddy@77 195 r.Handle("/authorize", wrap(context, GetGrantHandler))
paddy@77 196 r.Handle("/token", wrap(context, GetTokenHandler))
paddy@77 197 }
paddy@77 198
paddy@57 199 // GetGrantHandler presents and processes the page for asking a user to grant access
paddy@57 200 // to their data. See RFC 6749, Section 4.1.
paddy@51 201 func GetGrantHandler(w http.ResponseWriter, r *http.Request, context Context) {
paddy@69 202 session, err := checkCookie(r, context)
paddy@69 203 if err != nil {
paddy@76 204 if err == ErrNoSession || err == ErrInvalidSession {
paddy@77 205 redir := buildLoginRedirect(r, context)
paddy@77 206 if redir == "" {
paddy@77 207 log.Println("No login URL configured.")
paddy@77 208 w.WriteHeader(http.StatusInternalServerError)
paddy@77 209 context.Render(w, getGrantTemplateName, map[string]interface{}{
paddy@77 210 "internal_error": template.HTML("Missing login URL."),
paddy@77 211 })
paddy@77 212 return
paddy@77 213 }
paddy@77 214 http.Redirect(w, r, redir, http.StatusFound)
paddy@77 215 return
paddy@69 216 }
paddy@77 217 log.Println(err.Error())
paddy@77 218 w.WriteHeader(http.StatusInternalServerError)
paddy@77 219 context.Render(w, getGrantTemplateName, map[string]interface{}{
paddy@77 220 "internal_error": template.HTML(err.Error()),
paddy@77 221 })
paddy@77 222 return
paddy@69 223 }
paddy@56 224 if r.URL.Query().Get("client_id") == "" {
paddy@56 225 w.WriteHeader(http.StatusBadRequest)
paddy@56 226 context.Render(w, getGrantTemplateName, map[string]interface{}{
paddy@61 227 "error": template.HTML("Client ID must be specified in the request."),
paddy@56 228 })
paddy@56 229 return
paddy@56 230 }
paddy@56 231 clientID, err := uuid.Parse(r.URL.Query().Get("client_id"))
paddy@56 232 if err != nil {
paddy@56 233 w.WriteHeader(http.StatusBadRequest)
paddy@56 234 context.Render(w, getGrantTemplateName, map[string]interface{}{
paddy@61 235 "error": template.HTML("client_id is not a valid Client ID."),
paddy@56 236 })
paddy@56 237 return
paddy@56 238 }
paddy@64 239 redirectURI := r.URL.Query().Get("redirect_uri")
paddy@64 240 redirectURL, err := url.Parse(redirectURI)
paddy@64 241 if err != nil {
paddy@64 242 w.WriteHeader(http.StatusBadRequest)
paddy@64 243 context.Render(w, getGrantTemplateName, map[string]interface{}{
paddy@64 244 "error": template.HTML("The redirect_uri specified is not valid."),
paddy@64 245 })
paddy@64 246 return
paddy@64 247 }
paddy@56 248 client, err := context.GetClient(clientID)
paddy@56 249 if err != nil {
paddy@59 250 if err == ErrClientNotFound {
paddy@59 251 w.WriteHeader(http.StatusBadRequest)
paddy@59 252 context.Render(w, getGrantTemplateName, map[string]interface{}{
paddy@61 253 "error": template.HTML("The specified Client couldn’t be found."),
paddy@59 254 })
paddy@59 255 } else {
paddy@77 256 log.Println(err.Error())
paddy@59 257 w.WriteHeader(http.StatusInternalServerError)
paddy@59 258 context.Render(w, getGrantTemplateName, map[string]interface{}{
paddy@61 259 "internal_error": template.HTML(err.Error()),
paddy@59 260 })
paddy@59 261 }
paddy@56 262 return
paddy@56 263 }
paddy@56 264 // whether a redirect URI is valid or not depends on the number of endpoints
paddy@56 265 // the client has registered
paddy@56 266 numEndpoints, err := context.CountEndpoints(clientID)
paddy@56 267 if err != nil {
paddy@77 268 log.Println(err.Error())
paddy@56 269 w.WriteHeader(http.StatusInternalServerError)
paddy@56 270 context.Render(w, getGrantTemplateName, map[string]interface{}{
paddy@61 271 "internal_error": template.HTML(err.Error()),
paddy@56 272 })
paddy@56 273 return
paddy@56 274 }
paddy@56 275 var validURI bool
paddy@58 276 if redirectURI != "" {
paddy@58 277 // BUG(paddy): We really should normalize URIs before trying to compare them.
paddy@58 278 validURI, err = context.CheckEndpoint(clientID, redirectURI)
paddy@56 279 if err != nil {
paddy@77 280 log.Println(err.Error())
paddy@56 281 w.WriteHeader(http.StatusInternalServerError)
paddy@56 282 context.Render(w, getGrantTemplateName, map[string]interface{}{
paddy@61 283 "internal_error": template.HTML(err.Error()),
paddy@56 284 })
paddy@56 285 return
paddy@56 286 }
paddy@56 287 } else if redirectURI == "" && numEndpoints == 1 {
paddy@56 288 // if we don't specify the endpoint and there's only one endpoint, the
paddy@56 289 // request is valid, and we're redirecting to that one endpoint
paddy@56 290 validURI = true
paddy@56 291 endpoints, err := context.ListEndpoints(clientID, 1, 0)
paddy@56 292 if err != nil {
paddy@77 293 log.Println(err.Error())
paddy@56 294 w.WriteHeader(http.StatusInternalServerError)
paddy@56 295 context.Render(w, getGrantTemplateName, map[string]interface{}{
paddy@61 296 "internal_error": template.HTML(err.Error()),
paddy@56 297 })
paddy@56 298 return
paddy@56 299 }
paddy@56 300 if len(endpoints) != 1 {
paddy@56 301 validURI = false
paddy@56 302 } else {
paddy@66 303 u := endpoints[0].URI // Copy it here to avoid grabbing a pointer to the memstore
paddy@66 304 redirectURI = u.String()
paddy@66 305 redirectURL = &u
paddy@56 306 }
paddy@56 307 } else {
paddy@56 308 validURI = false
paddy@56 309 }
paddy@56 310 if !validURI {
paddy@56 311 w.WriteHeader(http.StatusBadRequest)
paddy@56 312 context.Render(w, getGrantTemplateName, map[string]interface{}{
paddy@61 313 "error": template.HTML("The redirect_uri specified is not valid."),
paddy@56 314 })
paddy@56 315 return
paddy@56 316 }
paddy@60 317 scope := r.URL.Query().Get("scope")
paddy@60 318 state := r.URL.Query().Get("state")
paddy@56 319 if r.URL.Query().Get("response_type") != "code" {
paddy@65 320 q := redirectURL.Query()
paddy@65 321 q.Add("error", "invalid_request")
paddy@65 322 q.Add("state", state)
paddy@65 323 redirectURL.RawQuery = q.Encode()
paddy@60 324 http.Redirect(w, r, redirectURL.String(), http.StatusFound)
paddy@60 325 return
paddy@56 326 }
paddy@56 327 if r.Method == "POST" {
paddy@63 328 // BUG(paddy): We need to implement CSRF protection when obtaining a grant code.
paddy@56 329 if r.PostFormValue("grant") == "approved" {
paddy@60 330 code := uuid.NewID().String()
paddy@60 331 grant := Grant{
paddy@60 332 Code: code,
paddy@60 333 Created: time.Now(),
paddy@60 334 ExpiresIn: defaultGrantExpiration,
paddy@60 335 ClientID: clientID,
paddy@60 336 Scope: scope,
paddy@69 337 RedirectURI: r.URL.Query().Get("redirect_uri"),
paddy@60 338 State: state,
paddy@69 339 ProfileID: session.ProfileID,
paddy@60 340 }
paddy@60 341 err := context.SaveGrant(grant)
paddy@60 342 if err != nil {
paddy@66 343 q := redirectURL.Query()
paddy@66 344 q.Add("error", "server_error")
paddy@66 345 q.Add("state", state)
paddy@66 346 redirectURL.RawQuery = q.Encode()
paddy@60 347 http.Redirect(w, r, redirectURL.String(), http.StatusFound)
paddy@60 348 return
paddy@60 349 }
paddy@66 350 q := redirectURL.Query()
paddy@66 351 q.Add("code", code)
paddy@66 352 q.Add("state", state)
paddy@66 353 redirectURL.RawQuery = q.Encode()
paddy@60 354 http.Redirect(w, r, redirectURL.String(), http.StatusFound)
paddy@60 355 return
paddy@56 356 }
paddy@66 357 q := redirectURL.Query()
paddy@66 358 q.Add("error", "access_denied")
paddy@66 359 q.Add("state", state)
paddy@66 360 redirectURL.RawQuery = q.Encode()
paddy@60 361 http.Redirect(w, r, redirectURL.String(), http.StatusFound)
paddy@60 362 return
paddy@56 363 }
paddy@85 364 profile, err := context.GetProfileByID(session.ProfileID)
paddy@85 365 if err != nil {
paddy@85 366 q := redirectURL.Query()
paddy@85 367 q.Add("error", "server_error")
paddy@85 368 q.Add("state", state)
paddy@85 369 redirectURL.RawQuery = q.Encode()
paddy@85 370 http.Redirect(w, r, redirectURL.String(), http.StatusFound)
paddy@85 371 return
paddy@85 372 }
paddy@51 373 w.WriteHeader(http.StatusOK)
paddy@56 374 context.Render(w, getGrantTemplateName, map[string]interface{}{
paddy@85 375 "client": client,
paddy@85 376 "redirectURL": redirectURL,
paddy@85 377 "scope": scope,
paddy@85 378 "profile": profile,
paddy@56 379 })
paddy@51 380 }
paddy@68 381
paddy@69 382 // GetTokenHandler allows a client to exchange an authorization grant for an
paddy@69 383 // access token. See RFC 6749 Section 4.1.3.
paddy@69 384 func GetTokenHandler(w http.ResponseWriter, r *http.Request, context Context) {
paddy@69 385 enc := json.NewEncoder(w)
paddy@69 386 grantType := r.PostFormValue("grant_type")
paddy@84 387 gt, ok := findGrantType(grantType)
paddy@84 388 if !ok {
paddy@82 389 w.WriteHeader(http.StatusBadRequest)
paddy@82 390 renderJSONError(enc, "invalid_request")
paddy@69 391 return
paddy@69 392 }
paddy@84 393 scope, profileID, valid := gt.Validate(w, r, context)
paddy@84 394 if !valid {
paddy@69 395 return
paddy@69 396 }
paddy@84 397 refresh := ""
paddy@84 398 if gt.IssuesRefresh {
paddy@84 399 refresh = uuid.NewID().String()
paddy@69 400 }
paddy@69 401 token := Token{
paddy@69 402 AccessToken: uuid.NewID().String(),
paddy@84 403 RefreshToken: refresh,
paddy@69 404 Created: time.Now(),
paddy@69 405 ExpiresIn: defaultTokenExpiration,
paddy@81 406 TokenType: "bearer",
paddy@84 407 Scope: scope,
paddy@84 408 ProfileID: profileID,
paddy@69 409 }
paddy@84 410 err := context.SaveToken(token)
paddy@69 411 if err != nil {
paddy@82 412 w.WriteHeader(http.StatusInternalServerError)
paddy@82 413 renderJSONError(enc, "server_error")
paddy@81 414 return
paddy@69 415 }
paddy@85 416 if gt.ReturnToken(w, r, token, context) && gt.Invalidate != nil {
paddy@85 417 go gt.Invalidate(r, context)
paddy@69 418 }
paddy@69 419 }
paddy@69 420
paddy@68 421 // TODO(paddy): exchange user credentials for access token
paddy@68 422 // TODO(paddy): exchange client credentials for access token
paddy@68 423 // TODO(paddy): implicit grant for access token
paddy@68 424 // TODO(paddy): exchange refresh token for access token