auth

Paddy 2015-03-18 Parent:d30a3a12d387 Child:7ae03163f578

146:b5432f50f057 Go to Latest

auth/oauth2.go

Fix tests. It made no sense to have the Profile's Passphrase length check in the Validate call of the ProfileChange type, because the ProfileChange had that property being applied as the _hashed_ passphrase. And We want to validate the _cleartext_ passphrase. So we removed that in e660a38fa936. But we forgot to update the tests. So I'm updating now.

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