auth
auth/oauth2.go
Rename Grant to AuthorizationCode. God bless gofmt. Rename all our instances of Grant to AuthorizationCode (including related variables and types, like grantStore and ErrGrantNotFound, plus all our comments and error strings. Whew.) to better reflect that it is only a single type of grant that could be accepted by the server.
| 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@87 | 22 authCookieName = "auth" |
| paddy@87 | 23 defaultAuthorizationCodeExpiration = 600 // default to ten minute grant expirations |
| paddy@87 | 24 getAuthorizationCodeTemplateName = "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@86 | 114 // RenderJSONToken is an implementation of the ReturnToken function for GrantTypes. It returns the token using JSON |
| paddy@86 | 115 // according to the spec. See RFC 6479, Section 4.1.4. |
| paddy@85 | 116 func RenderJSONToken(w http.ResponseWriter, r *http.Request, token Token, context Context) bool { |
| paddy@85 | 117 enc := json.NewEncoder(w) |
| paddy@85 | 118 resp := tokenResponse{ |
| paddy@85 | 119 AccessToken: token.AccessToken, |
| paddy@85 | 120 RefreshToken: token.RefreshToken, |
| paddy@85 | 121 ExpiresIn: token.ExpiresIn, |
| paddy@85 | 122 TokenType: token.TokenType, |
| paddy@85 | 123 } |
| paddy@85 | 124 err := enc.Encode(resp) |
| paddy@85 | 125 if err != nil { |
| paddy@85 | 126 // TODO(paddy): log this or something |
| paddy@85 | 127 return false |
| paddy@85 | 128 } |
| paddy@85 | 129 return true |
| paddy@85 | 130 } |
| paddy@85 | 131 |
| paddy@69 | 132 func checkCookie(r *http.Request, context Context) (Session, error) { |
| paddy@69 | 133 cookie, err := r.Cookie(authCookieName) |
| paddy@77 | 134 if err == http.ErrNoCookie { |
| paddy@77 | 135 return Session{}, ErrNoSession |
| paddy@77 | 136 } else if err != nil { |
| paddy@77 | 137 log.Println(err) |
| paddy@69 | 138 return Session{}, err |
| paddy@69 | 139 } |
| paddy@69 | 140 sess, err := context.GetSession(cookie.Value) |
| paddy@69 | 141 if err == ErrSessionNotFound { |
| paddy@69 | 142 return Session{}, ErrInvalidSession |
| paddy@69 | 143 } else if err != nil { |
| paddy@69 | 144 return Session{}, err |
| paddy@69 | 145 } |
| paddy@69 | 146 if !sess.Active { |
| paddy@69 | 147 return Session{}, ErrInvalidSession |
| paddy@69 | 148 } |
| paddy@69 | 149 return sess, nil |
| paddy@69 | 150 } |
| paddy@69 | 151 |
| paddy@77 | 152 func buildLoginRedirect(r *http.Request, context Context) string { |
| paddy@77 | 153 if context.loginURI == nil { |
| paddy@77 | 154 return "" |
| paddy@77 | 155 } |
| paddy@77 | 156 uri := *context.loginURI |
| paddy@77 | 157 q := uri.Query() |
| paddy@78 | 158 q.Set("from", r.URL.String()) |
| paddy@77 | 159 uri.RawQuery = q.Encode() |
| paddy@77 | 160 return uri.String() |
| paddy@77 | 161 } |
| paddy@77 | 162 |
| paddy@69 | 163 func authenticate(user, passphrase string, context Context) (Profile, error) { |
| paddy@69 | 164 profile, err := context.GetProfileByLogin(user) |
| paddy@69 | 165 if err != nil { |
| paddy@79 | 166 if err == ErrProfileNotFound || err == ErrLoginNotFound { |
| paddy@69 | 167 return Profile{}, ErrIncorrectAuth |
| paddy@69 | 168 } |
| paddy@69 | 169 return Profile{}, err |
| paddy@69 | 170 } |
| paddy@69 | 171 switch profile.PassphraseScheme { |
| paddy@69 | 172 case 1: |
| paddy@79 | 173 realPass, err := hex.DecodeString(profile.Passphrase) |
| paddy@79 | 174 if err != nil { |
| paddy@79 | 175 return Profile{}, err |
| paddy@79 | 176 } |
| paddy@69 | 177 candidate := pass.Check(sha256.New, profile.Iterations, []byte(passphrase), []byte(profile.Salt)) |
| paddy@79 | 178 if !pass.Compare(candidate, realPass) { |
| paddy@69 | 179 return Profile{}, ErrIncorrectAuth |
| paddy@69 | 180 } |
| paddy@69 | 181 default: |
| paddy@69 | 182 return Profile{}, ErrInvalidPassphraseScheme |
| paddy@69 | 183 } |
| paddy@69 | 184 return profile, nil |
| paddy@69 | 185 } |
| paddy@69 | 186 |
| paddy@77 | 187 func wrap(context Context, f func(w http.ResponseWriter, r *http.Request, context Context)) http.Handler { |
| paddy@77 | 188 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
| paddy@77 | 189 f(w, r, context) |
| paddy@77 | 190 }) |
| paddy@77 | 191 } |
| paddy@77 | 192 |
| paddy@77 | 193 // RegisterOAuth2 adds handlers to the passed router to handle the OAuth2 endpoints. |
| paddy@77 | 194 func RegisterOAuth2(r *mux.Router, context Context) { |
| paddy@87 | 195 r.Handle("/authorize", wrap(context, GetAuthorizationCodeHandler)) |
| paddy@77 | 196 r.Handle("/token", wrap(context, GetTokenHandler)) |
| paddy@77 | 197 } |
| paddy@77 | 198 |
| paddy@87 | 199 // GetAuthorizationCodeHandler presents and processes the page for asking a user to grant access |
| paddy@57 | 200 // to their data. See RFC 6749, Section 4.1. |
| paddy@87 | 201 func GetAuthorizationCodeHandler(w http.ResponseWriter, r *http.Request, context Context) { |
| paddy@69 | 202 session, err := checkCookie(r, context) |
| paddy@69 | 203 if err != nil { |
| paddy@76 | 204 if err == ErrNoSession || err == ErrInvalidSession { |
| paddy@77 | 205 redir := buildLoginRedirect(r, context) |
| paddy@77 | 206 if redir == "" { |
| paddy@77 | 207 log.Println("No login URL configured.") |
| paddy@77 | 208 w.WriteHeader(http.StatusInternalServerError) |
| paddy@87 | 209 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{ |
| paddy@77 | 210 "internal_error": template.HTML("Missing login URL."), |
| paddy@77 | 211 }) |
| paddy@77 | 212 return |
| paddy@77 | 213 } |
| paddy@77 | 214 http.Redirect(w, r, redir, http.StatusFound) |
| paddy@77 | 215 return |
| paddy@69 | 216 } |
| paddy@77 | 217 log.Println(err.Error()) |
| paddy@77 | 218 w.WriteHeader(http.StatusInternalServerError) |
| paddy@87 | 219 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{ |
| paddy@77 | 220 "internal_error": template.HTML(err.Error()), |
| paddy@77 | 221 }) |
| paddy@77 | 222 return |
| paddy@69 | 223 } |
| paddy@56 | 224 if r.URL.Query().Get("client_id") == "" { |
| paddy@56 | 225 w.WriteHeader(http.StatusBadRequest) |
| paddy@87 | 226 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{ |
| paddy@61 | 227 "error": template.HTML("Client ID must be specified in the request."), |
| paddy@56 | 228 }) |
| paddy@56 | 229 return |
| paddy@56 | 230 } |
| paddy@56 | 231 clientID, err := uuid.Parse(r.URL.Query().Get("client_id")) |
| paddy@56 | 232 if err != nil { |
| paddy@56 | 233 w.WriteHeader(http.StatusBadRequest) |
| paddy@87 | 234 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{ |
| paddy@61 | 235 "error": template.HTML("client_id is not a valid Client ID."), |
| paddy@56 | 236 }) |
| paddy@56 | 237 return |
| paddy@56 | 238 } |
| paddy@64 | 239 redirectURI := r.URL.Query().Get("redirect_uri") |
| paddy@64 | 240 redirectURL, err := url.Parse(redirectURI) |
| paddy@64 | 241 if err != nil { |
| paddy@64 | 242 w.WriteHeader(http.StatusBadRequest) |
| paddy@87 | 243 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{ |
| paddy@64 | 244 "error": template.HTML("The redirect_uri specified is not valid."), |
| paddy@64 | 245 }) |
| paddy@64 | 246 return |
| paddy@64 | 247 } |
| paddy@56 | 248 client, err := context.GetClient(clientID) |
| paddy@56 | 249 if err != nil { |
| paddy@59 | 250 if err == ErrClientNotFound { |
| paddy@59 | 251 w.WriteHeader(http.StatusBadRequest) |
| paddy@87 | 252 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{ |
| paddy@61 | 253 "error": template.HTML("The specified Client couldn’t be found."), |
| paddy@59 | 254 }) |
| paddy@59 | 255 } else { |
| paddy@77 | 256 log.Println(err.Error()) |
| paddy@59 | 257 w.WriteHeader(http.StatusInternalServerError) |
| paddy@87 | 258 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{ |
| paddy@61 | 259 "internal_error": template.HTML(err.Error()), |
| paddy@59 | 260 }) |
| paddy@59 | 261 } |
| paddy@56 | 262 return |
| paddy@56 | 263 } |
| paddy@56 | 264 // whether a redirect URI is valid or not depends on the number of endpoints |
| paddy@56 | 265 // the client has registered |
| paddy@56 | 266 numEndpoints, err := context.CountEndpoints(clientID) |
| paddy@56 | 267 if err != nil { |
| paddy@77 | 268 log.Println(err.Error()) |
| paddy@56 | 269 w.WriteHeader(http.StatusInternalServerError) |
| paddy@87 | 270 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{ |
| paddy@61 | 271 "internal_error": template.HTML(err.Error()), |
| paddy@56 | 272 }) |
| paddy@56 | 273 return |
| paddy@56 | 274 } |
| paddy@56 | 275 var validURI bool |
| paddy@58 | 276 if redirectURI != "" { |
| paddy@58 | 277 // BUG(paddy): We really should normalize URIs before trying to compare them. |
| paddy@58 | 278 validURI, err = context.CheckEndpoint(clientID, redirectURI) |
| paddy@56 | 279 if err != nil { |
| paddy@77 | 280 log.Println(err.Error()) |
| paddy@56 | 281 w.WriteHeader(http.StatusInternalServerError) |
| paddy@87 | 282 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{ |
| paddy@61 | 283 "internal_error": template.HTML(err.Error()), |
| paddy@56 | 284 }) |
| paddy@56 | 285 return |
| paddy@56 | 286 } |
| paddy@56 | 287 } else if redirectURI == "" && numEndpoints == 1 { |
| paddy@56 | 288 // if we don't specify the endpoint and there's only one endpoint, the |
| paddy@56 | 289 // request is valid, and we're redirecting to that one endpoint |
| paddy@56 | 290 validURI = true |
| paddy@56 | 291 endpoints, err := context.ListEndpoints(clientID, 1, 0) |
| paddy@56 | 292 if err != nil { |
| paddy@77 | 293 log.Println(err.Error()) |
| paddy@56 | 294 w.WriteHeader(http.StatusInternalServerError) |
| paddy@87 | 295 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{ |
| paddy@61 | 296 "internal_error": template.HTML(err.Error()), |
| paddy@56 | 297 }) |
| paddy@56 | 298 return |
| paddy@56 | 299 } |
| paddy@56 | 300 if len(endpoints) != 1 { |
| paddy@56 | 301 validURI = false |
| paddy@56 | 302 } else { |
| paddy@66 | 303 u := endpoints[0].URI // Copy it here to avoid grabbing a pointer to the memstore |
| paddy@66 | 304 redirectURI = u.String() |
| paddy@66 | 305 redirectURL = &u |
| paddy@56 | 306 } |
| paddy@56 | 307 } else { |
| paddy@56 | 308 validURI = false |
| paddy@56 | 309 } |
| paddy@56 | 310 if !validURI { |
| paddy@56 | 311 w.WriteHeader(http.StatusBadRequest) |
| paddy@87 | 312 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{ |
| paddy@61 | 313 "error": template.HTML("The redirect_uri specified is not valid."), |
| paddy@56 | 314 }) |
| paddy@56 | 315 return |
| paddy@56 | 316 } |
| paddy@60 | 317 scope := r.URL.Query().Get("scope") |
| paddy@60 | 318 state := r.URL.Query().Get("state") |
| paddy@56 | 319 if r.URL.Query().Get("response_type") != "code" { |
| paddy@65 | 320 q := redirectURL.Query() |
| paddy@65 | 321 q.Add("error", "invalid_request") |
| paddy@65 | 322 q.Add("state", state) |
| paddy@65 | 323 redirectURL.RawQuery = q.Encode() |
| paddy@60 | 324 http.Redirect(w, r, redirectURL.String(), http.StatusFound) |
| paddy@60 | 325 return |
| paddy@56 | 326 } |
| paddy@56 | 327 if r.Method == "POST" { |
| paddy@63 | 328 // BUG(paddy): We need to implement CSRF protection when obtaining a grant code. |
| paddy@56 | 329 if r.PostFormValue("grant") == "approved" { |
| paddy@60 | 330 code := uuid.NewID().String() |
| paddy@87 | 331 authCode := AuthorizationCode{ |
| paddy@60 | 332 Code: code, |
| paddy@60 | 333 Created: time.Now(), |
| paddy@87 | 334 ExpiresIn: defaultAuthorizationCodeExpiration, |
| paddy@60 | 335 ClientID: clientID, |
| paddy@60 | 336 Scope: scope, |
| paddy@69 | 337 RedirectURI: r.URL.Query().Get("redirect_uri"), |
| paddy@60 | 338 State: state, |
| paddy@69 | 339 ProfileID: session.ProfileID, |
| paddy@60 | 340 } |
| paddy@87 | 341 err := context.SaveAuthorizationCode(authCode) |
| paddy@60 | 342 if err != nil { |
| paddy@66 | 343 q := redirectURL.Query() |
| paddy@66 | 344 q.Add("error", "server_error") |
| paddy@66 | 345 q.Add("state", state) |
| paddy@66 | 346 redirectURL.RawQuery = q.Encode() |
| paddy@60 | 347 http.Redirect(w, r, redirectURL.String(), http.StatusFound) |
| paddy@60 | 348 return |
| paddy@60 | 349 } |
| paddy@66 | 350 q := redirectURL.Query() |
| paddy@66 | 351 q.Add("code", code) |
| paddy@66 | 352 q.Add("state", state) |
| paddy@66 | 353 redirectURL.RawQuery = q.Encode() |
| paddy@60 | 354 http.Redirect(w, r, redirectURL.String(), http.StatusFound) |
| paddy@60 | 355 return |
| paddy@56 | 356 } |
| paddy@66 | 357 q := redirectURL.Query() |
| paddy@66 | 358 q.Add("error", "access_denied") |
| paddy@66 | 359 q.Add("state", state) |
| paddy@66 | 360 redirectURL.RawQuery = q.Encode() |
| paddy@60 | 361 http.Redirect(w, r, redirectURL.String(), http.StatusFound) |
| paddy@60 | 362 return |
| paddy@56 | 363 } |
| paddy@85 | 364 profile, err := context.GetProfileByID(session.ProfileID) |
| paddy@85 | 365 if err != nil { |
| paddy@85 | 366 q := redirectURL.Query() |
| paddy@85 | 367 q.Add("error", "server_error") |
| paddy@85 | 368 q.Add("state", state) |
| paddy@85 | 369 redirectURL.RawQuery = q.Encode() |
| paddy@85 | 370 http.Redirect(w, r, redirectURL.String(), http.StatusFound) |
| paddy@85 | 371 return |
| paddy@85 | 372 } |
| paddy@51 | 373 w.WriteHeader(http.StatusOK) |
| paddy@87 | 374 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{ |
| paddy@85 | 375 "client": client, |
| paddy@85 | 376 "redirectURL": redirectURL, |
| paddy@85 | 377 "scope": scope, |
| paddy@85 | 378 "profile": profile, |
| paddy@56 | 379 }) |
| paddy@51 | 380 } |
| paddy@68 | 381 |
| paddy@69 | 382 // GetTokenHandler allows a client to exchange an authorization grant for an |
| paddy@69 | 383 // access token. See RFC 6749 Section 4.1.3. |
| paddy@69 | 384 func GetTokenHandler(w http.ResponseWriter, r *http.Request, context Context) { |
| paddy@69 | 385 enc := json.NewEncoder(w) |
| paddy@69 | 386 grantType := r.PostFormValue("grant_type") |
| paddy@84 | 387 gt, ok := findGrantType(grantType) |
| paddy@84 | 388 if !ok { |
| paddy@82 | 389 w.WriteHeader(http.StatusBadRequest) |
| paddy@82 | 390 renderJSONError(enc, "invalid_request") |
| paddy@69 | 391 return |
| paddy@69 | 392 } |
| paddy@84 | 393 scope, profileID, valid := gt.Validate(w, r, context) |
| paddy@84 | 394 if !valid { |
| paddy@69 | 395 return |
| paddy@69 | 396 } |
| paddy@84 | 397 refresh := "" |
| paddy@84 | 398 if gt.IssuesRefresh { |
| paddy@84 | 399 refresh = uuid.NewID().String() |
| paddy@69 | 400 } |
| paddy@69 | 401 token := Token{ |
| paddy@69 | 402 AccessToken: uuid.NewID().String(), |
| paddy@84 | 403 RefreshToken: refresh, |
| paddy@69 | 404 Created: time.Now(), |
| paddy@69 | 405 ExpiresIn: defaultTokenExpiration, |
| paddy@81 | 406 TokenType: "bearer", |
| paddy@84 | 407 Scope: scope, |
| paddy@84 | 408 ProfileID: profileID, |
| paddy@69 | 409 } |
| paddy@84 | 410 err := context.SaveToken(token) |
| paddy@69 | 411 if err != nil { |
| paddy@82 | 412 w.WriteHeader(http.StatusInternalServerError) |
| paddy@82 | 413 renderJSONError(enc, "server_error") |
| paddy@81 | 414 return |
| paddy@69 | 415 } |
| paddy@85 | 416 if gt.ReturnToken(w, r, token, context) && gt.Invalidate != nil { |
| paddy@85 | 417 go gt.Invalidate(r, context) |
| paddy@69 | 418 } |
| paddy@69 | 419 } |
| paddy@69 | 420 |
| paddy@68 | 421 // TODO(paddy): exchange user credentials for access token |
| paddy@68 | 422 // TODO(paddy): exchange client credentials for access token |
| paddy@68 | 423 // TODO(paddy): implicit grant for access token |
| paddy@68 | 424 // TODO(paddy): exchange refresh token for access token |