auth

Paddy 2015-01-18 Parent:823517aad893 Child:0a1e16b9c141

122:eb9842ae3ff1 Go to Latest

auth/oauth2.go

Enable the implict grant flow. Add the implicit grant flow. This can't be done in a grant type, because it's not specified through the grant_type parameter, for some absurd reason. Whatever. We basically achieved this by refactoring how we respond to the authorization endpoint, keying off the "response_type" parameter.

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@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@109 121 w.Header().Set("Content-Type", "application/json")
paddy@85 122 err := enc.Encode(resp)
paddy@85 123 if err != nil {
paddy@90 124 log.Println(err)
paddy@85 125 return false
paddy@85 126 }
paddy@85 127 return true
paddy@85 128 }
paddy@85 129
paddy@77 130 // RegisterOAuth2 adds handlers to the passed router to handle the OAuth2 endpoints.
paddy@77 131 func RegisterOAuth2(r *mux.Router, context Context) {
paddy@87 132 r.Handle("/authorize", wrap(context, GetAuthorizationCodeHandler))
paddy@77 133 r.Handle("/token", wrap(context, GetTokenHandler))
paddy@77 134 }
paddy@77 135
paddy@87 136 // GetAuthorizationCodeHandler presents and processes the page for asking a user to grant access
paddy@57 137 // to their data. See RFC 6749, Section 4.1.
paddy@87 138 func GetAuthorizationCodeHandler(w http.ResponseWriter, r *http.Request, context Context) {
paddy@69 139 session, err := checkCookie(r, context)
paddy@69 140 if err != nil {
paddy@76 141 if err == ErrNoSession || err == ErrInvalidSession {
paddy@77 142 redir := buildLoginRedirect(r, context)
paddy@77 143 if redir == "" {
paddy@77 144 log.Println("No login URL configured.")
paddy@77 145 w.WriteHeader(http.StatusInternalServerError)
paddy@87 146 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
paddy@77 147 "internal_error": template.HTML("Missing login URL."),
paddy@77 148 })
paddy@77 149 return
paddy@77 150 }
paddy@77 151 http.Redirect(w, r, redir, http.StatusFound)
paddy@77 152 return
paddy@69 153 }
paddy@77 154 log.Println(err.Error())
paddy@77 155 w.WriteHeader(http.StatusInternalServerError)
paddy@87 156 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
paddy@77 157 "internal_error": template.HTML(err.Error()),
paddy@77 158 })
paddy@77 159 return
paddy@69 160 }
paddy@56 161 if r.URL.Query().Get("client_id") == "" {
paddy@56 162 w.WriteHeader(http.StatusBadRequest)
paddy@87 163 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
paddy@61 164 "error": template.HTML("Client ID must be specified in the request."),
paddy@56 165 })
paddy@56 166 return
paddy@56 167 }
paddy@56 168 clientID, err := uuid.Parse(r.URL.Query().Get("client_id"))
paddy@56 169 if err != nil {
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 is not a valid Client ID."),
paddy@56 173 })
paddy@56 174 return
paddy@56 175 }
paddy@64 176 redirectURI := r.URL.Query().Get("redirect_uri")
paddy@56 177 client, err := context.GetClient(clientID)
paddy@56 178 if err != nil {
paddy@59 179 if err == ErrClientNotFound {
paddy@59 180 w.WriteHeader(http.StatusBadRequest)
paddy@87 181 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
paddy@61 182 "error": template.HTML("The specified Client couldn’t be found."),
paddy@59 183 })
paddy@59 184 } else {
paddy@77 185 log.Println(err.Error())
paddy@59 186 w.WriteHeader(http.StatusInternalServerError)
paddy@87 187 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
paddy@61 188 "internal_error": template.HTML(err.Error()),
paddy@59 189 })
paddy@59 190 }
paddy@56 191 return
paddy@56 192 }
paddy@117 193 // BUG(paddy): checking if the redirect URI is valid should be a helper function
paddy@56 194 // whether a redirect URI is valid or not depends on the number of endpoints
paddy@56 195 // the client has registered
paddy@56 196 numEndpoints, err := context.CountEndpoints(clientID)
paddy@56 197 if err != nil {
paddy@77 198 log.Println(err.Error())
paddy@56 199 w.WriteHeader(http.StatusInternalServerError)
paddy@87 200 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
paddy@61 201 "internal_error": template.HTML(err.Error()),
paddy@56 202 })
paddy@56 203 return
paddy@56 204 }
paddy@56 205 var validURI bool
paddy@58 206 if redirectURI != "" {
paddy@58 207 validURI, err = context.CheckEndpoint(clientID, redirectURI)
paddy@56 208 if err != nil {
paddy@116 209 if err == ErrEndpointURINotURL {
paddy@116 210 w.WriteHeader(http.StatusBadRequest)
paddy@116 211 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
paddy@116 212 "error": template.HTML("The redirect_uri specified is not valid."),
paddy@116 213 })
paddy@116 214 return
paddy@116 215 }
paddy@77 216 log.Println(err.Error())
paddy@56 217 w.WriteHeader(http.StatusInternalServerError)
paddy@87 218 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
paddy@61 219 "internal_error": template.HTML(err.Error()),
paddy@56 220 })
paddy@56 221 return
paddy@56 222 }
paddy@56 223 } else if redirectURI == "" && numEndpoints == 1 {
paddy@56 224 // if we don't specify the endpoint and there's only one endpoint, the
paddy@56 225 // request is valid, and we're redirecting to that one endpoint
paddy@56 226 validURI = true
paddy@56 227 endpoints, err := context.ListEndpoints(clientID, 1, 0)
paddy@56 228 if err != nil {
paddy@77 229 log.Println(err.Error())
paddy@56 230 w.WriteHeader(http.StatusInternalServerError)
paddy@87 231 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
paddy@61 232 "internal_error": template.HTML(err.Error()),
paddy@56 233 })
paddy@56 234 return
paddy@56 235 }
paddy@56 236 if len(endpoints) != 1 {
paddy@56 237 validURI = false
paddy@56 238 } else {
paddy@116 239 redirectURI = endpoints[0].URI
paddy@56 240 }
paddy@56 241 } else {
paddy@56 242 validURI = false
paddy@56 243 }
paddy@56 244 if !validURI {
paddy@56 245 w.WriteHeader(http.StatusBadRequest)
paddy@87 246 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
paddy@61 247 "error": template.HTML("The redirect_uri specified is not valid."),
paddy@56 248 })
paddy@56 249 return
paddy@56 250 }
paddy@116 251 redirectURL, err := url.Parse(redirectURI)
paddy@116 252 if err != nil {
paddy@116 253 w.WriteHeader(http.StatusBadRequest)
paddy@116 254 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
paddy@116 255 "error": template.HTML("The redirect_uri specified is not valid."),
paddy@116 256 })
paddy@116 257 return
paddy@116 258 }
paddy@60 259 scope := r.URL.Query().Get("scope")
paddy@60 260 state := r.URL.Query().Get("state")
paddy@122 261 responseType := r.URL.Query().Get("response_type")
paddy@122 262 q := redirectURL.Query()
paddy@122 263 q.Add("state", state)
paddy@122 264 if responseType != "code" && responseType != "token" {
paddy@65 265 q.Add("error", "invalid_request")
paddy@65 266 redirectURL.RawQuery = q.Encode()
paddy@60 267 http.Redirect(w, r, redirectURL.String(), http.StatusFound)
paddy@60 268 return
paddy@56 269 }
paddy@56 270 if r.Method == "POST" {
paddy@63 271 // BUG(paddy): We need to implement CSRF protection when obtaining a grant code.
paddy@56 272 if r.PostFormValue("grant") == "approved" {
paddy@122 273 var fragment bool
paddy@122 274 switch responseType {
paddy@122 275 case "code":
paddy@122 276 code := uuid.NewID().String()
paddy@122 277 authCode := AuthorizationCode{
paddy@122 278 Code: code,
paddy@122 279 Created: time.Now(),
paddy@122 280 ExpiresIn: defaultAuthorizationCodeExpiration,
paddy@122 281 ClientID: clientID,
paddy@122 282 Scope: scope,
paddy@122 283 RedirectURI: r.URL.Query().Get("redirect_uri"),
paddy@122 284 State: state,
paddy@122 285 ProfileID: session.ProfileID,
paddy@122 286 }
paddy@122 287 err := context.SaveAuthorizationCode(authCode)
paddy@122 288 if err != nil {
paddy@122 289 log.Println("Error saving authorization code:", err)
paddy@122 290 q.Add("error", "server_error")
paddy@122 291 break
paddy@122 292 }
paddy@122 293 q.Add("code", code)
paddy@122 294 case "token":
paddy@122 295 token := Token{
paddy@122 296 AccessToken: uuid.NewID().String(),
paddy@122 297 Created: time.Now(),
paddy@122 298 CreatedFrom: "",
paddy@122 299 ExpiresIn: defaultTokenExpiration,
paddy@122 300 TokenType: "bearer",
paddy@122 301 Scope: scope,
paddy@122 302 ProfileID: session.ProfileID,
paddy@122 303 }
paddy@122 304 err := context.SaveToken(token)
paddy@122 305 if err != nil {
paddy@122 306 log.Println("Error saving token:", err)
paddy@122 307 q.Add("error", "server_error")
paddy@122 308 break
paddy@122 309 }
paddy@122 310 q = url.Values{} // we're not altering the querystring, so don't clone it
paddy@122 311 q.Add("access_token", token.AccessToken)
paddy@122 312 q.Add("token_type", token.TokenType)
paddy@122 313 q.Add("expires_in", strconv.FormatInt(int64(token.ExpiresIn), 10))
paddy@122 314 q.Add("scope", token.Scope)
paddy@122 315 q.Add("state", state) // we wiped out the old values, so we need to set the state again
paddy@122 316 fragment = true
paddy@60 317 }
paddy@122 318 if fragment {
paddy@122 319 redirectURL.Fragment = q.Encode()
paddy@122 320 } else {
paddy@66 321 redirectURL.RawQuery = q.Encode()
paddy@60 322 }
paddy@60 323 http.Redirect(w, r, redirectURL.String(), http.StatusFound)
paddy@60 324 return
paddy@56 325 }
paddy@66 326 q.Add("error", "access_denied")
paddy@66 327 redirectURL.RawQuery = q.Encode()
paddy@60 328 http.Redirect(w, r, redirectURL.String(), http.StatusFound)
paddy@60 329 return
paddy@56 330 }
paddy@85 331 profile, err := context.GetProfileByID(session.ProfileID)
paddy@85 332 if err != nil {
paddy@122 333 log.Println("Error getting profile from session:", err)
paddy@85 334 q.Add("error", "server_error")
paddy@85 335 redirectURL.RawQuery = q.Encode()
paddy@85 336 http.Redirect(w, r, redirectURL.String(), http.StatusFound)
paddy@85 337 return
paddy@85 338 }
paddy@51 339 w.WriteHeader(http.StatusOK)
paddy@87 340 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
paddy@85 341 "client": client,
paddy@85 342 "redirectURL": redirectURL,
paddy@85 343 "scope": scope,
paddy@85 344 "profile": profile,
paddy@56 345 })
paddy@51 346 }
paddy@68 347
paddy@69 348 // GetTokenHandler allows a client to exchange an authorization grant for an
paddy@69 349 // access token. See RFC 6749 Section 4.1.3.
paddy@69 350 func GetTokenHandler(w http.ResponseWriter, r *http.Request, context Context) {
paddy@69 351 enc := json.NewEncoder(w)
paddy@69 352 grantType := r.PostFormValue("grant_type")
paddy@84 353 gt, ok := findGrantType(grantType)
paddy@84 354 if !ok {
paddy@82 355 w.WriteHeader(http.StatusBadRequest)
paddy@82 356 renderJSONError(enc, "invalid_request")
paddy@69 357 return
paddy@69 358 }
paddy@84 359 scope, profileID, valid := gt.Validate(w, r, context)
paddy@84 360 if !valid {
paddy@69 361 return
paddy@69 362 }
paddy@84 363 refresh := ""
paddy@84 364 if gt.IssuesRefresh {
paddy@84 365 refresh = uuid.NewID().String()
paddy@69 366 }
paddy@69 367 token := Token{
paddy@88 368 AccessToken: uuid.NewID().String(),
paddy@88 369 RefreshToken: refresh,
paddy@88 370 Created: time.Now(),
paddy@88 371 ExpiresIn: defaultTokenExpiration,
paddy@88 372 RefreshExpiresIn: defaultRefreshTokenExpiration,
paddy@88 373 TokenType: "bearer",
paddy@88 374 Scope: scope,
paddy@88 375 ProfileID: profileID,
paddy@69 376 }
paddy@84 377 err := context.SaveToken(token)
paddy@69 378 if err != nil {
paddy@82 379 w.WriteHeader(http.StatusInternalServerError)
paddy@82 380 renderJSONError(enc, "server_error")
paddy@81 381 return
paddy@69 382 }
paddy@85 383 if gt.ReturnToken(w, r, token, context) && gt.Invalidate != nil {
paddy@85 384 go gt.Invalidate(r, context)
paddy@69 385 }
paddy@69 386 }
paddy@69 387
paddy@68 388 // TODO(paddy): implicit grant for access token
paddy@68 389 // TODO(paddy): exchange refresh token for access token