auth

Paddy 2015-01-28 Parent:23c1a07c8a61 Child:d30a3a12d387

132:163ce22fa4c9 Go to Latest

auth/oauth2.go

Enable CSRF protection, add expiration to sessions. Sessions gain a CSRF token, which is passed as a parameter to the login page. The login page now checks for that CSRF token, and logs a CSRF attempt if the token does not match. I also added an expiration to sessions, so they don't last forever. Sessions should be pretty short--we just need to stay logged in for long enough to approve the OAuth request. Everything after that should be cookie based. Finally, I added a configuration parameter to control whether the session cookie should be set to Secure, requiring the use of HTTPS. For production use, this flag is a requirement, but it makes testing extremely difficult, so we need a way to disable it.

History
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 defaultAuthorizationCodeExpiration = 600 // default to ten minute grant expirations
20 getAuthorizationCodeTemplateName = "get_grant"
21 )
23 var (
24 // ErrNoAuth is returned when an Authorization header is not present or is empty.
25 ErrNoAuth = errors.New("no authorization header supplied")
26 // ErrInvalidAuthFormat is returned when an Authorization header is present but not the correct format.
27 ErrInvalidAuthFormat = errors.New("authorization header is not in a valid format")
28 // ErrIncorrectAuth is returned when a user authentication attempt does not match the stored values.
29 ErrIncorrectAuth = errors.New("invalid authentication")
30 // ErrInvalidPassphraseScheme is returned when an undefined passphrase scheme is used.
31 ErrInvalidPassphraseScheme = errors.New("invalid passphrase scheme")
32 // ErrNoSession is returned when no session ID is passed with a request.
33 ErrNoSession = errors.New("no session ID found")
35 grantTypesMap = grantTypes{types: map[string]GrantType{}}
36 )
38 type grantTypes struct {
39 types map[string]GrantType
40 sync.RWMutex
41 }
43 // GrantType defines a set of functions and metadata around a specific authorization grant strategy.
44 //
45 // The Validate function will be called when requests are made that match the GrantType, and should write any
46 // errors to the ResponseWriter. It is responsible for determining if the grant is valid and a token should be issued.
47 // 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
48 // is valid or not. It must not be nil.
49 //
50 // The Invalidate function will be called when the grant has successfully generated a token and the token has successfully
51 // been conveyed to the user. The Invalidate function is always called asynchronously, outside the request. It should take
52 // care of marking the grant as used, if the GrantType requires grants to be one-time only grants. The Invalidate function
53 // can be nil.
54 //
55 // IssuesRefresh determines whether the GrantType should yield a refresh token as well as an access token. If true, the client
56 // will be issued a refresh token.
57 //
58 // AllowsPublic determines whether the GrantType should allow public clients to use that grant. If true, clients without
59 // credentials will be able to use the grant to obtain a token.
60 //
61 // AuditString should return the string that will be saved in the resulting Token's CreatedFrom field, as an audit log of how
62 // the Token was authorized.
63 //
64 // The ReturnToken will be called when a token is created and needs to be returned to the client. If it returns true, the token
65 // was successfully returned and the Invalidate function will be called asynchronously.
66 type GrantType struct {
67 Validate func(w http.ResponseWriter, r *http.Request, context Context) (scope string, profileID uuid.ID, valid bool)
68 Invalidate func(r *http.Request, context Context) error
69 ReturnToken func(w http.ResponseWriter, r *http.Request, token Token, context Context) bool
70 AuditString func(r *http.Request) string
71 IssuesRefresh bool
72 AllowsPublic bool
73 }
75 type tokenResponse struct {
76 AccessToken string `json:"access_token"`
77 TokenType string `json:"token_type,omitempty"`
78 ExpiresIn int32 `json:"expires_in,omitempty"`
79 RefreshToken string `json:"refresh_token,omitempty"`
80 }
82 type errorResponse struct {
83 Error string `json:"error"`
84 Description string `json:"error_description,omitempty"`
85 URI string `json:"error_uri,omitempty"`
86 }
88 // RegisterGrantType associates a string with a GrantType. When the string is used as the value for "grant_type" when obtaining
89 // an access token, the associated GrantType's properties will be used.
90 //
91 // RegisterGrantType should be called in the `init()` function of packages, much like database/sql registers drivers. It will panic
92 // if a GrantType tries to register under a string that already has a GrantType registered for it.
93 func RegisterGrantType(name string, g GrantType) {
94 grantTypesMap.Lock()
95 defer grantTypesMap.Unlock()
96 if _, ok := grantTypesMap.types[name]; ok {
97 panic("Duplicate registration of grant_type " + name)
98 }
99 grantTypesMap.types[name] = g
100 }
102 func findGrantType(name string) (GrantType, bool) {
103 grantTypesMap.RLock()
104 defer grantTypesMap.RUnlock()
105 t, ok := grantTypesMap.types[name]
106 return t, ok
107 }
109 func renderJSONError(enc *json.Encoder, errorType string) {
110 err := enc.Encode(errorResponse{
111 Error: errorType,
112 })
113 if err != nil {
114 log.Println(err)
115 }
116 }
118 // RenderJSONToken is an implementation of the ReturnToken function for GrantTypes. It returns the token using JSON
119 // according to the spec. See RFC 6479, Section 4.1.4.
120 func RenderJSONToken(w http.ResponseWriter, r *http.Request, token Token, context Context) bool {
121 enc := json.NewEncoder(w)
122 resp := tokenResponse{
123 AccessToken: token.AccessToken,
124 RefreshToken: token.RefreshToken,
125 ExpiresIn: token.ExpiresIn,
126 TokenType: token.TokenType,
127 }
128 w.Header().Set("Content-Type", "application/json")
129 err := enc.Encode(resp)
130 if err != nil {
131 log.Println(err)
132 return false
133 }
134 return true
135 }
137 // RegisterOAuth2 adds handlers to the passed router to handle the OAuth2 endpoints.
138 func RegisterOAuth2(r *mux.Router, context Context) {
139 r.Handle("/authorize", wrap(context, GetAuthorizationCodeHandler))
140 r.Handle("/token", wrap(context, GetTokenHandler))
141 }
143 // GetAuthorizationCodeHandler presents and processes the page for asking a user to grant access
144 // to their data. See RFC 6749, Section 4.1.
145 func GetAuthorizationCodeHandler(w http.ResponseWriter, r *http.Request, context Context) {
146 session, err := checkCookie(r, context)
147 if err != nil {
148 if err == ErrNoSession || err == ErrInvalidSession {
149 redir := buildLoginRedirect(r, context)
150 if redir == "" {
151 log.Println("No login URL configured.")
152 w.WriteHeader(http.StatusInternalServerError)
153 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
154 "internal_error": template.HTML("Missing login URL."),
155 })
156 return
157 }
158 http.Redirect(w, r, redir, http.StatusFound)
159 return
160 }
161 log.Println(err.Error())
162 w.WriteHeader(http.StatusInternalServerError)
163 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
164 "internal_error": template.HTML(err.Error()),
165 })
166 return
167 }
168 if r.URL.Query().Get("client_id") == "" {
169 w.WriteHeader(http.StatusBadRequest)
170 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
171 "error": template.HTML("Client ID must be specified in the request."),
172 })
173 return
174 }
175 clientID, err := uuid.Parse(r.URL.Query().Get("client_id"))
176 if err != nil {
177 w.WriteHeader(http.StatusBadRequest)
178 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
179 "error": template.HTML("client_id is not a valid Client ID."),
180 })
181 return
182 }
183 redirectURI := r.URL.Query().Get("redirect_uri")
184 client, err := context.GetClient(clientID)
185 if err != nil {
186 if err == ErrClientNotFound {
187 w.WriteHeader(http.StatusBadRequest)
188 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
189 "error": template.HTML("The specified Client couldn’t be found."),
190 })
191 } else {
192 log.Println(err.Error())
193 w.WriteHeader(http.StatusInternalServerError)
194 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
195 "internal_error": template.HTML(err.Error()),
196 })
197 }
198 return
199 }
200 // 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 if checkCSRF(r, session) != nil {
280 log.Println("CSRF attempt detected.")
281 w.WriteHeader(http.StatusInternalServerError)
282 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
283 "error": template.HTML("There was an error authenticating your request."),
284 })
285 return
286 }
287 if r.PostFormValue("grant") == "approved" {
288 var fragment bool
289 switch responseType {
290 case "code":
291 code := uuid.NewID().String()
292 authCode := AuthorizationCode{
293 Code: code,
294 Created: time.Now(),
295 ExpiresIn: defaultAuthorizationCodeExpiration,
296 ClientID: clientID,
297 Scope: scope,
298 RedirectURI: r.URL.Query().Get("redirect_uri"),
299 State: state,
300 ProfileID: session.ProfileID,
301 }
302 err := context.SaveAuthorizationCode(authCode)
303 if err != nil {
304 log.Println("Error saving authorization code:", err)
305 q.Add("error", "server_error")
306 break
307 }
308 q.Add("code", code)
309 case "token":
310 token := Token{
311 AccessToken: uuid.NewID().String(),
312 Created: time.Now(),
313 CreatedFrom: "implicit",
314 ExpiresIn: defaultTokenExpiration,
315 TokenType: "bearer",
316 Scope: scope,
317 ProfileID: session.ProfileID,
318 ClientID: clientID,
319 }
320 err := context.SaveToken(token)
321 if err != nil {
322 log.Println("Error saving token:", err)
323 q.Add("error", "server_error")
324 break
325 }
326 q = url.Values{} // we're not altering the querystring, so don't clone it
327 q.Add("access_token", token.AccessToken)
328 q.Add("token_type", token.TokenType)
329 q.Add("expires_in", strconv.FormatInt(int64(token.ExpiresIn), 10))
330 q.Add("scope", token.Scope)
331 q.Add("state", state) // we wiped out the old values, so we need to set the state again
332 fragment = true
333 }
334 if fragment {
335 redirectURL.Fragment = q.Encode()
336 } else {
337 redirectURL.RawQuery = q.Encode()
338 }
339 http.Redirect(w, r, redirectURL.String(), http.StatusFound)
340 return
341 }
342 q.Add("error", "access_denied")
343 redirectURL.RawQuery = q.Encode()
344 http.Redirect(w, r, redirectURL.String(), http.StatusFound)
345 return
346 }
347 profile, err := context.GetProfileByID(session.ProfileID)
348 if err != nil {
349 log.Println("Error getting profile from session:", err)
350 q.Add("error", "server_error")
351 redirectURL.RawQuery = q.Encode()
352 http.Redirect(w, r, redirectURL.String(), http.StatusFound)
353 return
354 }
355 w.WriteHeader(http.StatusOK)
356 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
357 "client": client,
358 "redirectURL": redirectURL,
359 "scope": scope,
360 "profile": profile,
361 "csrftoken": session.CSRFToken,
362 })
363 }
365 // GetTokenHandler allows a client to exchange an authorization grant for an
366 // access token. See RFC 6749 Section 4.1.3.
367 func GetTokenHandler(w http.ResponseWriter, r *http.Request, context Context) {
368 enc := json.NewEncoder(w)
369 grantType := r.PostFormValue("grant_type")
370 gt, ok := findGrantType(grantType)
371 if !ok {
372 w.WriteHeader(http.StatusBadRequest)
373 renderJSONError(enc, "invalid_request")
374 return
375 }
376 clientID, success := verifyClient(w, r, gt.AllowsPublic, context)
377 if !success {
378 return
379 }
380 scope, profileID, valid := gt.Validate(w, r, context)
381 if !valid {
382 return
383 }
384 refresh := ""
385 if gt.IssuesRefresh {
386 refresh = uuid.NewID().String()
387 }
388 token := Token{
389 AccessToken: uuid.NewID().String(),
390 RefreshToken: refresh,
391 Created: time.Now(),
392 CreatedFrom: gt.AuditString(r),
393 ExpiresIn: defaultTokenExpiration,
394 TokenType: "bearer",
395 Scope: scope,
396 ProfileID: profileID,
397 ClientID: clientID,
398 }
399 err := context.SaveToken(token)
400 if err != nil {
401 w.WriteHeader(http.StatusInternalServerError)
402 renderJSONError(enc, "server_error")
403 return
404 }
405 if gt.ReturnToken(w, r, token, context) && gt.Invalidate != nil {
406 go gt.Invalidate(r, context)
407 }
408 }