auth

Paddy 2015-01-04 Parent:c03b5eb3179e Child:e000b1c24fc0

109:9a5999963868 Go to Latest

auth/oauth2.go

Correctly set Content-Type header when obtaining a token. Some OAuth2 clients break if you don't correctly set the Content-Type header when obtaining a token. Also, let's just try to be good Content-Type/Accept citizens, all around.

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@107 13 "code.secondbit.org/uuid.hg"
paddy@82 14 "github.com/gorilla/mux"
paddy@51 15 )
paddy@51 16
paddy@60 17 const (
paddy@87 18 authCookieName = "auth"
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@85 58 // 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 59 // was successfully returned and the Invalidate function will be called asynchronously.
paddy@84 60 type GrantType struct {
paddy@84 61 Validate func(w http.ResponseWriter, r *http.Request, context Context) (scope string, profileID uuid.ID, valid bool)
paddy@90 62 Invalidate func(r *http.Request, context Context) error
paddy@85 63 ReturnToken func(w http.ResponseWriter, r *http.Request, token Token, context Context) bool
paddy@84 64 IssuesRefresh bool
paddy@84 65 }
paddy@84 66
paddy@69 67 type tokenResponse struct {
paddy@69 68 AccessToken string `json:"access_token"`
paddy@69 69 TokenType string `json:"token_type,omitempty"`
paddy@69 70 ExpiresIn int32 `json:"expires_in,omitempty"`
paddy@69 71 RefreshToken string `json:"refresh_token,omitempty"`
paddy@69 72 }
paddy@69 73
paddy@82 74 type errorResponse struct {
paddy@82 75 Error string `json:"error"`
paddy@82 76 Description string `json:"error_description,omitempty"`
paddy@82 77 URI string `json:"error_uri,omitempty"`
paddy@82 78 }
paddy@82 79
paddy@84 80 // RegisterGrantType associates a string with a GrantType. When the string is used as the value for "grant_type" when obtaining
paddy@84 81 // an access token, the associated GrantType's properties will be used.
paddy@84 82 //
paddy@84 83 // RegisterGrantType should be called in the `init()` function of packages, much like database/sql registers drivers. It will panic
paddy@84 84 // if a GrantType tries to register under a string that already has a GrantType registered for it.
paddy@84 85 func RegisterGrantType(name string, g GrantType) {
paddy@84 86 grantTypesMap.Lock()
paddy@84 87 defer grantTypesMap.Unlock()
paddy@84 88 if _, ok := grantTypesMap.types[name]; ok {
paddy@84 89 panic("Duplicate registration of grant_type " + name)
paddy@84 90 }
paddy@84 91 grantTypesMap.types[name] = g
paddy@84 92 }
paddy@84 93
paddy@84 94 func findGrantType(name string) (GrantType, bool) {
paddy@84 95 grantTypesMap.RLock()
paddy@84 96 defer grantTypesMap.RUnlock()
paddy@84 97 t, ok := grantTypesMap.types[name]
paddy@84 98 return t, ok
paddy@84 99 }
paddy@84 100
paddy@82 101 func renderJSONError(enc *json.Encoder, errorType string) {
paddy@82 102 err := enc.Encode(errorResponse{
paddy@82 103 Error: errorType,
paddy@82 104 })
paddy@82 105 if err != nil {
paddy@90 106 log.Println(err)
paddy@69 107 }
paddy@69 108 }
paddy@69 109
paddy@86 110 // RenderJSONToken is an implementation of the ReturnToken function for GrantTypes. It returns the token using JSON
paddy@86 111 // according to the spec. See RFC 6479, Section 4.1.4.
paddy@85 112 func RenderJSONToken(w http.ResponseWriter, r *http.Request, token Token, context Context) bool {
paddy@85 113 enc := json.NewEncoder(w)
paddy@85 114 resp := tokenResponse{
paddy@85 115 AccessToken: token.AccessToken,
paddy@85 116 RefreshToken: token.RefreshToken,
paddy@85 117 ExpiresIn: token.ExpiresIn,
paddy@85 118 TokenType: token.TokenType,
paddy@85 119 }
paddy@109 120 w.Header().Set("Content-Type", "application/json")
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