auth

Paddy 2014-12-14 Parent:09c47387e455 Child:c03b5eb3179e

104:bc77a315f823 Go to Latest

auth/oauth2.go

Add request helpers. Fix a typo in the requestErrConflict constant. Create a response type that is used to build responses before sending them down the wire. Create vars for a few common types of error responses that never change. Create a negotiate middleware function that will respond with a 406 error if the client requests an encoding that we can't support. Create an encode helper that determines the requested encoding and uses it to send the data down the wire. Move our wrap middleware from the oauth2.go file to the request.go file, where it's more likely to be looked for.

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@84 10 "sync"
paddy@60 11 "time"
paddy@56 12
paddy@56 13 "code.secondbit.org/uuid"
paddy@82 14
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@85 59 // 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 60 // was successfully returned and the Invalidate function will be called asynchronously.
paddy@84 61 type GrantType struct {
paddy@84 62 Validate func(w http.ResponseWriter, r *http.Request, context Context) (scope string, profileID uuid.ID, valid bool)
paddy@90 63 Invalidate func(r *http.Request, context Context) error
paddy@85 64 ReturnToken func(w http.ResponseWriter, r *http.Request, token Token, context Context) bool
paddy@84 65 IssuesRefresh bool
paddy@84 66 }
paddy@84 67
paddy@69 68 type tokenResponse struct {
paddy@69 69 AccessToken string `json:"access_token"`
paddy@69 70 TokenType string `json:"token_type,omitempty"`
paddy@69 71 ExpiresIn int32 `json:"expires_in,omitempty"`
paddy@69 72 RefreshToken string `json:"refresh_token,omitempty"`
paddy@69 73 }
paddy@69 74
paddy@82 75 type errorResponse struct {
paddy@82 76 Error string `json:"error"`
paddy@82 77 Description string `json:"error_description,omitempty"`
paddy@82 78 URI string `json:"error_uri,omitempty"`
paddy@82 79 }
paddy@82 80
paddy@84 81 // RegisterGrantType associates a string with a GrantType. When the string is used as the value for "grant_type" when obtaining
paddy@84 82 // an access token, the associated GrantType's properties will be used.
paddy@84 83 //
paddy@84 84 // RegisterGrantType should be called in the `init()` function of packages, much like database/sql registers drivers. It will panic
paddy@84 85 // if a GrantType tries to register under a string that already has a GrantType registered for it.
paddy@84 86 func RegisterGrantType(name string, g GrantType) {
paddy@84 87 grantTypesMap.Lock()
paddy@84 88 defer grantTypesMap.Unlock()
paddy@84 89 if _, ok := grantTypesMap.types[name]; ok {
paddy@84 90 panic("Duplicate registration of grant_type " + name)
paddy@84 91 }
paddy@84 92 grantTypesMap.types[name] = g
paddy@84 93 }
paddy@84 94
paddy@84 95 func findGrantType(name string) (GrantType, bool) {
paddy@84 96 grantTypesMap.RLock()
paddy@84 97 defer grantTypesMap.RUnlock()
paddy@84 98 t, ok := grantTypesMap.types[name]
paddy@84 99 return t, ok
paddy@84 100 }
paddy@84 101
paddy@82 102 func renderJSONError(enc *json.Encoder, errorType string) {
paddy@82 103 err := enc.Encode(errorResponse{
paddy@82 104 Error: errorType,
paddy@82 105 })
paddy@82 106 if err != nil {
paddy@90 107 log.Println(err)
paddy@69 108 }
paddy@69 109 }
paddy@69 110
paddy@86 111 // RenderJSONToken is an implementation of the ReturnToken function for GrantTypes. It returns the token using JSON
paddy@86 112 // according to the spec. See RFC 6479, Section 4.1.4.
paddy@85 113 func RenderJSONToken(w http.ResponseWriter, r *http.Request, token Token, context Context) bool {
paddy@85 114 enc := json.NewEncoder(w)
paddy@85 115 resp := tokenResponse{
paddy@85 116 AccessToken: token.AccessToken,
paddy@85 117 RefreshToken: token.RefreshToken,
paddy@85 118 ExpiresIn: token.ExpiresIn,
paddy@85 119 TokenType: token.TokenType,
paddy@85 120 }
paddy@85 121 err := enc.Encode(resp)
paddy@85 122 if err != nil {
paddy@90 123 log.Println(err)
paddy@85 124 return false
paddy@85 125 }
paddy@85 126 return true
paddy@85 127 }
paddy@85 128
paddy@77 129 // RegisterOAuth2 adds handlers to the passed router to handle the OAuth2 endpoints.
paddy@77 130 func RegisterOAuth2(r *mux.Router, context Context) {
paddy@87 131 r.Handle("/authorize", wrap(context, GetAuthorizationCodeHandler))
paddy@77 132 r.Handle("/token", wrap(context, GetTokenHandler))
paddy@77 133 }
paddy@77 134
paddy@87 135 // GetAuthorizationCodeHandler presents and processes the page for asking a user to grant access
paddy@57 136 // to their data. See RFC 6749, Section 4.1.
paddy@87 137 func GetAuthorizationCodeHandler(w http.ResponseWriter, r *http.Request, context Context) {
paddy@69 138 session, err := checkCookie(r, context)
paddy@69 139 if err != nil {
paddy@76 140 if err == ErrNoSession || err == ErrInvalidSession {
paddy@77 141 redir := buildLoginRedirect(r, context)
paddy@77 142 if redir == "" {
paddy@77 143 log.Println("No login URL configured.")
paddy@77 144 w.WriteHeader(http.StatusInternalServerError)
paddy@87 145 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
paddy@77 146 "internal_error": template.HTML("Missing login URL."),
paddy@77 147 })
paddy@77 148 return
paddy@77 149 }
paddy@77 150 http.Redirect(w, r, redir, http.StatusFound)
paddy@77 151 return
paddy@69 152 }
paddy@77 153 log.Println(err.Error())
paddy@77 154 w.WriteHeader(http.StatusInternalServerError)
paddy@87 155 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
paddy@77 156 "internal_error": template.HTML(err.Error()),
paddy@77 157 })
paddy@77 158 return
paddy@69 159 }
paddy@56 160 if r.URL.Query().Get("client_id") == "" {
paddy@56 161 w.WriteHeader(http.StatusBadRequest)
paddy@87 162 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
paddy@61 163 "error": template.HTML("Client ID must be specified in the request."),
paddy@56 164 })
paddy@56 165 return
paddy@56 166 }
paddy@56 167 clientID, err := uuid.Parse(r.URL.Query().Get("client_id"))
paddy@56 168 if err != nil {
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 is not a valid Client ID."),
paddy@56 172 })
paddy@56 173 return
paddy@56 174 }
paddy@64 175 redirectURI := r.URL.Query().Get("redirect_uri")
paddy@64 176 redirectURL, err := url.Parse(redirectURI)
paddy@64 177 if err != nil {
paddy@64 178 w.WriteHeader(http.StatusBadRequest)
paddy@87 179 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
paddy@64 180 "error": template.HTML("The redirect_uri specified is not valid."),
paddy@64 181 })
paddy@64 182 return
paddy@64 183 }
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@95 200 // TODO(paddy): checking if the redirect URI is valid should be a helper function
paddy@56 201 // whether a redirect URI is valid or not depends on the number of endpoints
paddy@56 202 // the client has registered
paddy@56 203 numEndpoints, err := context.CountEndpoints(clientID)
paddy@56 204 if err != nil {
paddy@77 205 log.Println(err.Error())
paddy@56 206 w.WriteHeader(http.StatusInternalServerError)
paddy@87 207 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
paddy@61 208 "internal_error": template.HTML(err.Error()),
paddy@56 209 })
paddy@56 210 return
paddy@56 211 }
paddy@56 212 var validURI bool
paddy@58 213 if redirectURI != "" {
paddy@58 214 // BUG(paddy): We really should normalize URIs before trying to compare them.
paddy@58 215 validURI, err = context.CheckEndpoint(clientID, redirectURI)
paddy@56 216 if err != nil {
paddy@77 217 log.Println(err.Error())
paddy@56 218 w.WriteHeader(http.StatusInternalServerError)
paddy@87 219 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
paddy@61 220 "internal_error": template.HTML(err.Error()),
paddy@56 221 })
paddy@56 222 return
paddy@56 223 }
paddy@56 224 } else if redirectURI == "" && numEndpoints == 1 {
paddy@56 225 // if we don't specify the endpoint and there's only one endpoint, the
paddy@56 226 // request is valid, and we're redirecting to that one endpoint
paddy@56 227 validURI = true
paddy@56 228 endpoints, err := context.ListEndpoints(clientID, 1, 0)
paddy@56 229 if err != nil {
paddy@77 230 log.Println(err.Error())
paddy@56 231 w.WriteHeader(http.StatusInternalServerError)
paddy@87 232 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
paddy@61 233 "internal_error": template.HTML(err.Error()),
paddy@56 234 })
paddy@56 235 return
paddy@56 236 }
paddy@56 237 if len(endpoints) != 1 {
paddy@56 238 validURI = false
paddy@56 239 } else {
paddy@66 240 u := endpoints[0].URI // Copy it here to avoid grabbing a pointer to the memstore
paddy@66 241 redirectURI = u.String()
paddy@66 242 redirectURL = &u
paddy@56 243 }
paddy@56 244 } else {
paddy@56 245 validURI = false
paddy@56 246 }
paddy@56 247 if !validURI {
paddy@56 248 w.WriteHeader(http.StatusBadRequest)
paddy@87 249 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
paddy@61 250 "error": template.HTML("The redirect_uri specified is not valid."),
paddy@56 251 })
paddy@56 252 return
paddy@56 253 }
paddy@60 254 scope := r.URL.Query().Get("scope")
paddy@60 255 state := r.URL.Query().Get("state")
paddy@56 256 if r.URL.Query().Get("response_type") != "code" {
paddy@65 257 q := redirectURL.Query()
paddy@65 258 q.Add("error", "invalid_request")
paddy@65 259 q.Add("state", state)
paddy@65 260 redirectURL.RawQuery = q.Encode()
paddy@60 261 http.Redirect(w, r, redirectURL.String(), http.StatusFound)
paddy@60 262 return
paddy@56 263 }
paddy@56 264 if r.Method == "POST" {
paddy@63 265 // BUG(paddy): We need to implement CSRF protection when obtaining a grant code.
paddy@56 266 if r.PostFormValue("grant") == "approved" {
paddy@60 267 code := uuid.NewID().String()
paddy@87 268 authCode := AuthorizationCode{
paddy@60 269 Code: code,
paddy@60 270 Created: time.Now(),
paddy@87 271 ExpiresIn: defaultAuthorizationCodeExpiration,
paddy@60 272 ClientID: clientID,
paddy@60 273 Scope: scope,
paddy@69 274 RedirectURI: r.URL.Query().Get("redirect_uri"),
paddy@60 275 State: state,
paddy@69 276 ProfileID: session.ProfileID,
paddy@60 277 }
paddy@87 278 err := context.SaveAuthorizationCode(authCode)
paddy@60 279 if err != nil {
paddy@66 280 q := redirectURL.Query()
paddy@66 281 q.Add("error", "server_error")
paddy@66 282 q.Add("state", state)
paddy@66 283 redirectURL.RawQuery = q.Encode()
paddy@60 284 http.Redirect(w, r, redirectURL.String(), http.StatusFound)
paddy@60 285 return
paddy@60 286 }
paddy@66 287 q := redirectURL.Query()
paddy@66 288 q.Add("code", code)
paddy@66 289 q.Add("state", state)
paddy@66 290 redirectURL.RawQuery = q.Encode()
paddy@60 291 http.Redirect(w, r, redirectURL.String(), http.StatusFound)
paddy@60 292 return
paddy@56 293 }
paddy@66 294 q := redirectURL.Query()
paddy@66 295 q.Add("error", "access_denied")
paddy@66 296 q.Add("state", state)
paddy@66 297 redirectURL.RawQuery = q.Encode()
paddy@60 298 http.Redirect(w, r, redirectURL.String(), http.StatusFound)
paddy@60 299 return
paddy@56 300 }
paddy@85 301 profile, err := context.GetProfileByID(session.ProfileID)
paddy@85 302 if err != nil {
paddy@85 303 q := redirectURL.Query()
paddy@85 304 q.Add("error", "server_error")
paddy@85 305 q.Add("state", state)
paddy@85 306 redirectURL.RawQuery = q.Encode()
paddy@85 307 http.Redirect(w, r, redirectURL.String(), http.StatusFound)
paddy@85 308 return
paddy@85 309 }
paddy@51 310 w.WriteHeader(http.StatusOK)
paddy@87 311 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
paddy@85 312 "client": client,
paddy@85 313 "redirectURL": redirectURL,
paddy@85 314 "scope": scope,
paddy@85 315 "profile": profile,
paddy@56 316 })
paddy@51 317 }
paddy@68 318
paddy@69 319 // GetTokenHandler allows a client to exchange an authorization grant for an
paddy@69 320 // access token. See RFC 6749 Section 4.1.3.
paddy@69 321 func GetTokenHandler(w http.ResponseWriter, r *http.Request, context Context) {
paddy@69 322 enc := json.NewEncoder(w)
paddy@69 323 grantType := r.PostFormValue("grant_type")
paddy@84 324 gt, ok := findGrantType(grantType)
paddy@84 325 if !ok {
paddy@82 326 w.WriteHeader(http.StatusBadRequest)
paddy@82 327 renderJSONError(enc, "invalid_request")
paddy@69 328 return
paddy@69 329 }
paddy@84 330 scope, profileID, valid := gt.Validate(w, r, context)
paddy@84 331 if !valid {
paddy@69 332 return
paddy@69 333 }
paddy@84 334 refresh := ""
paddy@84 335 if gt.IssuesRefresh {
paddy@84 336 refresh = uuid.NewID().String()
paddy@69 337 }
paddy@69 338 token := Token{
paddy@88 339 AccessToken: uuid.NewID().String(),
paddy@88 340 RefreshToken: refresh,
paddy@88 341 Created: time.Now(),
paddy@88 342 ExpiresIn: defaultTokenExpiration,
paddy@88 343 RefreshExpiresIn: defaultRefreshTokenExpiration,
paddy@88 344 TokenType: "bearer",
paddy@88 345 Scope: scope,
paddy@88 346 ProfileID: profileID,
paddy@69 347 }
paddy@84 348 err := context.SaveToken(token)
paddy@69 349 if err != nil {
paddy@82 350 w.WriteHeader(http.StatusInternalServerError)
paddy@82 351 renderJSONError(enc, "server_error")
paddy@81 352 return
paddy@69 353 }
paddy@85 354 if gt.ReturnToken(w, r, token, context) && gt.Invalidate != nil {
paddy@85 355 go gt.Invalidate(r, context)
paddy@69 356 }
paddy@69 357 }
paddy@69 358
paddy@68 359 // TODO(paddy): exchange user credentials for access token
paddy@68 360 // TODO(paddy): exchange client credentials for access token
paddy@68 361 // TODO(paddy): implicit grant for access token
paddy@68 362 // TODO(paddy): exchange refresh token for access token