auth
auth/oauth2.go
Break client verification out, break token returns out. Break client verification out into a helper function to avoid rewriting it for pretty much every grant. Break token returns out into a new function as part of the GrantType, so that implicit grants can redirect with the token value. Split returning the token as JSON into its own exported function, which can be used in multiple grants. Return more relevant information to the template when a user is deciding whether or not to authorize a grant.
| paddy@51 | 1 package auth |
| paddy@51 | 2 |
| paddy@51 | 3 import ( |
| paddy@82 | 4 "crypto/sha256" |
| paddy@79 | 5 "encoding/hex" |
| paddy@69 | 6 "encoding/json" |
| paddy@69 | 7 "errors" |
| paddy@61 | 8 "html/template" |
| paddy@77 | 9 "log" |
| paddy@51 | 10 "net/http" |
| paddy@60 | 11 "net/url" |
| paddy@84 | 12 "sync" |
| paddy@60 | 13 "time" |
| paddy@56 | 14 |
| paddy@69 | 15 "code.secondbit.org/pass" |
| paddy@56 | 16 "code.secondbit.org/uuid" |
| paddy@82 | 17 |
| paddy@82 | 18 "github.com/gorilla/mux" |
| paddy@51 | 19 ) |
| paddy@51 | 20 |
| paddy@60 | 21 const ( |
| paddy@69 | 22 authCookieName = "auth" |
| paddy@69 | 23 defaultGrantExpiration = 600 // default to ten minute grant expirations |
| paddy@60 | 24 getGrantTemplateName = "get_grant" |
| paddy@60 | 25 ) |
| paddy@51 | 26 |
| paddy@69 | 27 var ( |
| paddy@69 | 28 // ErrNoAuth is returned when an Authorization header is not present or is empty. |
| paddy@69 | 29 ErrNoAuth = errors.New("no authorization header supplied") |
| paddy@69 | 30 // ErrInvalidAuthFormat is returned when an Authorization header is present but not the correct format. |
| paddy@69 | 31 ErrInvalidAuthFormat = errors.New("authorization header is not in a valid format") |
| paddy@69 | 32 // ErrIncorrectAuth is returned when a user authentication attempt does not match the stored values. |
| paddy@69 | 33 ErrIncorrectAuth = errors.New("invalid authentication") |
| paddy@69 | 34 // ErrInvalidPassphraseScheme is returned when an undefined passphrase scheme is used. |
| paddy@69 | 35 ErrInvalidPassphraseScheme = errors.New("invalid passphrase scheme") |
| paddy@69 | 36 // ErrNoSession is returned when no session ID is passed with a request. |
| paddy@69 | 37 ErrNoSession = errors.New("no session ID found") |
| paddy@84 | 38 |
| paddy@84 | 39 grantTypesMap = grantTypes{types: map[string]GrantType{}} |
| paddy@69 | 40 ) |
| paddy@69 | 41 |
| paddy@84 | 42 type grantTypes struct { |
| paddy@84 | 43 types map[string]GrantType |
| paddy@84 | 44 sync.RWMutex |
| paddy@84 | 45 } |
| paddy@84 | 46 |
| paddy@84 | 47 // GrantType defines a set of functions and metadata around a specific authorization grant strategy. |
| paddy@84 | 48 // |
| paddy@84 | 49 // The Validate function will be called when requests are made that match the GrantType, and should write any |
| paddy@84 | 50 // errors to the ResponseWriter. It is responsible for determining if the grant is valid and a token should be issued. |
| paddy@84 | 51 // 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 | 52 // is valid or not. It must not be nil. |
| paddy@84 | 53 // |
| paddy@84 | 54 // The Invalidate function will be called when the grant has successfully generated a token and the token has successfully |
| paddy@84 | 55 // been conveyed to the user. The Invalidate function is always called asynchronously, outside the request. It should take |
| paddy@84 | 56 // care of marking the grant as used, if the GrantType requires grants to be one-time only grants. The Invalidate function |
| paddy@84 | 57 // can be nil. |
| paddy@84 | 58 // |
| paddy@84 | 59 // IssuesRefresh determines whether the GrantType should yield a refresh token as well as an access token. If true, the client |
| paddy@84 | 60 // will be issued a refresh token. |
| paddy@85 | 61 // |
| paddy@85 | 62 // 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 | 63 // was successfully returned and the Invalidate function will be called asynchronously. |
| paddy@84 | 64 type GrantType struct { |
| paddy@84 | 65 Validate func(w http.ResponseWriter, r *http.Request, context Context) (scope string, profileID uuid.ID, valid bool) |
| paddy@84 | 66 Invalidate func(r *http.Request, context Context) bool |
| paddy@85 | 67 ReturnToken func(w http.ResponseWriter, r *http.Request, token Token, context Context) bool |
| paddy@84 | 68 IssuesRefresh bool |
| paddy@84 | 69 } |
| paddy@84 | 70 |
| paddy@69 | 71 type tokenResponse struct { |
| paddy@69 | 72 AccessToken string `json:"access_token"` |
| paddy@69 | 73 TokenType string `json:"token_type,omitempty"` |
| paddy@69 | 74 ExpiresIn int32 `json:"expires_in,omitempty"` |
| paddy@69 | 75 RefreshToken string `json:"refresh_token,omitempty"` |
| paddy@69 | 76 } |
| paddy@69 | 77 |
| paddy@82 | 78 type errorResponse struct { |
| paddy@82 | 79 Error string `json:"error"` |
| paddy@82 | 80 Description string `json:"error_description,omitempty"` |
| paddy@82 | 81 URI string `json:"error_uri,omitempty"` |
| paddy@82 | 82 } |
| paddy@82 | 83 |
| paddy@84 | 84 // RegisterGrantType associates a string with a GrantType. When the string is used as the value for "grant_type" when obtaining |
| paddy@84 | 85 // an access token, the associated GrantType's properties will be used. |
| paddy@84 | 86 // |
| paddy@84 | 87 // RegisterGrantType should be called in the `init()` function of packages, much like database/sql registers drivers. It will panic |
| paddy@84 | 88 // if a GrantType tries to register under a string that already has a GrantType registered for it. |
| paddy@84 | 89 func RegisterGrantType(name string, g GrantType) { |
| paddy@84 | 90 grantTypesMap.Lock() |
| paddy@84 | 91 defer grantTypesMap.Unlock() |
| paddy@84 | 92 if _, ok := grantTypesMap.types[name]; ok { |
| paddy@84 | 93 panic("Duplicate registration of grant_type " + name) |
| paddy@84 | 94 } |
| paddy@84 | 95 grantTypesMap.types[name] = g |
| paddy@84 | 96 } |
| paddy@84 | 97 |
| paddy@84 | 98 func findGrantType(name string) (GrantType, bool) { |
| paddy@84 | 99 grantTypesMap.RLock() |
| paddy@84 | 100 defer grantTypesMap.RUnlock() |
| paddy@84 | 101 t, ok := grantTypesMap.types[name] |
| paddy@84 | 102 return t, ok |
| paddy@84 | 103 } |
| paddy@84 | 104 |
| paddy@82 | 105 func renderJSONError(enc *json.Encoder, errorType string) { |
| paddy@82 | 106 err := enc.Encode(errorResponse{ |
| paddy@82 | 107 Error: errorType, |
| paddy@82 | 108 }) |
| paddy@82 | 109 if err != nil { |
| paddy@82 | 110 // TODO(paddy): log this or something |
| paddy@69 | 111 } |
| paddy@69 | 112 } |
| paddy@69 | 113 |
| paddy@85 | 114 func RenderJSONToken(w http.ResponseWriter, r *http.Request, token Token, context Context) bool { |
| paddy@85 | 115 enc := json.NewEncoder(w) |
| paddy@85 | 116 resp := tokenResponse{ |
| paddy@85 | 117 AccessToken: token.AccessToken, |
| paddy@85 | 118 RefreshToken: token.RefreshToken, |
| paddy@85 | 119 ExpiresIn: token.ExpiresIn, |
| paddy@85 | 120 TokenType: token.TokenType, |
| paddy@85 | 121 } |
| paddy@85 | 122 err := enc.Encode(resp) |
| paddy@85 | 123 if err != nil { |
| paddy@85 | 124 // TODO(paddy): log this or something |
| paddy@85 | 125 return false |
| paddy@85 | 126 } |
| paddy@85 | 127 return true |
| paddy@85 | 128 } |
| paddy@85 | 129 |
| paddy@69 | 130 func checkCookie(r *http.Request, context Context) (Session, error) { |
| paddy@69 | 131 cookie, err := r.Cookie(authCookieName) |
| paddy@77 | 132 if err == http.ErrNoCookie { |
| paddy@77 | 133 return Session{}, ErrNoSession |
| paddy@77 | 134 } else if err != nil { |
| paddy@77 | 135 log.Println(err) |
| paddy@69 | 136 return Session{}, err |
| paddy@69 | 137 } |
| paddy@69 | 138 sess, err := context.GetSession(cookie.Value) |
| paddy@69 | 139 if err == ErrSessionNotFound { |
| paddy@69 | 140 return Session{}, ErrInvalidSession |
| paddy@69 | 141 } else if err != nil { |
| paddy@69 | 142 return Session{}, err |
| paddy@69 | 143 } |
| paddy@69 | 144 if !sess.Active { |
| paddy@69 | 145 return Session{}, ErrInvalidSession |
| paddy@69 | 146 } |
| paddy@69 | 147 return sess, nil |
| paddy@69 | 148 } |
| paddy@69 | 149 |
| paddy@77 | 150 func buildLoginRedirect(r *http.Request, context Context) string { |
| paddy@77 | 151 if context.loginURI == nil { |
| paddy@77 | 152 return "" |
| paddy@77 | 153 } |
| paddy@77 | 154 uri := *context.loginURI |
| paddy@77 | 155 q := uri.Query() |
| paddy@78 | 156 q.Set("from", r.URL.String()) |
| paddy@77 | 157 uri.RawQuery = q.Encode() |
| paddy@77 | 158 return uri.String() |
| paddy@77 | 159 } |
| paddy@77 | 160 |
| paddy@69 | 161 func authenticate(user, passphrase string, context Context) (Profile, error) { |
| paddy@69 | 162 profile, err := context.GetProfileByLogin(user) |
| paddy@69 | 163 if err != nil { |
| paddy@79 | 164 if err == ErrProfileNotFound || err == ErrLoginNotFound { |
| paddy@69 | 165 return Profile{}, ErrIncorrectAuth |
| paddy@69 | 166 } |
| paddy@69 | 167 return Profile{}, err |
| paddy@69 | 168 } |
| paddy@69 | 169 switch profile.PassphraseScheme { |
| paddy@69 | 170 case 1: |
| paddy@79 | 171 realPass, err := hex.DecodeString(profile.Passphrase) |
| paddy@79 | 172 if err != nil { |
| paddy@79 | 173 return Profile{}, err |
| paddy@79 | 174 } |
| paddy@69 | 175 candidate := pass.Check(sha256.New, profile.Iterations, []byte(passphrase), []byte(profile.Salt)) |
| paddy@79 | 176 if !pass.Compare(candidate, realPass) { |
| paddy@69 | 177 return Profile{}, ErrIncorrectAuth |
| paddy@69 | 178 } |
| paddy@69 | 179 default: |
| paddy@69 | 180 return Profile{}, ErrInvalidPassphraseScheme |
| paddy@69 | 181 } |
| paddy@69 | 182 return profile, nil |
| paddy@69 | 183 } |
| paddy@69 | 184 |
| paddy@77 | 185 func wrap(context Context, f func(w http.ResponseWriter, r *http.Request, context Context)) http.Handler { |
| paddy@77 | 186 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
| paddy@77 | 187 f(w, r, context) |
| paddy@77 | 188 }) |
| paddy@77 | 189 } |
| paddy@77 | 190 |
| paddy@77 | 191 // RegisterOAuth2 adds handlers to the passed router to handle the OAuth2 endpoints. |
| paddy@77 | 192 func RegisterOAuth2(r *mux.Router, context Context) { |
| paddy@77 | 193 r.Handle("/authorize", wrap(context, GetGrantHandler)) |
| paddy@77 | 194 r.Handle("/token", wrap(context, GetTokenHandler)) |
| paddy@77 | 195 } |
| paddy@77 | 196 |
| paddy@57 | 197 // GetGrantHandler presents and processes the page for asking a user to grant access |
| paddy@57 | 198 // to their data. See RFC 6749, Section 4.1. |
| paddy@51 | 199 func GetGrantHandler(w http.ResponseWriter, r *http.Request, context Context) { |
| paddy@69 | 200 session, err := checkCookie(r, context) |
| paddy@69 | 201 if err != nil { |
| paddy@76 | 202 if err == ErrNoSession || err == ErrInvalidSession { |
| paddy@77 | 203 redir := buildLoginRedirect(r, context) |
| paddy@77 | 204 if redir == "" { |
| paddy@77 | 205 log.Println("No login URL configured.") |
| paddy@77 | 206 w.WriteHeader(http.StatusInternalServerError) |
| paddy@77 | 207 context.Render(w, getGrantTemplateName, map[string]interface{}{ |
| paddy@77 | 208 "internal_error": template.HTML("Missing login URL."), |
| paddy@77 | 209 }) |
| paddy@77 | 210 return |
| paddy@77 | 211 } |
| paddy@77 | 212 http.Redirect(w, r, redir, http.StatusFound) |
| paddy@77 | 213 return |
| paddy@69 | 214 } |
| paddy@77 | 215 log.Println(err.Error()) |
| paddy@77 | 216 w.WriteHeader(http.StatusInternalServerError) |
| paddy@77 | 217 context.Render(w, getGrantTemplateName, map[string]interface{}{ |
| paddy@77 | 218 "internal_error": template.HTML(err.Error()), |
| paddy@77 | 219 }) |
| paddy@77 | 220 return |
| paddy@69 | 221 } |
| paddy@56 | 222 if r.URL.Query().Get("client_id") == "" { |
| paddy@56 | 223 w.WriteHeader(http.StatusBadRequest) |
| paddy@56 | 224 context.Render(w, getGrantTemplateName, map[string]interface{}{ |
| paddy@61 | 225 "error": template.HTML("Client ID must be specified in the request."), |
| paddy@56 | 226 }) |
| paddy@56 | 227 return |
| paddy@56 | 228 } |
| paddy@56 | 229 clientID, err := uuid.Parse(r.URL.Query().Get("client_id")) |
| paddy@56 | 230 if err != nil { |
| paddy@56 | 231 w.WriteHeader(http.StatusBadRequest) |
| paddy@56 | 232 context.Render(w, getGrantTemplateName, map[string]interface{}{ |
| paddy@61 | 233 "error": template.HTML("client_id is not a valid Client ID."), |
| paddy@56 | 234 }) |
| paddy@56 | 235 return |
| paddy@56 | 236 } |
| paddy@64 | 237 redirectURI := r.URL.Query().Get("redirect_uri") |
| paddy@64 | 238 redirectURL, err := url.Parse(redirectURI) |
| paddy@64 | 239 if err != nil { |
| paddy@64 | 240 w.WriteHeader(http.StatusBadRequest) |
| paddy@64 | 241 context.Render(w, getGrantTemplateName, map[string]interface{}{ |
| paddy@64 | 242 "error": template.HTML("The redirect_uri specified is not valid."), |
| paddy@64 | 243 }) |
| paddy@64 | 244 return |
| paddy@64 | 245 } |
| paddy@56 | 246 client, err := context.GetClient(clientID) |
| paddy@56 | 247 if err != nil { |
| paddy@59 | 248 if err == ErrClientNotFound { |
| paddy@59 | 249 w.WriteHeader(http.StatusBadRequest) |
| paddy@59 | 250 context.Render(w, getGrantTemplateName, map[string]interface{}{ |
| paddy@61 | 251 "error": template.HTML("The specified Client couldn’t be found."), |
| paddy@59 | 252 }) |
| paddy@59 | 253 } else { |
| paddy@77 | 254 log.Println(err.Error()) |
| paddy@59 | 255 w.WriteHeader(http.StatusInternalServerError) |
| paddy@59 | 256 context.Render(w, getGrantTemplateName, map[string]interface{}{ |
| paddy@61 | 257 "internal_error": template.HTML(err.Error()), |
| paddy@59 | 258 }) |
| paddy@59 | 259 } |
| paddy@56 | 260 return |
| paddy@56 | 261 } |
| paddy@56 | 262 // whether a redirect URI is valid or not depends on the number of endpoints |
| paddy@56 | 263 // the client has registered |
| paddy@56 | 264 numEndpoints, err := context.CountEndpoints(clientID) |
| paddy@56 | 265 if err != nil { |
| paddy@77 | 266 log.Println(err.Error()) |
| paddy@56 | 267 w.WriteHeader(http.StatusInternalServerError) |
| paddy@56 | 268 context.Render(w, getGrantTemplateName, map[string]interface{}{ |
| paddy@61 | 269 "internal_error": template.HTML(err.Error()), |
| paddy@56 | 270 }) |
| paddy@56 | 271 return |
| paddy@56 | 272 } |
| paddy@56 | 273 var validURI bool |
| paddy@58 | 274 if redirectURI != "" { |
| paddy@58 | 275 // BUG(paddy): We really should normalize URIs before trying to compare them. |
| paddy@58 | 276 validURI, err = context.CheckEndpoint(clientID, redirectURI) |
| paddy@56 | 277 if err != nil { |
| paddy@77 | 278 log.Println(err.Error()) |
| paddy@56 | 279 w.WriteHeader(http.StatusInternalServerError) |
| paddy@56 | 280 context.Render(w, getGrantTemplateName, map[string]interface{}{ |
| paddy@61 | 281 "internal_error": template.HTML(err.Error()), |
| paddy@56 | 282 }) |
| paddy@56 | 283 return |
| paddy@56 | 284 } |
| paddy@56 | 285 } else if redirectURI == "" && numEndpoints == 1 { |
| paddy@56 | 286 // if we don't specify the endpoint and there's only one endpoint, the |
| paddy@56 | 287 // request is valid, and we're redirecting to that one endpoint |
| paddy@56 | 288 validURI = true |
| paddy@56 | 289 endpoints, err := context.ListEndpoints(clientID, 1, 0) |
| paddy@56 | 290 if err != nil { |
| paddy@77 | 291 log.Println(err.Error()) |
| paddy@56 | 292 w.WriteHeader(http.StatusInternalServerError) |
| paddy@56 | 293 context.Render(w, getGrantTemplateName, map[string]interface{}{ |
| paddy@61 | 294 "internal_error": template.HTML(err.Error()), |
| paddy@56 | 295 }) |
| paddy@56 | 296 return |
| paddy@56 | 297 } |
| paddy@56 | 298 if len(endpoints) != 1 { |
| paddy@56 | 299 validURI = false |
| paddy@56 | 300 } else { |
| paddy@66 | 301 u := endpoints[0].URI // Copy it here to avoid grabbing a pointer to the memstore |
| paddy@66 | 302 redirectURI = u.String() |
| paddy@66 | 303 redirectURL = &u |
| paddy@56 | 304 } |
| paddy@56 | 305 } else { |
| paddy@56 | 306 validURI = false |
| paddy@56 | 307 } |
| paddy@56 | 308 if !validURI { |
| paddy@56 | 309 w.WriteHeader(http.StatusBadRequest) |
| paddy@56 | 310 context.Render(w, getGrantTemplateName, map[string]interface{}{ |
| paddy@61 | 311 "error": template.HTML("The redirect_uri specified is not valid."), |
| paddy@56 | 312 }) |
| paddy@56 | 313 return |
| paddy@56 | 314 } |
| paddy@60 | 315 scope := r.URL.Query().Get("scope") |
| paddy@60 | 316 state := r.URL.Query().Get("state") |
| paddy@56 | 317 if r.URL.Query().Get("response_type") != "code" { |
| paddy@65 | 318 q := redirectURL.Query() |
| paddy@65 | 319 q.Add("error", "invalid_request") |
| paddy@65 | 320 q.Add("state", state) |
| paddy@65 | 321 redirectURL.RawQuery = q.Encode() |
| paddy@60 | 322 http.Redirect(w, r, redirectURL.String(), http.StatusFound) |
| paddy@60 | 323 return |
| paddy@56 | 324 } |
| paddy@56 | 325 if r.Method == "POST" { |
| paddy@63 | 326 // BUG(paddy): We need to implement CSRF protection when obtaining a grant code. |
| paddy@56 | 327 if r.PostFormValue("grant") == "approved" { |
| paddy@60 | 328 code := uuid.NewID().String() |
| paddy@60 | 329 grant := Grant{ |
| paddy@60 | 330 Code: code, |
| paddy@60 | 331 Created: time.Now(), |
| paddy@60 | 332 ExpiresIn: defaultGrantExpiration, |
| paddy@60 | 333 ClientID: clientID, |
| paddy@60 | 334 Scope: scope, |
| paddy@69 | 335 RedirectURI: r.URL.Query().Get("redirect_uri"), |
| paddy@60 | 336 State: state, |
| paddy@69 | 337 ProfileID: session.ProfileID, |
| paddy@60 | 338 } |
| paddy@60 | 339 err := context.SaveGrant(grant) |
| paddy@60 | 340 if err != nil { |
| paddy@66 | 341 q := redirectURL.Query() |
| paddy@66 | 342 q.Add("error", "server_error") |
| paddy@66 | 343 q.Add("state", state) |
| paddy@66 | 344 redirectURL.RawQuery = q.Encode() |
| paddy@60 | 345 http.Redirect(w, r, redirectURL.String(), http.StatusFound) |
| paddy@60 | 346 return |
| paddy@60 | 347 } |
| paddy@66 | 348 q := redirectURL.Query() |
| paddy@66 | 349 q.Add("code", code) |
| paddy@66 | 350 q.Add("state", state) |
| paddy@66 | 351 redirectURL.RawQuery = q.Encode() |
| paddy@60 | 352 http.Redirect(w, r, redirectURL.String(), http.StatusFound) |
| paddy@60 | 353 return |
| paddy@56 | 354 } |
| paddy@66 | 355 q := redirectURL.Query() |
| paddy@66 | 356 q.Add("error", "access_denied") |
| paddy@66 | 357 q.Add("state", state) |
| 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@85 | 364 q := redirectURL.Query() |
| paddy@85 | 365 q.Add("error", "server_error") |
| paddy@85 | 366 q.Add("state", state) |
| paddy@85 | 367 redirectURL.RawQuery = q.Encode() |
| paddy@85 | 368 http.Redirect(w, r, redirectURL.String(), http.StatusFound) |
| paddy@85 | 369 return |
| paddy@85 | 370 } |
| paddy@51 | 371 w.WriteHeader(http.StatusOK) |
| paddy@56 | 372 context.Render(w, getGrantTemplateName, map[string]interface{}{ |
| paddy@85 | 373 "client": client, |
| paddy@85 | 374 "redirectURL": redirectURL, |
| paddy@85 | 375 "scope": scope, |
| paddy@85 | 376 "profile": profile, |
| 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@84 | 391 scope, profileID, valid := gt.Validate(w, r, context) |
| paddy@84 | 392 if !valid { |
| paddy@69 | 393 return |
| paddy@69 | 394 } |
| paddy@84 | 395 refresh := "" |
| paddy@84 | 396 if gt.IssuesRefresh { |
| paddy@84 | 397 refresh = uuid.NewID().String() |
| paddy@69 | 398 } |
| paddy@69 | 399 token := Token{ |
| paddy@69 | 400 AccessToken: uuid.NewID().String(), |
| paddy@84 | 401 RefreshToken: refresh, |
| paddy@69 | 402 Created: time.Now(), |
| paddy@69 | 403 ExpiresIn: defaultTokenExpiration, |
| paddy@81 | 404 TokenType: "bearer", |
| paddy@84 | 405 Scope: scope, |
| paddy@84 | 406 ProfileID: profileID, |
| paddy@69 | 407 } |
| paddy@84 | 408 err := context.SaveToken(token) |
| paddy@69 | 409 if err != nil { |
| paddy@82 | 410 w.WriteHeader(http.StatusInternalServerError) |
| paddy@82 | 411 renderJSONError(enc, "server_error") |
| paddy@81 | 412 return |
| paddy@69 | 413 } |
| paddy@85 | 414 if gt.ReturnToken(w, r, token, context) && gt.Invalidate != nil { |
| paddy@85 | 415 go gt.Invalidate(r, context) |
| paddy@69 | 416 } |
| paddy@69 | 417 } |
| paddy@69 | 418 |
| paddy@68 | 419 // TODO(paddy): exchange user credentials for access token |
| paddy@68 | 420 // TODO(paddy): exchange client credentials for access token |
| paddy@68 | 421 // TODO(paddy): implicit grant for access token |
| paddy@68 | 422 // TODO(paddy): exchange refresh token for access token |