auth
2015-12-14
Parent:b7e685839a1b
auth/oauth2.go
Update nsq import path. go-nsq has moved to nsqio/go-nsq, so we need to update the import path appropriately.
| paddy@51 | 1 package auth |
| paddy@51 | 2 |
| paddy@51 | 3 import ( |
| paddy@147 | 4 "crypto/rand" |
| paddy@147 | 5 "encoding/hex" |
| paddy@69 | 6 "encoding/json" |
| paddy@69 | 7 "errors" |
| paddy@61 | 8 "html/template" |
| paddy@147 | 9 "io" |
| paddy@77 | 10 "log" |
| paddy@51 | 11 "net/http" |
| paddy@60 | 12 "net/url" |
| paddy@122 | 13 "strconv" |
| paddy@135 | 14 "strings" |
| paddy@84 | 15 "sync" |
| paddy@60 | 16 "time" |
| paddy@56 | 17 |
| paddy@181 | 18 "code.secondbit.org/scopes.hg/types" |
| paddy@107 | 19 "code.secondbit.org/uuid.hg" |
| paddy@82 | 20 "github.com/gorilla/mux" |
| paddy@51 | 21 ) |
| paddy@51 | 22 |
| paddy@60 | 23 const ( |
| paddy@87 | 24 defaultAuthorizationCodeExpiration = 600 // default to ten minute grant expirations |
| paddy@87 | 25 getAuthorizationCodeTemplateName = "get_grant" |
| paddy@60 | 26 ) |
| paddy@51 | 27 |
| paddy@69 | 28 var ( |
| paddy@69 | 29 // ErrNoAuth is returned when an Authorization header is not present or is empty. |
| paddy@69 | 30 ErrNoAuth = errors.New("no authorization header supplied") |
| paddy@69 | 31 // ErrInvalidAuthFormat is returned when an Authorization header is present but not the correct format. |
| paddy@69 | 32 ErrInvalidAuthFormat = errors.New("authorization header is not in a valid format") |
| paddy@69 | 33 // ErrIncorrectAuth is returned when a user authentication attempt does not match the stored values. |
| paddy@69 | 34 ErrIncorrectAuth = errors.New("invalid authentication") |
| paddy@69 | 35 // ErrInvalidPassphraseScheme is returned when an undefined passphrase scheme is used. |
| paddy@69 | 36 ErrInvalidPassphraseScheme = errors.New("invalid passphrase scheme") |
| paddy@69 | 37 // ErrNoSession is returned when no session ID is passed with a request. |
| paddy@69 | 38 ErrNoSession = errors.New("no session ID found") |
| paddy@84 | 39 |
| paddy@84 | 40 grantTypesMap = grantTypes{types: map[string]GrantType{}} |
| paddy@69 | 41 ) |
| paddy@69 | 42 |
| paddy@84 | 43 type grantTypes struct { |
| paddy@84 | 44 types map[string]GrantType |
| paddy@84 | 45 sync.RWMutex |
| paddy@84 | 46 } |
| paddy@84 | 47 |
| paddy@84 | 48 // GrantType defines a set of functions and metadata around a specific authorization grant strategy. |
| paddy@84 | 49 // |
| paddy@84 | 50 // The Validate function will be called when requests are made that match the GrantType, and should write any |
| paddy@84 | 51 // errors to the ResponseWriter. It is responsible for determining if the grant is valid and a token should be issued. |
| paddy@84 | 52 // 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 | 53 // is valid or not. It must not be nil. |
| paddy@84 | 54 // |
| paddy@84 | 55 // The Invalidate function will be called when the grant has successfully generated a token and the token has successfully |
| paddy@84 | 56 // been conveyed to the user. The Invalidate function is always called asynchronously, outside the request. It should take |
| paddy@84 | 57 // care of marking the grant as used, if the GrantType requires grants to be one-time only grants. The Invalidate function |
| paddy@84 | 58 // can be nil. |
| paddy@84 | 59 // |
| paddy@84 | 60 // IssuesRefresh determines whether the GrantType should yield a refresh token as well as an access token. If true, the client |
| paddy@84 | 61 // will be issued a refresh token. |
| paddy@85 | 62 // |
| paddy@123 | 63 // AllowsPublic determines whether the GrantType should allow public clients to use that grant. If true, clients without |
| paddy@123 | 64 // credentials will be able to use the grant to obtain a token. |
| paddy@123 | 65 // |
| paddy@124 | 66 // AuditString should return the string that will be saved in the resulting Token's CreatedFrom field, as an audit log of how |
| paddy@124 | 67 // the Token was authorized. |
| paddy@124 | 68 // |
| paddy@85 | 69 // 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 | 70 // was successfully returned and the Invalidate function will be called asynchronously. |
| paddy@84 | 71 type GrantType struct { |
| paddy@181 | 72 Validate func(w http.ResponseWriter, r *http.Request, context Context) (scopes scopeTypes.Scopes, profileID uuid.ID, valid bool) |
| paddy@90 | 73 Invalidate func(r *http.Request, context Context) error |
| paddy@85 | 74 ReturnToken func(w http.ResponseWriter, r *http.Request, token Token, context Context) bool |
| paddy@124 | 75 AuditString func(r *http.Request) string |
| paddy@84 | 76 IssuesRefresh bool |
| paddy@123 | 77 AllowsPublic bool |
| paddy@84 | 78 } |
| paddy@84 | 79 |
| paddy@69 | 80 type tokenResponse struct { |
| paddy@69 | 81 AccessToken string `json:"access_token"` |
| paddy@69 | 82 TokenType string `json:"token_type,omitempty"` |
| paddy@69 | 83 ExpiresIn int32 `json:"expires_in,omitempty"` |
| paddy@69 | 84 RefreshToken string `json:"refresh_token,omitempty"` |
| paddy@69 | 85 } |
| paddy@69 | 86 |
| paddy@82 | 87 type errorResponse struct { |
| paddy@82 | 88 Error string `json:"error"` |
| paddy@82 | 89 Description string `json:"error_description,omitempty"` |
| paddy@82 | 90 URI string `json:"error_uri,omitempty"` |
| paddy@82 | 91 } |
| paddy@82 | 92 |
| paddy@84 | 93 // RegisterGrantType associates a string with a GrantType. When the string is used as the value for "grant_type" when obtaining |
| paddy@84 | 94 // an access token, the associated GrantType's properties will be used. |
| paddy@84 | 95 // |
| paddy@84 | 96 // RegisterGrantType should be called in the `init()` function of packages, much like database/sql registers drivers. It will panic |
| paddy@84 | 97 // if a GrantType tries to register under a string that already has a GrantType registered for it. |
| paddy@84 | 98 func RegisterGrantType(name string, g GrantType) { |
| paddy@84 | 99 grantTypesMap.Lock() |
| paddy@84 | 100 defer grantTypesMap.Unlock() |
| paddy@84 | 101 if _, ok := grantTypesMap.types[name]; ok { |
| paddy@84 | 102 panic("Duplicate registration of grant_type " + name) |
| paddy@84 | 103 } |
| paddy@84 | 104 grantTypesMap.types[name] = g |
| paddy@84 | 105 } |
| paddy@84 | 106 |
| paddy@84 | 107 func findGrantType(name string) (GrantType, bool) { |
| paddy@84 | 108 grantTypesMap.RLock() |
| paddy@84 | 109 defer grantTypesMap.RUnlock() |
| paddy@84 | 110 t, ok := grantTypesMap.types[name] |
| paddy@84 | 111 return t, ok |
| paddy@84 | 112 } |
| paddy@84 | 113 |
| paddy@82 | 114 func renderJSONError(enc *json.Encoder, errorType string) { |
| paddy@82 | 115 err := enc.Encode(errorResponse{ |
| paddy@82 | 116 Error: errorType, |
| paddy@82 | 117 }) |
| paddy@82 | 118 if err != nil { |
| paddy@90 | 119 log.Println(err) |
| paddy@69 | 120 } |
| paddy@69 | 121 } |
| paddy@69 | 122 |
| paddy@86 | 123 // RenderJSONToken is an implementation of the ReturnToken function for GrantTypes. It returns the token using JSON |
| paddy@86 | 124 // according to the spec. See RFC 6479, Section 4.1.4. |
| paddy@85 | 125 func RenderJSONToken(w http.ResponseWriter, r *http.Request, token Token, context Context) bool { |
| paddy@85 | 126 enc := json.NewEncoder(w) |
| paddy@85 | 127 resp := tokenResponse{ |
| paddy@85 | 128 AccessToken: token.AccessToken, |
| paddy@85 | 129 RefreshToken: token.RefreshToken, |
| paddy@85 | 130 ExpiresIn: token.ExpiresIn, |
| paddy@85 | 131 TokenType: token.TokenType, |
| paddy@85 | 132 } |
| paddy@109 | 133 w.Header().Set("Content-Type", "application/json") |
| paddy@85 | 134 err := enc.Encode(resp) |
| paddy@85 | 135 if err != nil { |
| paddy@90 | 136 log.Println(err) |
| paddy@85 | 137 return false |
| paddy@85 | 138 } |
| paddy@85 | 139 return true |
| paddy@85 | 140 } |
| paddy@85 | 141 |
| paddy@77 | 142 // RegisterOAuth2 adds handlers to the passed router to handle the OAuth2 endpoints. |
| paddy@77 | 143 func RegisterOAuth2(r *mux.Router, context Context) { |
| paddy@87 | 144 r.Handle("/authorize", wrap(context, GetAuthorizationCodeHandler)) |
| paddy@77 | 145 r.Handle("/token", wrap(context, GetTokenHandler)) |
| paddy@77 | 146 } |
| paddy@77 | 147 |
| paddy@87 | 148 // GetAuthorizationCodeHandler presents and processes the page for asking a user to grant access |
| paddy@57 | 149 // to their data. See RFC 6749, Section 4.1. |
| paddy@87 | 150 func GetAuthorizationCodeHandler(w http.ResponseWriter, r *http.Request, context Context) { |
| paddy@69 | 151 session, err := checkCookie(r, context) |
| paddy@69 | 152 if err != nil { |
| paddy@76 | 153 if err == ErrNoSession || err == ErrInvalidSession { |
| paddy@77 | 154 redir := buildLoginRedirect(r, context) |
| paddy@77 | 155 if redir == "" { |
| paddy@77 | 156 log.Println("No login URL configured.") |
| paddy@77 | 157 w.WriteHeader(http.StatusInternalServerError) |
| paddy@87 | 158 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{ |
| paddy@77 | 159 "internal_error": template.HTML("Missing login URL."), |
| paddy@77 | 160 }) |
| paddy@77 | 161 return |
| paddy@77 | 162 } |
| paddy@77 | 163 http.Redirect(w, r, redir, http.StatusFound) |
| paddy@77 | 164 return |
| paddy@69 | 165 } |
| paddy@77 | 166 log.Println(err.Error()) |
| paddy@77 | 167 w.WriteHeader(http.StatusInternalServerError) |
| paddy@87 | 168 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{ |
| paddy@77 | 169 "internal_error": template.HTML(err.Error()), |
| paddy@77 | 170 }) |
| paddy@77 | 171 return |
| paddy@69 | 172 } |
| paddy@56 | 173 if r.URL.Query().Get("client_id") == "" { |
| 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 must be specified in the request."), |
| paddy@56 | 177 }) |
| paddy@56 | 178 return |
| paddy@56 | 179 } |
| paddy@56 | 180 clientID, err := uuid.Parse(r.URL.Query().Get("client_id")) |
| paddy@56 | 181 if err != nil { |
| paddy@56 | 182 w.WriteHeader(http.StatusBadRequest) |
| paddy@87 | 183 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{ |
| paddy@61 | 184 "error": template.HTML("client_id is not a valid Client ID."), |
| paddy@56 | 185 }) |
| paddy@56 | 186 return |
| paddy@56 | 187 } |
| paddy@64 | 188 redirectURI := r.URL.Query().Get("redirect_uri") |
| paddy@56 | 189 client, err := context.GetClient(clientID) |
| paddy@56 | 190 if err != nil { |
| paddy@59 | 191 if err == ErrClientNotFound { |
| paddy@59 | 192 w.WriteHeader(http.StatusBadRequest) |
| paddy@87 | 193 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{ |
| paddy@61 | 194 "error": template.HTML("The specified Client couldn’t be found."), |
| paddy@59 | 195 }) |
| paddy@59 | 196 } else { |
| paddy@77 | 197 log.Println(err.Error()) |
| paddy@59 | 198 w.WriteHeader(http.StatusInternalServerError) |
| paddy@87 | 199 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{ |
| paddy@61 | 200 "internal_error": template.HTML(err.Error()), |
| paddy@59 | 201 }) |
| paddy@59 | 202 } |
| paddy@56 | 203 return |
| paddy@56 | 204 } |
| paddy@128 | 205 // BUG(paddy): Checking if the redirect URI is valid should be a helper function. |
| paddy@128 | 206 |
| paddy@56 | 207 // whether a redirect URI is valid or not depends on the number of endpoints |
| paddy@56 | 208 // the client has registered |
| paddy@56 | 209 numEndpoints, err := context.CountEndpoints(clientID) |
| paddy@56 | 210 if err != nil { |
| paddy@77 | 211 log.Println(err.Error()) |
| paddy@56 | 212 w.WriteHeader(http.StatusInternalServerError) |
| paddy@87 | 213 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{ |
| paddy@61 | 214 "internal_error": template.HTML(err.Error()), |
| paddy@56 | 215 }) |
| paddy@56 | 216 return |
| paddy@56 | 217 } |
| paddy@56 | 218 var validURI bool |
| paddy@58 | 219 if redirectURI != "" { |
| paddy@58 | 220 validURI, err = context.CheckEndpoint(clientID, redirectURI) |
| paddy@56 | 221 if err != nil { |
| paddy@116 | 222 if err == ErrEndpointURINotURL { |
| paddy@116 | 223 w.WriteHeader(http.StatusBadRequest) |
| paddy@116 | 224 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{ |
| paddy@116 | 225 "error": template.HTML("The redirect_uri specified is not valid."), |
| paddy@116 | 226 }) |
| paddy@116 | 227 return |
| paddy@116 | 228 } |
| 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 } else if redirectURI == "" && numEndpoints == 1 { |
| paddy@56 | 237 // if we don't specify the endpoint and there's only one endpoint, the |
| paddy@56 | 238 // request is valid, and we're redirecting to that one endpoint |
| paddy@56 | 239 validURI = true |
| paddy@56 | 240 endpoints, err := context.ListEndpoints(clientID, 1, 0) |
| paddy@56 | 241 if err != nil { |
| paddy@77 | 242 log.Println(err.Error()) |
| paddy@56 | 243 w.WriteHeader(http.StatusInternalServerError) |
| paddy@87 | 244 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{ |
| paddy@61 | 245 "internal_error": template.HTML(err.Error()), |
| paddy@56 | 246 }) |
| paddy@56 | 247 return |
| paddy@56 | 248 } |
| paddy@56 | 249 if len(endpoints) != 1 { |
| paddy@56 | 250 validURI = false |
| paddy@56 | 251 } else { |
| paddy@116 | 252 redirectURI = endpoints[0].URI |
| paddy@56 | 253 } |
| paddy@56 | 254 } else { |
| paddy@56 | 255 validURI = false |
| paddy@56 | 256 } |
| paddy@56 | 257 if !validURI { |
| paddy@56 | 258 w.WriteHeader(http.StatusBadRequest) |
| paddy@87 | 259 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{ |
| paddy@61 | 260 "error": template.HTML("The redirect_uri specified is not valid."), |
| paddy@56 | 261 }) |
| paddy@56 | 262 return |
| paddy@56 | 263 } |
| paddy@116 | 264 redirectURL, err := url.Parse(redirectURI) |
| paddy@116 | 265 if err != nil { |
| paddy@116 | 266 w.WriteHeader(http.StatusBadRequest) |
| paddy@116 | 267 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{ |
| paddy@116 | 268 "error": template.HTML("The redirect_uri specified is not valid."), |
| paddy@116 | 269 }) |
| paddy@116 | 270 return |
| paddy@116 | 271 } |
| paddy@60 | 272 state := r.URL.Query().Get("state") |
| paddy@122 | 273 responseType := r.URL.Query().Get("response_type") |
| paddy@122 | 274 q := redirectURL.Query() |
| paddy@122 | 275 q.Add("state", state) |
| paddy@122 | 276 if responseType != "code" && responseType != "token" { |
| paddy@65 | 277 q.Add("error", "invalid_request") |
| paddy@65 | 278 redirectURL.RawQuery = q.Encode() |
| paddy@60 | 279 http.Redirect(w, r, redirectURL.String(), http.StatusFound) |
| paddy@60 | 280 return |
| paddy@56 | 281 } |
| paddy@181 | 282 scopes := scopeTypes.StringsToScopes(strings.Split(r.URL.Query().Get("scope"), " ")) |
| paddy@181 | 283 // BUG(paddy): need to check if Scopes actually exist |
| paddy@181 | 284 /*if err == ErrScopeNotFound { |
| paddy@181 | 285 q.Add("error", "invalid_scope") |
| paddy@135 | 286 redirectURL.RawQuery = q.Encode() |
| paddy@135 | 287 http.Redirect(w, r, redirectURL.String(), http.StatusFound) |
| paddy@135 | 288 return |
| paddy@135 | 289 } |
| paddy@181 | 290 log.Println("Error retrieving scopes:", err) |
| paddy@181 | 291 q.Add("error", "server_error") |
| paddy@181 | 292 redirectURL.RawQuery = q.Encode() |
| paddy@181 | 293 http.Redirect(w, r, redirectURL.String(), http.StatusFound) |
| paddy@181 | 294 return |
| paddy@181 | 295 */ |
| paddy@56 | 296 if r.Method == "POST" { |
| paddy@132 | 297 if checkCSRF(r, session) != nil { |
| paddy@132 | 298 log.Println("CSRF attempt detected.") |
| paddy@132 | 299 w.WriteHeader(http.StatusInternalServerError) |
| paddy@132 | 300 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{ |
| paddy@132 | 301 "error": template.HTML("There was an error authenticating your request."), |
| paddy@132 | 302 }) |
| paddy@132 | 303 return |
| paddy@132 | 304 } |
| paddy@56 | 305 if r.PostFormValue("grant") == "approved" { |
| paddy@122 | 306 var fragment bool |
| paddy@122 | 307 switch responseType { |
| paddy@122 | 308 case "code": |
| paddy@147 | 309 code := make([]byte, 16) |
| paddy@147 | 310 _, err := io.ReadFull(rand.Reader, code) |
| paddy@147 | 311 if err != nil { |
| paddy@147 | 312 log.Printf("Error generating code: %#+v\n", err) |
| paddy@147 | 313 q.Add("error", "server_error") |
| paddy@147 | 314 break |
| paddy@147 | 315 } |
| paddy@122 | 316 authCode := AuthorizationCode{ |
| paddy@147 | 317 Code: hex.EncodeToString(code), |
| paddy@122 | 318 Created: time.Now(), |
| paddy@122 | 319 ExpiresIn: defaultAuthorizationCodeExpiration, |
| paddy@122 | 320 ClientID: clientID, |
| paddy@163 | 321 Scopes: scopes, |
| paddy@122 | 322 RedirectURI: r.URL.Query().Get("redirect_uri"), |
| paddy@122 | 323 State: state, |
| paddy@122 | 324 ProfileID: session.ProfileID, |
| paddy@122 | 325 } |
| paddy@147 | 326 err = context.SaveAuthorizationCode(authCode) |
| paddy@122 | 327 if err != nil { |
| paddy@122 | 328 log.Println("Error saving authorization code:", err) |
| paddy@122 | 329 q.Add("error", "server_error") |
| paddy@122 | 330 break |
| paddy@122 | 331 } |
| paddy@147 | 332 q.Add("code", authCode.Code) |
| paddy@122 | 333 case "token": |
| paddy@122 | 334 token := Token{ |
| paddy@122 | 335 Created: time.Now(), |
| paddy@125 | 336 CreatedFrom: "implicit", |
| paddy@122 | 337 ExpiresIn: defaultTokenExpiration, |
| paddy@122 | 338 TokenType: "bearer", |
| paddy@163 | 339 Scopes: scopes, |
| paddy@122 | 340 ProfileID: session.ProfileID, |
| paddy@125 | 341 ClientID: clientID, |
| paddy@122 | 342 } |
| paddy@168 | 343 access, err := token.GenerateAccessToken(context.config.JWTPrivateKey) |
| paddy@168 | 344 if err != nil { |
| paddy@168 | 345 log.Printf("Error signing token: %+v\n", err) |
| paddy@168 | 346 q.Add("error", "server_error") |
| paddy@168 | 347 break |
| paddy@168 | 348 } |
| paddy@168 | 349 token.AccessToken = access |
| paddy@168 | 350 err = context.SaveToken(token) |
| paddy@122 | 351 if err != nil { |
| paddy@122 | 352 log.Println("Error saving token:", err) |
| paddy@122 | 353 q.Add("error", "server_error") |
| paddy@122 | 354 break |
| paddy@122 | 355 } |
| paddy@122 | 356 q = url.Values{} // we're not altering the querystring, so don't clone it |
| paddy@122 | 357 q.Add("access_token", token.AccessToken) |
| paddy@122 | 358 q.Add("token_type", token.TokenType) |
| paddy@122 | 359 q.Add("expires_in", strconv.FormatInt(int64(token.ExpiresIn), 10)) |
| paddy@163 | 360 q.Add("scope", strings.Join(token.Scopes.Strings(), " ")) |
| paddy@122 | 361 q.Add("state", state) // we wiped out the old values, so we need to set the state again |
| paddy@122 | 362 fragment = true |
| paddy@60 | 363 } |
| paddy@122 | 364 if fragment { |
| paddy@122 | 365 redirectURL.Fragment = q.Encode() |
| paddy@122 | 366 } else { |
| paddy@66 | 367 redirectURL.RawQuery = q.Encode() |
| paddy@60 | 368 } |
| paddy@60 | 369 http.Redirect(w, r, redirectURL.String(), http.StatusFound) |
| paddy@60 | 370 return |
| paddy@56 | 371 } |
| paddy@66 | 372 q.Add("error", "access_denied") |
| paddy@66 | 373 redirectURL.RawQuery = q.Encode() |
| paddy@60 | 374 http.Redirect(w, r, redirectURL.String(), http.StatusFound) |
| paddy@60 | 375 return |
| paddy@56 | 376 } |
| paddy@85 | 377 profile, err := context.GetProfileByID(session.ProfileID) |
| paddy@85 | 378 if err != nil { |
| paddy@122 | 379 log.Println("Error getting profile from session:", err) |
| paddy@85 | 380 q.Add("error", "server_error") |
| paddy@85 | 381 redirectURL.RawQuery = q.Encode() |
| paddy@85 | 382 http.Redirect(w, r, redirectURL.String(), http.StatusFound) |
| paddy@85 | 383 return |
| paddy@85 | 384 } |
| paddy@51 | 385 w.WriteHeader(http.StatusOK) |
| paddy@87 | 386 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{ |
| paddy@85 | 387 "client": client, |
| paddy@85 | 388 "redirectURL": redirectURL, |
| paddy@135 | 389 "scopes": scopes, |
| paddy@85 | 390 "profile": profile, |
| paddy@132 | 391 "csrftoken": session.CSRFToken, |
| paddy@56 | 392 }) |
| paddy@51 | 393 } |
| paddy@68 | 394 |
| paddy@69 | 395 // GetTokenHandler allows a client to exchange an authorization grant for an |
| paddy@69 | 396 // access token. See RFC 6749 Section 4.1.3. |
| paddy@69 | 397 func GetTokenHandler(w http.ResponseWriter, r *http.Request, context Context) { |
| paddy@69 | 398 enc := json.NewEncoder(w) |
| paddy@69 | 399 grantType := r.PostFormValue("grant_type") |
| paddy@84 | 400 gt, ok := findGrantType(grantType) |
| paddy@84 | 401 if !ok { |
| paddy@82 | 402 w.WriteHeader(http.StatusBadRequest) |
| paddy@82 | 403 renderJSONError(enc, "invalid_request") |
| paddy@69 | 404 return |
| paddy@69 | 405 } |
| paddy@123 | 406 clientID, success := verifyClient(w, r, gt.AllowsPublic, context) |
| paddy@123 | 407 if !success { |
| paddy@123 | 408 return |
| paddy@123 | 409 } |
| paddy@135 | 410 scopes, profileID, valid := gt.Validate(w, r, context) |
| paddy@84 | 411 if !valid { |
| paddy@69 | 412 return |
| paddy@69 | 413 } |
| paddy@84 | 414 refresh := "" |
| paddy@84 | 415 if gt.IssuesRefresh { |
| paddy@84 | 416 refresh = uuid.NewID().String() |
| paddy@69 | 417 } |
| paddy@69 | 418 token := Token{ |
| paddy@125 | 419 AccessToken: uuid.NewID().String(), |
| paddy@125 | 420 RefreshToken: refresh, |
| paddy@125 | 421 Created: time.Now(), |
| paddy@125 | 422 CreatedFrom: gt.AuditString(r), |
| paddy@125 | 423 ExpiresIn: defaultTokenExpiration, |
| paddy@125 | 424 TokenType: "bearer", |
| paddy@135 | 425 Scopes: scopes, |
| paddy@125 | 426 ProfileID: profileID, |
| paddy@125 | 427 ClientID: clientID, |
| paddy@69 | 428 } |
| paddy@168 | 429 access, err := token.GenerateAccessToken(context.config.JWTPrivateKey) |
| paddy@168 | 430 if err != nil { |
| paddy@168 | 431 log.Printf("Error signing token: %+v\n", err) |
| paddy@168 | 432 w.WriteHeader(http.StatusInternalServerError) |
| paddy@168 | 433 renderJSONError(enc, "server_error") |
| paddy@168 | 434 return |
| paddy@168 | 435 } |
| paddy@168 | 436 token.AccessToken = access |
| paddy@168 | 437 err = context.SaveToken(token) |
| paddy@69 | 438 if err != nil { |
| paddy@82 | 439 w.WriteHeader(http.StatusInternalServerError) |
| paddy@82 | 440 renderJSONError(enc, "server_error") |
| paddy@81 | 441 return |
| paddy@69 | 442 } |
| paddy@85 | 443 if gt.ReturnToken(w, r, token, context) && gt.Invalidate != nil { |
| paddy@85 | 444 go gt.Invalidate(r, context) |
| paddy@69 | 445 } |
| paddy@69 | 446 } |