auth
auth/oauth2.go
Support email verification. The bulk of this commit is auto-modifying files to export variables (mostly our request error types and our response type) so that they can be reused in a Go client for that API. We also implement the beginnings of a Go client for that API, implementing the bare minimum we need for our immediate purposes: the ability to retrieve information about a Login. This, of course, means we need an API endpoint that will return information about a Login, which in turn required us to implement a GetLogin method in our profileStore. Which got in-memory and postgres implementations. That done, we could add the Verification field and Verified field to the Login type, to keep track of whether we've verified the user's ownership of those communication methods (if the Login is, in fact, a communication method). This required us to update sql/postgres_init.sql to account for the new fields we're tracking. It also means that when creating a Login, we had to generate a UUID to use as the Verification field. To make things complete, we needed a verifyLogin method on the profileStore to mark a Login as verified. That, in turn, required an endpoint to control this through the API. While doing so, I lumped things together in an UpdateLogin handler just so we could reuse the endpoint and logic when resending a verification email that may have never reached the user, for whatever reason (the quintessential "send again" button). Finally, we implemented an email_verification listener that will pull email_verification events off NSQ, check for the requisite data integrity, and use mailgun to email out a verification/welcome email.
| 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@107 | 18 "code.secondbit.org/uuid.hg" |
| paddy@82 | 19 "github.com/gorilla/mux" |
| paddy@51 | 20 ) |
| paddy@51 | 21 |
| paddy@60 | 22 const ( |
| 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@123 | 62 // AllowsPublic determines whether the GrantType should allow public clients to use that grant. If true, clients without |
| paddy@123 | 63 // credentials will be able to use the grant to obtain a token. |
| paddy@123 | 64 // |
| paddy@124 | 65 // AuditString should return the string that will be saved in the resulting Token's CreatedFrom field, as an audit log of how |
| paddy@124 | 66 // the Token was authorized. |
| paddy@124 | 67 // |
| paddy@85 | 68 // 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 | 69 // was successfully returned and the Invalidate function will be called asynchronously. |
| paddy@84 | 70 type GrantType struct { |
| paddy@163 | 71 Validate func(w http.ResponseWriter, r *http.Request, context Context) (scopes Scopes, profileID uuid.ID, valid bool) |
| paddy@90 | 72 Invalidate func(r *http.Request, context Context) error |
| paddy@85 | 73 ReturnToken func(w http.ResponseWriter, r *http.Request, token Token, context Context) bool |
| paddy@124 | 74 AuditString func(r *http.Request) string |
| paddy@84 | 75 IssuesRefresh bool |
| paddy@123 | 76 AllowsPublic bool |
| paddy@84 | 77 } |
| paddy@84 | 78 |
| paddy@69 | 79 type tokenResponse struct { |
| paddy@69 | 80 AccessToken string `json:"access_token"` |
| paddy@69 | 81 TokenType string `json:"token_type,omitempty"` |
| paddy@69 | 82 ExpiresIn int32 `json:"expires_in,omitempty"` |
| paddy@69 | 83 RefreshToken string `json:"refresh_token,omitempty"` |
| paddy@69 | 84 } |
| paddy@69 | 85 |
| paddy@82 | 86 type errorResponse struct { |
| paddy@82 | 87 Error string `json:"error"` |
| paddy@82 | 88 Description string `json:"error_description,omitempty"` |
| paddy@82 | 89 URI string `json:"error_uri,omitempty"` |
| paddy@82 | 90 } |
| paddy@82 | 91 |
| paddy@84 | 92 // RegisterGrantType associates a string with a GrantType. When the string is used as the value for "grant_type" when obtaining |
| paddy@84 | 93 // an access token, the associated GrantType's properties will be used. |
| paddy@84 | 94 // |
| paddy@84 | 95 // RegisterGrantType should be called in the `init()` function of packages, much like database/sql registers drivers. It will panic |
| paddy@84 | 96 // if a GrantType tries to register under a string that already has a GrantType registered for it. |
| paddy@84 | 97 func RegisterGrantType(name string, g GrantType) { |
| paddy@84 | 98 grantTypesMap.Lock() |
| paddy@84 | 99 defer grantTypesMap.Unlock() |
| paddy@84 | 100 if _, ok := grantTypesMap.types[name]; ok { |
| paddy@84 | 101 panic("Duplicate registration of grant_type " + name) |
| paddy@84 | 102 } |
| paddy@84 | 103 grantTypesMap.types[name] = g |
| paddy@84 | 104 } |
| paddy@84 | 105 |
| paddy@84 | 106 func findGrantType(name string) (GrantType, bool) { |
| paddy@84 | 107 grantTypesMap.RLock() |
| paddy@84 | 108 defer grantTypesMap.RUnlock() |
| paddy@84 | 109 t, ok := grantTypesMap.types[name] |
| paddy@84 | 110 return t, ok |
| paddy@84 | 111 } |
| paddy@84 | 112 |
| paddy@82 | 113 func renderJSONError(enc *json.Encoder, errorType string) { |
| paddy@82 | 114 err := enc.Encode(errorResponse{ |
| paddy@82 | 115 Error: errorType, |
| paddy@82 | 116 }) |
| paddy@82 | 117 if err != nil { |
| paddy@90 | 118 log.Println(err) |
| paddy@69 | 119 } |
| paddy@69 | 120 } |
| paddy@69 | 121 |
| paddy@86 | 122 // RenderJSONToken is an implementation of the ReturnToken function for GrantTypes. It returns the token using JSON |
| paddy@86 | 123 // according to the spec. See RFC 6479, Section 4.1.4. |
| paddy@85 | 124 func RenderJSONToken(w http.ResponseWriter, r *http.Request, token Token, context Context) bool { |
| paddy@85 | 125 enc := json.NewEncoder(w) |
| paddy@85 | 126 resp := tokenResponse{ |
| paddy@85 | 127 AccessToken: token.AccessToken, |
| paddy@85 | 128 RefreshToken: token.RefreshToken, |
| paddy@85 | 129 ExpiresIn: token.ExpiresIn, |
| paddy@85 | 130 TokenType: token.TokenType, |
| paddy@85 | 131 } |
| paddy@109 | 132 w.Header().Set("Content-Type", "application/json") |
| paddy@85 | 133 err := enc.Encode(resp) |
| paddy@85 | 134 if err != nil { |
| paddy@90 | 135 log.Println(err) |
| paddy@85 | 136 return false |
| paddy@85 | 137 } |
| paddy@85 | 138 return true |
| paddy@85 | 139 } |
| paddy@85 | 140 |
| paddy@77 | 141 // RegisterOAuth2 adds handlers to the passed router to handle the OAuth2 endpoints. |
| paddy@77 | 142 func RegisterOAuth2(r *mux.Router, context Context) { |
| paddy@87 | 143 r.Handle("/authorize", wrap(context, GetAuthorizationCodeHandler)) |
| paddy@77 | 144 r.Handle("/token", wrap(context, GetTokenHandler)) |
| paddy@77 | 145 } |
| paddy@77 | 146 |
| paddy@87 | 147 // GetAuthorizationCodeHandler presents and processes the page for asking a user to grant access |
| paddy@57 | 148 // to their data. See RFC 6749, Section 4.1. |
| paddy@87 | 149 func GetAuthorizationCodeHandler(w http.ResponseWriter, r *http.Request, context Context) { |
| paddy@69 | 150 session, err := checkCookie(r, context) |
| paddy@69 | 151 if err != nil { |
| paddy@76 | 152 if err == ErrNoSession || err == ErrInvalidSession { |
| paddy@77 | 153 redir := buildLoginRedirect(r, context) |
| paddy@77 | 154 if redir == "" { |
| paddy@77 | 155 log.Println("No login URL configured.") |
| paddy@77 | 156 w.WriteHeader(http.StatusInternalServerError) |
| paddy@87 | 157 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{ |
| paddy@77 | 158 "internal_error": template.HTML("Missing login URL."), |
| paddy@77 | 159 }) |
| paddy@77 | 160 return |
| paddy@77 | 161 } |
| paddy@77 | 162 http.Redirect(w, r, redir, http.StatusFound) |
| paddy@77 | 163 return |
| paddy@69 | 164 } |
| paddy@77 | 165 log.Println(err.Error()) |
| paddy@77 | 166 w.WriteHeader(http.StatusInternalServerError) |
| paddy@87 | 167 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{ |
| paddy@77 | 168 "internal_error": template.HTML(err.Error()), |
| paddy@77 | 169 }) |
| paddy@77 | 170 return |
| paddy@69 | 171 } |
| paddy@56 | 172 if r.URL.Query().Get("client_id") == "" { |
| paddy@56 | 173 w.WriteHeader(http.StatusBadRequest) |
| paddy@87 | 174 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{ |
| paddy@61 | 175 "error": template.HTML("Client ID must be specified in the request."), |
| paddy@56 | 176 }) |
| paddy@56 | 177 return |
| paddy@56 | 178 } |
| paddy@56 | 179 clientID, err := uuid.Parse(r.URL.Query().Get("client_id")) |
| paddy@56 | 180 if err != nil { |
| paddy@56 | 181 w.WriteHeader(http.StatusBadRequest) |
| paddy@87 | 182 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{ |
| paddy@61 | 183 "error": template.HTML("client_id is not a valid Client ID."), |
| paddy@56 | 184 }) |
| paddy@56 | 185 return |
| paddy@56 | 186 } |
| paddy@64 | 187 redirectURI := r.URL.Query().Get("redirect_uri") |
| paddy@56 | 188 client, err := context.GetClient(clientID) |
| paddy@56 | 189 if err != nil { |
| paddy@59 | 190 if err == ErrClientNotFound { |
| paddy@59 | 191 w.WriteHeader(http.StatusBadRequest) |
| paddy@87 | 192 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{ |
| paddy@61 | 193 "error": template.HTML("The specified Client couldn’t be found."), |
| paddy@59 | 194 }) |
| paddy@59 | 195 } else { |
| paddy@77 | 196 log.Println(err.Error()) |
| paddy@59 | 197 w.WriteHeader(http.StatusInternalServerError) |
| paddy@87 | 198 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{ |
| paddy@61 | 199 "internal_error": template.HTML(err.Error()), |
| paddy@59 | 200 }) |
| paddy@59 | 201 } |
| paddy@56 | 202 return |
| paddy@56 | 203 } |
| paddy@128 | 204 // BUG(paddy): Checking if the redirect URI is valid should be a helper function. |
| paddy@128 | 205 |
| paddy@56 | 206 // whether a redirect URI is valid or not depends on the number of endpoints |
| paddy@56 | 207 // the client has registered |
| paddy@56 | 208 numEndpoints, err := context.CountEndpoints(clientID) |
| paddy@56 | 209 if err != nil { |
| paddy@77 | 210 log.Println(err.Error()) |
| paddy@56 | 211 w.WriteHeader(http.StatusInternalServerError) |
| paddy@87 | 212 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{ |
| paddy@61 | 213 "internal_error": template.HTML(err.Error()), |
| paddy@56 | 214 }) |
| paddy@56 | 215 return |
| paddy@56 | 216 } |
| paddy@56 | 217 var validURI bool |
| paddy@58 | 218 if redirectURI != "" { |
| paddy@58 | 219 validURI, err = context.CheckEndpoint(clientID, redirectURI) |
| paddy@56 | 220 if err != nil { |
| paddy@116 | 221 if err == ErrEndpointURINotURL { |
| paddy@116 | 222 w.WriteHeader(http.StatusBadRequest) |
| paddy@116 | 223 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{ |
| paddy@116 | 224 "error": template.HTML("The redirect_uri specified is not valid."), |
| paddy@116 | 225 }) |
| paddy@116 | 226 return |
| paddy@116 | 227 } |
| paddy@77 | 228 log.Println(err.Error()) |
| paddy@56 | 229 w.WriteHeader(http.StatusInternalServerError) |
| paddy@87 | 230 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{ |
| paddy@61 | 231 "internal_error": template.HTML(err.Error()), |
| paddy@56 | 232 }) |
| paddy@56 | 233 return |
| paddy@56 | 234 } |
| paddy@56 | 235 } else if redirectURI == "" && numEndpoints == 1 { |
| paddy@56 | 236 // if we don't specify the endpoint and there's only one endpoint, the |
| paddy@56 | 237 // request is valid, and we're redirecting to that one endpoint |
| paddy@56 | 238 validURI = true |
| paddy@56 | 239 endpoints, err := context.ListEndpoints(clientID, 1, 0) |
| paddy@56 | 240 if err != nil { |
| paddy@77 | 241 log.Println(err.Error()) |
| paddy@56 | 242 w.WriteHeader(http.StatusInternalServerError) |
| paddy@87 | 243 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{ |
| paddy@61 | 244 "internal_error": template.HTML(err.Error()), |
| paddy@56 | 245 }) |
| paddy@56 | 246 return |
| paddy@56 | 247 } |
| paddy@56 | 248 if len(endpoints) != 1 { |
| paddy@56 | 249 validURI = false |
| paddy@56 | 250 } else { |
| paddy@116 | 251 redirectURI = endpoints[0].URI |
| paddy@56 | 252 } |
| paddy@56 | 253 } else { |
| paddy@56 | 254 validURI = false |
| paddy@56 | 255 } |
| paddy@56 | 256 if !validURI { |
| paddy@56 | 257 w.WriteHeader(http.StatusBadRequest) |
| paddy@87 | 258 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{ |
| paddy@61 | 259 "error": template.HTML("The redirect_uri specified is not valid."), |
| paddy@56 | 260 }) |
| paddy@56 | 261 return |
| paddy@56 | 262 } |
| paddy@116 | 263 redirectURL, err := url.Parse(redirectURI) |
| paddy@116 | 264 if err != nil { |
| paddy@116 | 265 w.WriteHeader(http.StatusBadRequest) |
| paddy@116 | 266 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{ |
| paddy@116 | 267 "error": template.HTML("The redirect_uri specified is not valid."), |
| paddy@116 | 268 }) |
| paddy@116 | 269 return |
| paddy@116 | 270 } |
| paddy@60 | 271 state := r.URL.Query().Get("state") |
| paddy@122 | 272 responseType := r.URL.Query().Get("response_type") |
| paddy@122 | 273 q := redirectURL.Query() |
| paddy@122 | 274 q.Add("state", state) |
| paddy@122 | 275 if responseType != "code" && responseType != "token" { |
| paddy@65 | 276 q.Add("error", "invalid_request") |
| paddy@65 | 277 redirectURL.RawQuery = q.Encode() |
| paddy@60 | 278 http.Redirect(w, r, redirectURL.String(), http.StatusFound) |
| paddy@60 | 279 return |
| paddy@56 | 280 } |
| paddy@135 | 281 scopeParams := strings.Split(r.URL.Query().Get("scope"), " ") |
| paddy@135 | 282 scopes, err := context.GetScopes(scopeParams) |
| paddy@135 | 283 if err != nil { |
| paddy@153 | 284 if err == ErrScopeNotFound { |
| paddy@135 | 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@135 | 290 log.Println("Error retrieving scopes:", err) |
| paddy@135 | 291 q.Add("error", "server_error") |
| paddy@135 | 292 redirectURL.RawQuery = q.Encode() |
| paddy@135 | 293 http.Redirect(w, r, redirectURL.String(), http.StatusFound) |
| paddy@135 | 294 return |
| paddy@135 | 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 } |