auth

Paddy 2015-01-18 Parent:eb9842ae3ff1 Child:d14f0a81498c

123:0a1e16b9c141 Go to Latest

auth/oauth2.go

Refactor verifyClient, implement refresh tokens. Refactor verifyClient into verifyClient and getClientAuth. We moved verifyClient out of each of the GrantType's validation functions and into the access token endpoint, where it will be called before the GrantType's validation function. Yay, less code repetition. And seeing as we always want to verify the client, that seems like a good way to prevent things like 118a69954621 from happening. This did, however, force us to add an AllowsPublic property to the GrantType, so the token endpoint knows whether or not a public Client is valid for any given GrantType. We also implemented the refresh token grant type, which required adding ClientID and RefreshRevoked as properties on the Token type. We need ClientID because we need to constrain refresh tokens to the client that issued them. We also should probably keep track of which tokens belong to which clients, just as a general rule of thumb. RefreshRevoked had to be created, next to Revoked, because the AccessToken could be revoked and the RefreshToken still valid, or vice versa. Notably, when you issue a new refresh token, the old one is revoked, but the access token is still valid. It remains to be seen whether this is a good way to track things or not. The number of duplicated properties lead me to believe our type is not a great representation of the underlying concepts.

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