Refactor verifyClient, implement refresh tokens.
Refactor verifyClient into verifyClient and getClientAuth. We moved verifyClient
out of each of the GrantType's validation functions and into the access token
endpoint, where it will be called before the GrantType's validation function.
Yay, less code repetition. And seeing as we always want to verify the client,
that seems like a good way to prevent things like 118a69954621 from happening.
This did, however, force us to add an AllowsPublic property to the GrantType, so
the token endpoint knows whether or not a public Client is valid for any given
GrantType.
We also implemented the refresh token grant type, which required adding ClientID
and RefreshRevoked as properties on the Token type. We need ClientID because we
need to constrain refresh tokens to the client that issued them. We also should
probably keep track of which tokens belong to which clients, just as a general
rule of thumb. RefreshRevoked had to be created, next to Revoked, because the
AccessToken could be revoked and the RefreshToken still valid, or vice versa.
Notably, when you issue a new refresh token, the old one is revoked, but the
access token is still valid. It remains to be seen whether this is a good way to
track things or not. The number of duplicated properties lead me to believe our
type is not a great representation of the underlying concepts.
14 "code.secondbit.org/uuid.hg"
15 "github.com/gorilla/mux"
19 authCookieName = "auth"
20 defaultAuthorizationCodeExpiration = 600 // default to ten minute grant expirations
21 getAuthorizationCodeTemplateName = "get_grant"
25 // ErrNoAuth is returned when an Authorization header is not present or is empty.
26 ErrNoAuth = errors.New("no authorization header supplied")
27 // ErrInvalidAuthFormat is returned when an Authorization header is present but not the correct format.
28 ErrInvalidAuthFormat = errors.New("authorization header is not in a valid format")
29 // ErrIncorrectAuth is returned when a user authentication attempt does not match the stored values.
30 ErrIncorrectAuth = errors.New("invalid authentication")
31 // ErrInvalidPassphraseScheme is returned when an undefined passphrase scheme is used.
32 ErrInvalidPassphraseScheme = errors.New("invalid passphrase scheme")
33 // ErrNoSession is returned when no session ID is passed with a request.
34 ErrNoSession = errors.New("no session ID found")
36 grantTypesMap = grantTypes{types: map[string]GrantType{}}
39 type grantTypes struct {
40 types map[string]GrantType
44 // GrantType defines a set of functions and metadata around a specific authorization grant strategy.
46 // The Validate function will be called when requests are made that match the GrantType, and should write any
47 // errors to the ResponseWriter. It is responsible for determining if the grant is valid and a token should be issued.
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
49 // is valid or not. It must not be nil.
51 // The Invalidate function will be called when the grant has successfully generated a token and the token has successfully
52 // been conveyed to the user. The Invalidate function is always called asynchronously, outside the request. It should take
53 // care of marking the grant as used, if the GrantType requires grants to be one-time only grants. The Invalidate function
56 // IssuesRefresh determines whether the GrantType should yield a refresh token as well as an access token. If true, the client
57 // will be issued a refresh token.
59 // AllowsPublic determines whether the GrantType should allow public clients to use that grant. If true, clients without
60 // credentials will be able to use the grant to obtain a token.
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
63 // was successfully returned and the Invalidate function will be called asynchronously.
64 type GrantType struct {
65 Validate func(w http.ResponseWriter, r *http.Request, context Context) (scope string, profileID uuid.ID, valid bool)
66 Invalidate func(r *http.Request, context Context) error
67 ReturnToken func(w http.ResponseWriter, r *http.Request, token Token, context Context) bool
72 type tokenResponse struct {
73 AccessToken string `json:"access_token"`
74 TokenType string `json:"token_type,omitempty"`
75 ExpiresIn int32 `json:"expires_in,omitempty"`
76 RefreshToken string `json:"refresh_token,omitempty"`
79 type errorResponse struct {
80 Error string `json:"error"`
81 Description string `json:"error_description,omitempty"`
82 URI string `json:"error_uri,omitempty"`
85 // RegisterGrantType associates a string with a GrantType. When the string is used as the value for "grant_type" when obtaining
86 // an access token, the associated GrantType's properties will be used.
88 // RegisterGrantType should be called in the `init()` function of packages, much like database/sql registers drivers. It will panic
89 // if a GrantType tries to register under a string that already has a GrantType registered for it.
90 func RegisterGrantType(name string, g GrantType) {
92 defer grantTypesMap.Unlock()
93 if _, ok := grantTypesMap.types[name]; ok {
94 panic("Duplicate registration of grant_type " + name)
96 grantTypesMap.types[name] = g
99 func findGrantType(name string) (GrantType, bool) {
100 grantTypesMap.RLock()
101 defer grantTypesMap.RUnlock()
102 t, ok := grantTypesMap.types[name]
106 func renderJSONError(enc *json.Encoder, errorType string) {
107 err := enc.Encode(errorResponse{
115 // RenderJSONToken is an implementation of the ReturnToken function for GrantTypes. It returns the token using JSON
116 // according to the spec. See RFC 6479, Section 4.1.4.
117 func RenderJSONToken(w http.ResponseWriter, r *http.Request, token Token, context Context) bool {
118 enc := json.NewEncoder(w)
119 resp := tokenResponse{
120 AccessToken: token.AccessToken,
121 RefreshToken: token.RefreshToken,
122 ExpiresIn: token.ExpiresIn,
123 TokenType: token.TokenType,
125 w.Header().Set("Content-Type", "application/json")
126 err := enc.Encode(resp)
134 // RegisterOAuth2 adds handlers to the passed router to handle the OAuth2 endpoints.
135 func RegisterOAuth2(r *mux.Router, context Context) {
136 r.Handle("/authorize", wrap(context, GetAuthorizationCodeHandler))
137 r.Handle("/token", wrap(context, GetTokenHandler))
140 // GetAuthorizationCodeHandler presents and processes the page for asking a user to grant access
141 // to their data. See RFC 6749, Section 4.1.
142 func GetAuthorizationCodeHandler(w http.ResponseWriter, r *http.Request, context Context) {
143 session, err := checkCookie(r, context)
145 if err == ErrNoSession || err == ErrInvalidSession {
146 redir := buildLoginRedirect(r, context)
148 log.Println("No login URL configured.")
149 w.WriteHeader(http.StatusInternalServerError)
150 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
151 "internal_error": template.HTML("Missing login URL."),
155 http.Redirect(w, r, redir, http.StatusFound)
158 log.Println(err.Error())
159 w.WriteHeader(http.StatusInternalServerError)
160 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
161 "internal_error": template.HTML(err.Error()),
165 if r.URL.Query().Get("client_id") == "" {
166 w.WriteHeader(http.StatusBadRequest)
167 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
168 "error": template.HTML("Client ID must be specified in the request."),
172 clientID, err := uuid.Parse(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 is not a valid Client ID."),
180 redirectURI := r.URL.Query().Get("redirect_uri")
181 client, err := context.GetClient(clientID)
183 if err == ErrClientNotFound {
184 w.WriteHeader(http.StatusBadRequest)
185 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
186 "error": template.HTML("The specified Client couldn’t be found."),
189 log.Println(err.Error())
190 w.WriteHeader(http.StatusInternalServerError)
191 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
192 "internal_error": template.HTML(err.Error()),
197 // BUG(paddy): checking if the redirect URI is valid should be a helper function
198 // whether a redirect URI is valid or not depends on the number of endpoints
199 // the client has registered
200 numEndpoints, err := context.CountEndpoints(clientID)
202 log.Println(err.Error())
203 w.WriteHeader(http.StatusInternalServerError)
204 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
205 "internal_error": template.HTML(err.Error()),
210 if redirectURI != "" {
211 validURI, err = context.CheckEndpoint(clientID, redirectURI)
213 if err == ErrEndpointURINotURL {
214 w.WriteHeader(http.StatusBadRequest)
215 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
216 "error": template.HTML("The redirect_uri specified is not valid."),
220 log.Println(err.Error())
221 w.WriteHeader(http.StatusInternalServerError)
222 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
223 "internal_error": template.HTML(err.Error()),
227 } else if redirectURI == "" && numEndpoints == 1 {
228 // if we don't specify the endpoint and there's only one endpoint, the
229 // request is valid, and we're redirecting to that one endpoint
231 endpoints, err := context.ListEndpoints(clientID, 1, 0)
233 log.Println(err.Error())
234 w.WriteHeader(http.StatusInternalServerError)
235 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
236 "internal_error": template.HTML(err.Error()),
240 if len(endpoints) != 1 {
243 redirectURI = endpoints[0].URI
249 w.WriteHeader(http.StatusBadRequest)
250 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
251 "error": template.HTML("The redirect_uri specified is not valid."),
255 redirectURL, err := url.Parse(redirectURI)
257 w.WriteHeader(http.StatusBadRequest)
258 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
259 "error": template.HTML("The redirect_uri specified is not valid."),
263 scope := r.URL.Query().Get("scope")
264 state := r.URL.Query().Get("state")
265 responseType := r.URL.Query().Get("response_type")
266 q := redirectURL.Query()
267 q.Add("state", state)
268 if responseType != "code" && responseType != "token" {
269 q.Add("error", "invalid_request")
270 redirectURL.RawQuery = q.Encode()
271 http.Redirect(w, r, redirectURL.String(), http.StatusFound)
274 if r.Method == "POST" {
275 // BUG(paddy): We need to implement CSRF protection when obtaining a grant code.
276 if r.PostFormValue("grant") == "approved" {
278 switch responseType {
280 code := uuid.NewID().String()
281 authCode := AuthorizationCode{
284 ExpiresIn: defaultAuthorizationCodeExpiration,
287 RedirectURI: r.URL.Query().Get("redirect_uri"),
289 ProfileID: session.ProfileID,
291 err := context.SaveAuthorizationCode(authCode)
293 log.Println("Error saving authorization code:", err)
294 q.Add("error", "server_error")
300 AccessToken: uuid.NewID().String(),
303 ExpiresIn: defaultTokenExpiration,
306 ProfileID: session.ProfileID,
308 err := context.SaveToken(token)
310 log.Println("Error saving token:", err)
311 q.Add("error", "server_error")
314 q = url.Values{} // we're not altering the querystring, so don't clone it
315 q.Add("access_token", token.AccessToken)
316 q.Add("token_type", token.TokenType)
317 q.Add("expires_in", strconv.FormatInt(int64(token.ExpiresIn), 10))
318 q.Add("scope", token.Scope)
319 q.Add("state", state) // we wiped out the old values, so we need to set the state again
323 redirectURL.Fragment = q.Encode()
325 redirectURL.RawQuery = q.Encode()
327 http.Redirect(w, r, redirectURL.String(), http.StatusFound)
330 q.Add("error", "access_denied")
331 redirectURL.RawQuery = q.Encode()
332 http.Redirect(w, r, redirectURL.String(), http.StatusFound)
335 profile, err := context.GetProfileByID(session.ProfileID)
337 log.Println("Error getting profile from session:", err)
338 q.Add("error", "server_error")
339 redirectURL.RawQuery = q.Encode()
340 http.Redirect(w, r, redirectURL.String(), http.StatusFound)
343 w.WriteHeader(http.StatusOK)
344 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
346 "redirectURL": redirectURL,
352 // GetTokenHandler allows a client to exchange an authorization grant for an
353 // access token. See RFC 6749 Section 4.1.3.
354 func GetTokenHandler(w http.ResponseWriter, r *http.Request, context Context) {
355 enc := json.NewEncoder(w)
356 grantType := r.PostFormValue("grant_type")
357 gt, ok := findGrantType(grantType)
359 w.WriteHeader(http.StatusBadRequest)
360 renderJSONError(enc, "invalid_request")
363 clientID, success := verifyClient(w, r, gt.AllowsPublic, context)
367 scope, profileID, valid := gt.Validate(w, r, context)
372 if gt.IssuesRefresh {
373 refresh = uuid.NewID().String()
376 AccessToken: uuid.NewID().String(),
377 RefreshToken: refresh,
379 ExpiresIn: defaultTokenExpiration,
380 RefreshExpiresIn: defaultRefreshTokenExpiration,
383 ProfileID: profileID,
386 err := context.SaveToken(token)
388 w.WriteHeader(http.StatusInternalServerError)
389 renderJSONError(enc, "server_error")
392 if gt.ReturnToken(w, r, token, context) && gt.Invalidate != nil {
393 go gt.Invalidate(r, context)