auth

Paddy 2015-12-14 Parent:b7e685839a1b

182:cd5f07f9811b Go to Latest

auth/oauth2.go

Update nsq import path. go-nsq has moved to nsqio/go-nsq, so we need to update the import path appropriately.

History
1 package auth
3 import (
4 "crypto/rand"
5 "encoding/hex"
6 "encoding/json"
7 "errors"
8 "html/template"
9 "io"
10 "log"
11 "net/http"
12 "net/url"
13 "strconv"
14 "strings"
15 "sync"
16 "time"
18 "code.secondbit.org/scopes.hg/types"
19 "code.secondbit.org/uuid.hg"
20 "github.com/gorilla/mux"
21 )
23 const (
24 defaultAuthorizationCodeExpiration = 600 // default to ten minute grant expirations
25 getAuthorizationCodeTemplateName = "get_grant"
26 )
28 var (
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{}}
41 )
43 type grantTypes struct {
44 types map[string]GrantType
45 sync.RWMutex
46 }
48 // GrantType defines a set of functions and metadata around a specific authorization grant strategy.
49 //
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.
54 //
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
58 // can be nil.
59 //
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.
62 //
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.
65 //
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.
68 //
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
76 IssuesRefresh bool
77 AllowsPublic bool
78 }
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"`
85 }
87 type errorResponse struct {
88 Error string `json:"error"`
89 Description string `json:"error_description,omitempty"`
90 URI string `json:"error_uri,omitempty"`
91 }
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.
95 //
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) {
99 grantTypesMap.Lock()
100 defer grantTypesMap.Unlock()
101 if _, ok := grantTypesMap.types[name]; ok {
102 panic("Duplicate registration of grant_type " + name)
103 }
104 grantTypesMap.types[name] = g
105 }
107 func findGrantType(name string) (GrantType, bool) {
108 grantTypesMap.RLock()
109 defer grantTypesMap.RUnlock()
110 t, ok := grantTypesMap.types[name]
111 return t, ok
112 }
114 func renderJSONError(enc *json.Encoder, errorType string) {
115 err := enc.Encode(errorResponse{
116 Error: errorType,
117 })
118 if err != nil {
119 log.Println(err)
120 }
121 }
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,
132 }
133 w.Header().Set("Content-Type", "application/json")
134 err := enc.Encode(resp)
135 if err != nil {
136 log.Println(err)
137 return false
138 }
139 return true
140 }
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))
146 }
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)
152 if err != nil {
153 if err == ErrNoSession || err == ErrInvalidSession {
154 redir := buildLoginRedirect(r, context)
155 if redir == "" {
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."),
160 })
161 return
162 }
163 http.Redirect(w, r, redir, http.StatusFound)
164 return
165 }
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()),
170 })
171 return
172 }
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."),
177 })
178 return
179 }
180 clientID, err := uuid.Parse(r.URL.Query().Get("client_id"))
181 if err != nil {
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."),
185 })
186 return
187 }
188 redirectURI := r.URL.Query().Get("redirect_uri")
189 client, err := context.GetClient(clientID)
190 if err != nil {
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."),
195 })
196 } else {
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()),
201 })
202 }
203 return
204 }
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)
210 if err != nil {
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()),
215 })
216 return
217 }
218 var validURI bool
219 if redirectURI != "" {
220 validURI, err = context.CheckEndpoint(clientID, redirectURI)
221 if err != nil {
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."),
226 })
227 return
228 }
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()),
233 })
234 return
235 }
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
239 validURI = true
240 endpoints, err := context.ListEndpoints(clientID, 1, 0)
241 if err != nil {
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()),
246 })
247 return
248 }
249 if len(endpoints) != 1 {
250 validURI = false
251 } else {
252 redirectURI = endpoints[0].URI
253 }
254 } else {
255 validURI = false
256 }
257 if !validURI {
258 w.WriteHeader(http.StatusBadRequest)
259 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
260 "error": template.HTML("The redirect_uri specified is not valid."),
261 })
262 return
263 }
264 redirectURL, err := url.Parse(redirectURI)
265 if err != nil {
266 w.WriteHeader(http.StatusBadRequest)
267 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
268 "error": template.HTML("The redirect_uri specified is not valid."),
269 })
270 return
271 }
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)
280 return
281 }
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)
288 return
289 }
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)
294 return
295 */
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."),
302 })
303 return
304 }
305 if r.PostFormValue("grant") == "approved" {
306 var fragment bool
307 switch responseType {
308 case "code":
309 code := make([]byte, 16)
310 _, err := io.ReadFull(rand.Reader, code)
311 if err != nil {
312 log.Printf("Error generating code: %#+v\n", err)
313 q.Add("error", "server_error")
314 break
315 }
316 authCode := AuthorizationCode{
317 Code: hex.EncodeToString(code),
318 Created: time.Now(),
319 ExpiresIn: defaultAuthorizationCodeExpiration,
320 ClientID: clientID,
321 Scopes: scopes,
322 RedirectURI: r.URL.Query().Get("redirect_uri"),
323 State: state,
324 ProfileID: session.ProfileID,
325 }
326 err = context.SaveAuthorizationCode(authCode)
327 if err != nil {
328 log.Println("Error saving authorization code:", err)
329 q.Add("error", "server_error")
330 break
331 }
332 q.Add("code", authCode.Code)
333 case "token":
334 token := Token{
335 Created: time.Now(),
336 CreatedFrom: "implicit",
337 ExpiresIn: defaultTokenExpiration,
338 TokenType: "bearer",
339 Scopes: scopes,
340 ProfileID: session.ProfileID,
341 ClientID: clientID,
342 }
343 access, err := token.GenerateAccessToken(context.config.JWTPrivateKey)
344 if err != nil {
345 log.Printf("Error signing token: %+v\n", err)
346 q.Add("error", "server_error")
347 break
348 }
349 token.AccessToken = access
350 err = context.SaveToken(token)
351 if err != nil {
352 log.Println("Error saving token:", err)
353 q.Add("error", "server_error")
354 break
355 }
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
362 fragment = true
363 }
364 if fragment {
365 redirectURL.Fragment = q.Encode()
366 } else {
367 redirectURL.RawQuery = q.Encode()
368 }
369 http.Redirect(w, r, redirectURL.String(), http.StatusFound)
370 return
371 }
372 q.Add("error", "access_denied")
373 redirectURL.RawQuery = q.Encode()
374 http.Redirect(w, r, redirectURL.String(), http.StatusFound)
375 return
376 }
377 profile, err := context.GetProfileByID(session.ProfileID)
378 if err != nil {
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)
383 return
384 }
385 w.WriteHeader(http.StatusOK)
386 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
387 "client": client,
388 "redirectURL": redirectURL,
389 "scopes": scopes,
390 "profile": profile,
391 "csrftoken": session.CSRFToken,
392 })
393 }
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)
401 if !ok {
402 w.WriteHeader(http.StatusBadRequest)
403 renderJSONError(enc, "invalid_request")
404 return
405 }
406 clientID, success := verifyClient(w, r, gt.AllowsPublic, context)
407 if !success {
408 return
409 }
410 scopes, profileID, valid := gt.Validate(w, r, context)
411 if !valid {
412 return
413 }
414 refresh := ""
415 if gt.IssuesRefresh {
416 refresh = uuid.NewID().String()
417 }
418 token := Token{
419 AccessToken: uuid.NewID().String(),
420 RefreshToken: refresh,
421 Created: time.Now(),
422 CreatedFrom: gt.AuditString(r),
423 ExpiresIn: defaultTokenExpiration,
424 TokenType: "bearer",
425 Scopes: scopes,
426 ProfileID: profileID,
427 ClientID: clientID,
428 }
429 access, err := token.GenerateAccessToken(context.config.JWTPrivateKey)
430 if err != nil {
431 log.Printf("Error signing token: %+v\n", err)
432 w.WriteHeader(http.StatusInternalServerError)
433 renderJSONError(enc, "server_error")
434 return
435 }
436 token.AccessToken = access
437 err = context.SaveToken(token)
438 if err != nil {
439 w.WriteHeader(http.StatusInternalServerError)
440 renderJSONError(enc, "server_error")
441 return
442 }
443 if gt.ReturnToken(w, r, token, context) && gt.Invalidate != nil {
444 go gt.Invalidate(r, context)
445 }
446 }