Break out scopes and events.
This repo has gotten unwieldy, and there are portions of it that need to be
imported by a large number of other packages.
For example, scopes will be used in almost every API we write. Rather than
importing the entirety of this codebase into every API we write, I've opted to
move the scope logic out into a scopes package, with a subpackage for the
defined types, which is all most projects actually want to import.
We also define some event type constants, and importing those shouldn't require
a project to import all our dependencies, either. So I made an events subpackage
that just holds those constants.
This package has become a little bit of a red-headed stepchild and is do for a
refactor, but I'm trying to put that off as long as I can.
The refactoring of our scopes stuff has left a bug wherein a token can be
granted for scopes that don't exist. I'm going to need to revisit that, and also
how to limit scopes to only be granted to the users that should be able to
request them. But that's a battle for another day.
18 "code.secondbit.org/scopes.hg/types"
19 "code.secondbit.org/uuid.hg"
20 "github.com/gorilla/mux"
24 defaultAuthorizationCodeExpiration = 600 // default to ten minute grant expirations
25 getAuthorizationCodeTemplateName = "get_grant"
29 // ErrNoAuth is returned when an Authorization header is not present or is empty.
30 ErrNoAuth = errors.New("no authorization header supplied")
31 // ErrInvalidAuthFormat is returned when an Authorization header is present but not the correct format.
32 ErrInvalidAuthFormat = errors.New("authorization header is not in a valid format")
33 // ErrIncorrectAuth is returned when a user authentication attempt does not match the stored values.
34 ErrIncorrectAuth = errors.New("invalid authentication")
35 // ErrInvalidPassphraseScheme is returned when an undefined passphrase scheme is used.
36 ErrInvalidPassphraseScheme = errors.New("invalid passphrase scheme")
37 // ErrNoSession is returned when no session ID is passed with a request.
38 ErrNoSession = errors.New("no session ID found")
40 grantTypesMap = grantTypes{types: map[string]GrantType{}}
43 type grantTypes struct {
44 types map[string]GrantType
48 // GrantType defines a set of functions and metadata around a specific authorization grant strategy.
50 // The Validate function will be called when requests are made that match the GrantType, and should write any
51 // errors to the ResponseWriter. It is responsible for determining if the grant is valid and a token should be issued.
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
53 // is valid or not. It must not be nil.
55 // The Invalidate function will be called when the grant has successfully generated a token and the token has successfully
56 // been conveyed to the user. The Invalidate function is always called asynchronously, outside the request. It should take
57 // care of marking the grant as used, if the GrantType requires grants to be one-time only grants. The Invalidate function
60 // IssuesRefresh determines whether the GrantType should yield a refresh token as well as an access token. If true, the client
61 // will be issued a refresh token.
63 // AllowsPublic determines whether the GrantType should allow public clients to use that grant. If true, clients without
64 // credentials will be able to use the grant to obtain a token.
66 // AuditString should return the string that will be saved in the resulting Token's CreatedFrom field, as an audit log of how
67 // the Token was authorized.
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
70 // was successfully returned and the Invalidate function will be called asynchronously.
71 type GrantType struct {
72 Validate func(w http.ResponseWriter, r *http.Request, context Context) (scopes scopeTypes.Scopes, profileID uuid.ID, valid bool)
73 Invalidate func(r *http.Request, context Context) error
74 ReturnToken func(w http.ResponseWriter, r *http.Request, token Token, context Context) bool
75 AuditString func(r *http.Request) string
80 type tokenResponse struct {
81 AccessToken string `json:"access_token"`
82 TokenType string `json:"token_type,omitempty"`
83 ExpiresIn int32 `json:"expires_in,omitempty"`
84 RefreshToken string `json:"refresh_token,omitempty"`
87 type errorResponse struct {
88 Error string `json:"error"`
89 Description string `json:"error_description,omitempty"`
90 URI string `json:"error_uri,omitempty"`
93 // RegisterGrantType associates a string with a GrantType. When the string is used as the value for "grant_type" when obtaining
94 // an access token, the associated GrantType's properties will be used.
96 // RegisterGrantType should be called in the `init()` function of packages, much like database/sql registers drivers. It will panic
97 // if a GrantType tries to register under a string that already has a GrantType registered for it.
98 func RegisterGrantType(name string, g GrantType) {
100 defer grantTypesMap.Unlock()
101 if _, ok := grantTypesMap.types[name]; ok {
102 panic("Duplicate registration of grant_type " + name)
104 grantTypesMap.types[name] = g
107 func findGrantType(name string) (GrantType, bool) {
108 grantTypesMap.RLock()
109 defer grantTypesMap.RUnlock()
110 t, ok := grantTypesMap.types[name]
114 func renderJSONError(enc *json.Encoder, errorType string) {
115 err := enc.Encode(errorResponse{
123 // RenderJSONToken is an implementation of the ReturnToken function for GrantTypes. It returns the token using JSON
124 // according to the spec. See RFC 6479, Section 4.1.4.
125 func RenderJSONToken(w http.ResponseWriter, r *http.Request, token Token, context Context) bool {
126 enc := json.NewEncoder(w)
127 resp := tokenResponse{
128 AccessToken: token.AccessToken,
129 RefreshToken: token.RefreshToken,
130 ExpiresIn: token.ExpiresIn,
131 TokenType: token.TokenType,
133 w.Header().Set("Content-Type", "application/json")
134 err := enc.Encode(resp)
142 // RegisterOAuth2 adds handlers to the passed router to handle the OAuth2 endpoints.
143 func RegisterOAuth2(r *mux.Router, context Context) {
144 r.Handle("/authorize", wrap(context, GetAuthorizationCodeHandler))
145 r.Handle("/token", wrap(context, GetTokenHandler))
148 // GetAuthorizationCodeHandler presents and processes the page for asking a user to grant access
149 // to their data. See RFC 6749, Section 4.1.
150 func GetAuthorizationCodeHandler(w http.ResponseWriter, r *http.Request, context Context) {
151 session, err := checkCookie(r, context)
153 if err == ErrNoSession || err == ErrInvalidSession {
154 redir := buildLoginRedirect(r, context)
156 log.Println("No login URL configured.")
157 w.WriteHeader(http.StatusInternalServerError)
158 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
159 "internal_error": template.HTML("Missing login URL."),
163 http.Redirect(w, r, redir, http.StatusFound)
166 log.Println(err.Error())
167 w.WriteHeader(http.StatusInternalServerError)
168 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
169 "internal_error": template.HTML(err.Error()),
173 if r.URL.Query().Get("client_id") == "" {
174 w.WriteHeader(http.StatusBadRequest)
175 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
176 "error": template.HTML("Client ID must be specified in the request."),
180 clientID, err := uuid.Parse(r.URL.Query().Get("client_id"))
182 w.WriteHeader(http.StatusBadRequest)
183 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
184 "error": template.HTML("client_id is not a valid Client ID."),
188 redirectURI := r.URL.Query().Get("redirect_uri")
189 client, err := context.GetClient(clientID)
191 if err == ErrClientNotFound {
192 w.WriteHeader(http.StatusBadRequest)
193 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
194 "error": template.HTML("The specified Client couldn’t be found."),
197 log.Println(err.Error())
198 w.WriteHeader(http.StatusInternalServerError)
199 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
200 "internal_error": template.HTML(err.Error()),
205 // BUG(paddy): Checking if the redirect URI is valid should be a helper function.
207 // whether a redirect URI is valid or not depends on the number of endpoints
208 // the client has registered
209 numEndpoints, err := context.CountEndpoints(clientID)
211 log.Println(err.Error())
212 w.WriteHeader(http.StatusInternalServerError)
213 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
214 "internal_error": template.HTML(err.Error()),
219 if redirectURI != "" {
220 validURI, err = context.CheckEndpoint(clientID, redirectURI)
222 if err == ErrEndpointURINotURL {
223 w.WriteHeader(http.StatusBadRequest)
224 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
225 "error": template.HTML("The redirect_uri specified is not valid."),
229 log.Println(err.Error())
230 w.WriteHeader(http.StatusInternalServerError)
231 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
232 "internal_error": template.HTML(err.Error()),
236 } else if redirectURI == "" && numEndpoints == 1 {
237 // if we don't specify the endpoint and there's only one endpoint, the
238 // request is valid, and we're redirecting to that one endpoint
240 endpoints, err := context.ListEndpoints(clientID, 1, 0)
242 log.Println(err.Error())
243 w.WriteHeader(http.StatusInternalServerError)
244 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
245 "internal_error": template.HTML(err.Error()),
249 if len(endpoints) != 1 {
252 redirectURI = endpoints[0].URI
258 w.WriteHeader(http.StatusBadRequest)
259 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
260 "error": template.HTML("The redirect_uri specified is not valid."),
264 redirectURL, err := url.Parse(redirectURI)
266 w.WriteHeader(http.StatusBadRequest)
267 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
268 "error": template.HTML("The redirect_uri specified is not valid."),
272 state := r.URL.Query().Get("state")
273 responseType := r.URL.Query().Get("response_type")
274 q := redirectURL.Query()
275 q.Add("state", state)
276 if responseType != "code" && responseType != "token" {
277 q.Add("error", "invalid_request")
278 redirectURL.RawQuery = q.Encode()
279 http.Redirect(w, r, redirectURL.String(), http.StatusFound)
282 scopes := scopeTypes.StringsToScopes(strings.Split(r.URL.Query().Get("scope"), " "))
283 // BUG(paddy): need to check if Scopes actually exist
284 /*if err == ErrScopeNotFound {
285 q.Add("error", "invalid_scope")
286 redirectURL.RawQuery = q.Encode()
287 http.Redirect(w, r, redirectURL.String(), http.StatusFound)
290 log.Println("Error retrieving scopes:", err)
291 q.Add("error", "server_error")
292 redirectURL.RawQuery = q.Encode()
293 http.Redirect(w, r, redirectURL.String(), http.StatusFound)
296 if r.Method == "POST" {
297 if checkCSRF(r, session) != nil {
298 log.Println("CSRF attempt detected.")
299 w.WriteHeader(http.StatusInternalServerError)
300 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
301 "error": template.HTML("There was an error authenticating your request."),
305 if r.PostFormValue("grant") == "approved" {
307 switch responseType {
309 code := make([]byte, 16)
310 _, err := io.ReadFull(rand.Reader, code)
312 log.Printf("Error generating code: %#+v\n", err)
313 q.Add("error", "server_error")
316 authCode := AuthorizationCode{
317 Code: hex.EncodeToString(code),
319 ExpiresIn: defaultAuthorizationCodeExpiration,
322 RedirectURI: r.URL.Query().Get("redirect_uri"),
324 ProfileID: session.ProfileID,
326 err = context.SaveAuthorizationCode(authCode)
328 log.Println("Error saving authorization code:", err)
329 q.Add("error", "server_error")
332 q.Add("code", authCode.Code)
336 CreatedFrom: "implicit",
337 ExpiresIn: defaultTokenExpiration,
340 ProfileID: session.ProfileID,
343 access, err := token.GenerateAccessToken(context.config.JWTPrivateKey)
345 log.Printf("Error signing token: %+v\n", err)
346 q.Add("error", "server_error")
349 token.AccessToken = access
350 err = context.SaveToken(token)
352 log.Println("Error saving token:", err)
353 q.Add("error", "server_error")
356 q = url.Values{} // we're not altering the querystring, so don't clone it
357 q.Add("access_token", token.AccessToken)
358 q.Add("token_type", token.TokenType)
359 q.Add("expires_in", strconv.FormatInt(int64(token.ExpiresIn), 10))
360 q.Add("scope", strings.Join(token.Scopes.Strings(), " "))
361 q.Add("state", state) // we wiped out the old values, so we need to set the state again
365 redirectURL.Fragment = q.Encode()
367 redirectURL.RawQuery = q.Encode()
369 http.Redirect(w, r, redirectURL.String(), http.StatusFound)
372 q.Add("error", "access_denied")
373 redirectURL.RawQuery = q.Encode()
374 http.Redirect(w, r, redirectURL.String(), http.StatusFound)
377 profile, err := context.GetProfileByID(session.ProfileID)
379 log.Println("Error getting profile from session:", err)
380 q.Add("error", "server_error")
381 redirectURL.RawQuery = q.Encode()
382 http.Redirect(w, r, redirectURL.String(), http.StatusFound)
385 w.WriteHeader(http.StatusOK)
386 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
388 "redirectURL": redirectURL,
391 "csrftoken": session.CSRFToken,
395 // GetTokenHandler allows a client to exchange an authorization grant for an
396 // access token. See RFC 6749 Section 4.1.3.
397 func GetTokenHandler(w http.ResponseWriter, r *http.Request, context Context) {
398 enc := json.NewEncoder(w)
399 grantType := r.PostFormValue("grant_type")
400 gt, ok := findGrantType(grantType)
402 w.WriteHeader(http.StatusBadRequest)
403 renderJSONError(enc, "invalid_request")
406 clientID, success := verifyClient(w, r, gt.AllowsPublic, context)
410 scopes, profileID, valid := gt.Validate(w, r, context)
415 if gt.IssuesRefresh {
416 refresh = uuid.NewID().String()
419 AccessToken: uuid.NewID().String(),
420 RefreshToken: refresh,
422 CreatedFrom: gt.AuditString(r),
423 ExpiresIn: defaultTokenExpiration,
426 ProfileID: profileID,
429 access, err := token.GenerateAccessToken(context.config.JWTPrivateKey)
431 log.Printf("Error signing token: %+v\n", err)
432 w.WriteHeader(http.StatusInternalServerError)
433 renderJSONError(enc, "server_error")
436 token.AccessToken = access
437 err = context.SaveToken(token)
439 w.WriteHeader(http.StatusInternalServerError)
440 renderJSONError(enc, "server_error")
443 if gt.ReturnToken(w, r, token, context) && gt.Invalidate != nil {
444 go gt.Invalidate(r, context)