auth

Paddy 2015-02-20 Parent:163ce22fa4c9 Child:d30a3a12d387

134:d103a598548c Go to Latest

auth/oauth2.go

Introduced scopes. Created a Scope type and a scopeStore interface, along with the memstore methods for the scopeStore. This will allow applications to define access with granularity, so users can grant access to some data, not _all_ data. We're operating on the assumption that there won't be an unreasonable number of scopes defined, so there is no paging operation included for the ListScopes method. This is a decision that may have to be revisited in the future, depending on usecases.

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