auth
auth/oauth2.go
Drop RemoveToken, test RevokeToken for refresh tokens. The RemoveToken function was never actually used, and there's no reason to use it. Tokens are revoked, not removed. So I deleted it. And while I was updating the tests, I finally inserted the test for revoking a refresh token, mainly to get rid of that damn TODO.
1 package auth
3 import (
4 "encoding/json"
5 "errors"
6 "html/template"
7 "log"
8 "net/http"
9 "net/url"
10 "strconv"
11 "sync"
12 "time"
14 "code.secondbit.org/uuid.hg"
15 "github.com/gorilla/mux"
16 )
18 const (
19 authCookieName = "auth"
20 defaultAuthorizationCodeExpiration = 600 // default to ten minute grant expirations
21 getAuthorizationCodeTemplateName = "get_grant"
22 )
24 var (
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{}}
37 )
39 type grantTypes struct {
40 types map[string]GrantType
41 sync.RWMutex
42 }
44 // GrantType defines a set of functions and metadata around a specific authorization grant strategy.
45 //
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.
50 //
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
54 // can be nil.
55 //
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.
58 //
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.
61 //
62 // AuditString should return the string that will be saved in the resulting Token's CreatedFrom field, as an audit log of how
63 // the Token was authorized.
64 //
65 // The ReturnToken will be called when a token is created and needs to be returned to the client. If it returns true, the token
66 // was successfully returned and the Invalidate function will be called asynchronously.
67 type GrantType struct {
68 Validate func(w http.ResponseWriter, r *http.Request, context Context) (scope string, profileID uuid.ID, valid bool)
69 Invalidate func(r *http.Request, context Context) error
70 ReturnToken func(w http.ResponseWriter, r *http.Request, token Token, context Context) bool
71 AuditString func(r *http.Request) string
72 IssuesRefresh bool
73 AllowsPublic bool
74 }
76 type tokenResponse struct {
77 AccessToken string `json:"access_token"`
78 TokenType string `json:"token_type,omitempty"`
79 ExpiresIn int32 `json:"expires_in,omitempty"`
80 RefreshToken string `json:"refresh_token,omitempty"`
81 }
83 type errorResponse struct {
84 Error string `json:"error"`
85 Description string `json:"error_description,omitempty"`
86 URI string `json:"error_uri,omitempty"`
87 }
89 // RegisterGrantType associates a string with a GrantType. When the string is used as the value for "grant_type" when obtaining
90 // an access token, the associated GrantType's properties will be used.
91 //
92 // RegisterGrantType should be called in the `init()` function of packages, much like database/sql registers drivers. It will panic
93 // if a GrantType tries to register under a string that already has a GrantType registered for it.
94 func RegisterGrantType(name string, g GrantType) {
95 grantTypesMap.Lock()
96 defer grantTypesMap.Unlock()
97 if _, ok := grantTypesMap.types[name]; ok {
98 panic("Duplicate registration of grant_type " + name)
99 }
100 grantTypesMap.types[name] = g
101 }
103 func findGrantType(name string) (GrantType, bool) {
104 grantTypesMap.RLock()
105 defer grantTypesMap.RUnlock()
106 t, ok := grantTypesMap.types[name]
107 return t, ok
108 }
110 func renderJSONError(enc *json.Encoder, errorType string) {
111 err := enc.Encode(errorResponse{
112 Error: errorType,
113 })
114 if err != nil {
115 log.Println(err)
116 }
117 }
119 // RenderJSONToken is an implementation of the ReturnToken function for GrantTypes. It returns the token using JSON
120 // according to the spec. See RFC 6479, Section 4.1.4.
121 func RenderJSONToken(w http.ResponseWriter, r *http.Request, token Token, context Context) bool {
122 enc := json.NewEncoder(w)
123 resp := tokenResponse{
124 AccessToken: token.AccessToken,
125 RefreshToken: token.RefreshToken,
126 ExpiresIn: token.ExpiresIn,
127 TokenType: token.TokenType,
128 }
129 w.Header().Set("Content-Type", "application/json")
130 err := enc.Encode(resp)
131 if err != nil {
132 log.Println(err)
133 return false
134 }
135 return true
136 }
138 // RegisterOAuth2 adds handlers to the passed router to handle the OAuth2 endpoints.
139 func RegisterOAuth2(r *mux.Router, context Context) {
140 r.Handle("/authorize", wrap(context, GetAuthorizationCodeHandler))
141 r.Handle("/token", wrap(context, GetTokenHandler))
142 }
144 // GetAuthorizationCodeHandler presents and processes the page for asking a user to grant access
145 // to their data. See RFC 6749, Section 4.1.
146 func GetAuthorizationCodeHandler(w http.ResponseWriter, r *http.Request, context Context) {
147 session, err := checkCookie(r, context)
148 if err != nil {
149 if err == ErrNoSession || err == ErrInvalidSession {
150 redir := buildLoginRedirect(r, context)
151 if redir == "" {
152 log.Println("No login URL configured.")
153 w.WriteHeader(http.StatusInternalServerError)
154 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
155 "internal_error": template.HTML("Missing login URL."),
156 })
157 return
158 }
159 http.Redirect(w, r, redir, http.StatusFound)
160 return
161 }
162 log.Println(err.Error())
163 w.WriteHeader(http.StatusInternalServerError)
164 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
165 "internal_error": template.HTML(err.Error()),
166 })
167 return
168 }
169 if r.URL.Query().Get("client_id") == "" {
170 w.WriteHeader(http.StatusBadRequest)
171 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
172 "error": template.HTML("Client ID must be specified in the request."),
173 })
174 return
175 }
176 clientID, err := uuid.Parse(r.URL.Query().Get("client_id"))
177 if err != nil {
178 w.WriteHeader(http.StatusBadRequest)
179 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
180 "error": template.HTML("client_id is not a valid Client ID."),
181 })
182 return
183 }
184 redirectURI := r.URL.Query().Get("redirect_uri")
185 client, err := context.GetClient(clientID)
186 if err != nil {
187 if err == ErrClientNotFound {
188 w.WriteHeader(http.StatusBadRequest)
189 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
190 "error": template.HTML("The specified Client couldn’t be found."),
191 })
192 } else {
193 log.Println(err.Error())
194 w.WriteHeader(http.StatusInternalServerError)
195 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
196 "internal_error": template.HTML(err.Error()),
197 })
198 }
199 return
200 }
201 // BUG(paddy): checking if the redirect URI is valid should be a helper function
202 // whether a redirect URI is valid or not depends on the number of endpoints
203 // the client has registered
204 numEndpoints, err := context.CountEndpoints(clientID)
205 if err != nil {
206 log.Println(err.Error())
207 w.WriteHeader(http.StatusInternalServerError)
208 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
209 "internal_error": template.HTML(err.Error()),
210 })
211 return
212 }
213 var validURI bool
214 if redirectURI != "" {
215 validURI, err = context.CheckEndpoint(clientID, redirectURI)
216 if err != nil {
217 if err == ErrEndpointURINotURL {
218 w.WriteHeader(http.StatusBadRequest)
219 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
220 "error": template.HTML("The redirect_uri specified is not valid."),
221 })
222 return
223 }
224 log.Println(err.Error())
225 w.WriteHeader(http.StatusInternalServerError)
226 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
227 "internal_error": template.HTML(err.Error()),
228 })
229 return
230 }
231 } else if redirectURI == "" && numEndpoints == 1 {
232 // if we don't specify the endpoint and there's only one endpoint, the
233 // request is valid, and we're redirecting to that one endpoint
234 validURI = true
235 endpoints, err := context.ListEndpoints(clientID, 1, 0)
236 if err != nil {
237 log.Println(err.Error())
238 w.WriteHeader(http.StatusInternalServerError)
239 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
240 "internal_error": template.HTML(err.Error()),
241 })
242 return
243 }
244 if len(endpoints) != 1 {
245 validURI = false
246 } else {
247 redirectURI = endpoints[0].URI
248 }
249 } else {
250 validURI = false
251 }
252 if !validURI {
253 w.WriteHeader(http.StatusBadRequest)
254 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
255 "error": template.HTML("The redirect_uri specified is not valid."),
256 })
257 return
258 }
259 redirectURL, err := url.Parse(redirectURI)
260 if err != nil {
261 w.WriteHeader(http.StatusBadRequest)
262 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
263 "error": template.HTML("The redirect_uri specified is not valid."),
264 })
265 return
266 }
267 scope := r.URL.Query().Get("scope")
268 state := r.URL.Query().Get("state")
269 responseType := r.URL.Query().Get("response_type")
270 q := redirectURL.Query()
271 q.Add("state", state)
272 if responseType != "code" && responseType != "token" {
273 q.Add("error", "invalid_request")
274 redirectURL.RawQuery = q.Encode()
275 http.Redirect(w, r, redirectURL.String(), http.StatusFound)
276 return
277 }
278 if r.Method == "POST" {
279 // BUG(paddy): We need to implement CSRF protection when obtaining a grant code.
280 if r.PostFormValue("grant") == "approved" {
281 var fragment bool
282 switch responseType {
283 case "code":
284 code := uuid.NewID().String()
285 authCode := AuthorizationCode{
286 Code: code,
287 Created: time.Now(),
288 ExpiresIn: defaultAuthorizationCodeExpiration,
289 ClientID: clientID,
290 Scope: scope,
291 RedirectURI: r.URL.Query().Get("redirect_uri"),
292 State: state,
293 ProfileID: session.ProfileID,
294 }
295 err := context.SaveAuthorizationCode(authCode)
296 if err != nil {
297 log.Println("Error saving authorization code:", err)
298 q.Add("error", "server_error")
299 break
300 }
301 q.Add("code", code)
302 case "token":
303 token := Token{
304 AccessToken: uuid.NewID().String(),
305 Created: time.Now(),
306 CreatedFrom: "implicit",
307 ExpiresIn: defaultTokenExpiration,
308 TokenType: "bearer",
309 Scope: scope,
310 ProfileID: session.ProfileID,
311 ClientID: clientID,
312 }
313 err := context.SaveToken(token)
314 if err != nil {
315 log.Println("Error saving token:", err)
316 q.Add("error", "server_error")
317 break
318 }
319 q = url.Values{} // we're not altering the querystring, so don't clone it
320 q.Add("access_token", token.AccessToken)
321 q.Add("token_type", token.TokenType)
322 q.Add("expires_in", strconv.FormatInt(int64(token.ExpiresIn), 10))
323 q.Add("scope", token.Scope)
324 q.Add("state", state) // we wiped out the old values, so we need to set the state again
325 fragment = true
326 }
327 if fragment {
328 redirectURL.Fragment = q.Encode()
329 } else {
330 redirectURL.RawQuery = q.Encode()
331 }
332 http.Redirect(w, r, redirectURL.String(), http.StatusFound)
333 return
334 }
335 q.Add("error", "access_denied")
336 redirectURL.RawQuery = q.Encode()
337 http.Redirect(w, r, redirectURL.String(), http.StatusFound)
338 return
339 }
340 profile, err := context.GetProfileByID(session.ProfileID)
341 if err != nil {
342 log.Println("Error getting profile from session:", err)
343 q.Add("error", "server_error")
344 redirectURL.RawQuery = q.Encode()
345 http.Redirect(w, r, redirectURL.String(), http.StatusFound)
346 return
347 }
348 w.WriteHeader(http.StatusOK)
349 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
350 "client": client,
351 "redirectURL": redirectURL,
352 "scope": scope,
353 "profile": profile,
354 })
355 }
357 // GetTokenHandler allows a client to exchange an authorization grant for an
358 // access token. See RFC 6749 Section 4.1.3.
359 func GetTokenHandler(w http.ResponseWriter, r *http.Request, context Context) {
360 enc := json.NewEncoder(w)
361 grantType := r.PostFormValue("grant_type")
362 gt, ok := findGrantType(grantType)
363 if !ok {
364 w.WriteHeader(http.StatusBadRequest)
365 renderJSONError(enc, "invalid_request")
366 return
367 }
368 clientID, success := verifyClient(w, r, gt.AllowsPublic, context)
369 if !success {
370 return
371 }
372 scope, profileID, valid := gt.Validate(w, r, context)
373 if !valid {
374 return
375 }
376 refresh := ""
377 if gt.IssuesRefresh {
378 refresh = uuid.NewID().String()
379 }
380 token := Token{
381 AccessToken: uuid.NewID().String(),
382 RefreshToken: refresh,
383 Created: time.Now(),
384 CreatedFrom: gt.AuditString(r),
385 ExpiresIn: defaultTokenExpiration,
386 TokenType: "bearer",
387 Scope: scope,
388 ProfileID: profileID,
389 ClientID: clientID,
390 }
391 err := context.SaveToken(token)
392 if err != nil {
393 w.WriteHeader(http.StatusInternalServerError)
394 renderJSONError(enc, "server_error")
395 return
396 }
397 if gt.ReturnToken(w, r, token, context) && gt.Invalidate != nil {
398 go gt.Invalidate(r, context)
399 }
400 }