auth
auth/oauth2.go
Attach our Scope type to AuthCodes and Tokens. When obtaining an AuthorizationCode or Token, attach a slice of strings, each one a Scope ID, instead of just attaching the encoded string the user passes in. This will allow us to change our Scope encoding down the line, and is more conceptually faithful. Also, if an authorization request is made with an invalid scope, return the invalid_scope error.
| 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 } |