auth

Paddy 2015-04-11 Parent:3e8964a914ef Child:73e12d5a1124

157:202e991accc2 Go to Latest

auth/oauth2.go

Wire up the postgres database for authd. Have authd use the AUTH_PG_DB environment variable to detect support for the postgres *Stores, and if postgres is supported, use it. If postgres isn't supported, fall back on the in-memory store. Also create-if-not-exists the test scopes, instead of panicking when the scope already exists.

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/uuid.hg"
19 "github.com/gorilla/mux"
20 )
22 const (
23 defaultAuthorizationCodeExpiration = 600 // default to ten minute grant expirations
24 getAuthorizationCodeTemplateName = "get_grant"
25 )
27 var (
28 // ErrNoAuth is returned when an Authorization header is not present or is empty.
29 ErrNoAuth = errors.New("no authorization header supplied")
30 // ErrInvalidAuthFormat is returned when an Authorization header is present but not the correct format.
31 ErrInvalidAuthFormat = errors.New("authorization header is not in a valid format")
32 // ErrIncorrectAuth is returned when a user authentication attempt does not match the stored values.
33 ErrIncorrectAuth = errors.New("invalid authentication")
34 // ErrInvalidPassphraseScheme is returned when an undefined passphrase scheme is used.
35 ErrInvalidPassphraseScheme = errors.New("invalid passphrase scheme")
36 // ErrNoSession is returned when no session ID is passed with a request.
37 ErrNoSession = errors.New("no session ID found")
39 grantTypesMap = grantTypes{types: map[string]GrantType{}}
40 )
42 type grantTypes struct {
43 types map[string]GrantType
44 sync.RWMutex
45 }
47 // GrantType defines a set of functions and metadata around a specific authorization grant strategy.
48 //
49 // The Validate function will be called when requests are made that match the GrantType, and should write any
50 // errors to the ResponseWriter. It is responsible for determining if the grant is valid and a token should be issued.
51 // 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
52 // is valid or not. It must not be nil.
53 //
54 // The Invalidate function will be called when the grant has successfully generated a token and the token has successfully
55 // been conveyed to the user. The Invalidate function is always called asynchronously, outside the request. It should take
56 // care of marking the grant as used, if the GrantType requires grants to be one-time only grants. The Invalidate function
57 // can be nil.
58 //
59 // IssuesRefresh determines whether the GrantType should yield a refresh token as well as an access token. If true, the client
60 // will be issued a refresh token.
61 //
62 // AllowsPublic determines whether the GrantType should allow public clients to use that grant. If true, clients without
63 // credentials will be able to use the grant to obtain a token.
64 //
65 // AuditString should return the string that will be saved in the resulting Token's CreatedFrom field, as an audit log of how
66 // the Token was authorized.
67 //
68 // The ReturnToken will be called when a token is created and needs to be returned to the client. If it returns true, the token
69 // was successfully returned and the Invalidate function will be called asynchronously.
70 type GrantType struct {
71 Validate func(w http.ResponseWriter, r *http.Request, context Context) (scopes []string, profileID uuid.ID, valid bool)
72 Invalidate func(r *http.Request, context Context) error
73 ReturnToken func(w http.ResponseWriter, r *http.Request, token Token, context Context) bool
74 AuditString func(r *http.Request) string
75 IssuesRefresh bool
76 AllowsPublic bool
77 }
79 type tokenResponse struct {
80 AccessToken string `json:"access_token"`
81 TokenType string `json:"token_type,omitempty"`
82 ExpiresIn int32 `json:"expires_in,omitempty"`
83 RefreshToken string `json:"refresh_token,omitempty"`
84 }
86 type errorResponse struct {
87 Error string `json:"error"`
88 Description string `json:"error_description,omitempty"`
89 URI string `json:"error_uri,omitempty"`
90 }
92 // RegisterGrantType associates a string with a GrantType. When the string is used as the value for "grant_type" when obtaining
93 // an access token, the associated GrantType's properties will be used.
94 //
95 // RegisterGrantType should be called in the `init()` function of packages, much like database/sql registers drivers. It will panic
96 // if a GrantType tries to register under a string that already has a GrantType registered for it.
97 func RegisterGrantType(name string, g GrantType) {
98 grantTypesMap.Lock()
99 defer grantTypesMap.Unlock()
100 if _, ok := grantTypesMap.types[name]; ok {
101 panic("Duplicate registration of grant_type " + name)
102 }
103 grantTypesMap.types[name] = g
104 }
106 func findGrantType(name string) (GrantType, bool) {
107 grantTypesMap.RLock()
108 defer grantTypesMap.RUnlock()
109 t, ok := grantTypesMap.types[name]
110 return t, ok
111 }
113 func renderJSONError(enc *json.Encoder, errorType string) {
114 err := enc.Encode(errorResponse{
115 Error: errorType,
116 })
117 if err != nil {
118 log.Println(err)
119 }
120 }
122 // RenderJSONToken is an implementation of the ReturnToken function for GrantTypes. It returns the token using JSON
123 // according to the spec. See RFC 6479, Section 4.1.4.
124 func RenderJSONToken(w http.ResponseWriter, r *http.Request, token Token, context Context) bool {
125 enc := json.NewEncoder(w)
126 resp := tokenResponse{
127 AccessToken: token.AccessToken,
128 RefreshToken: token.RefreshToken,
129 ExpiresIn: token.ExpiresIn,
130 TokenType: token.TokenType,
131 }
132 w.Header().Set("Content-Type", "application/json")
133 err := enc.Encode(resp)
134 if err != nil {
135 log.Println(err)
136 return false
137 }
138 return true
139 }
141 // RegisterOAuth2 adds handlers to the passed router to handle the OAuth2 endpoints.
142 func RegisterOAuth2(r *mux.Router, context Context) {
143 r.Handle("/authorize", wrap(context, GetAuthorizationCodeHandler))
144 r.Handle("/token", wrap(context, GetTokenHandler))
145 }
147 // GetAuthorizationCodeHandler presents and processes the page for asking a user to grant access
148 // to their data. See RFC 6749, Section 4.1.
149 func GetAuthorizationCodeHandler(w http.ResponseWriter, r *http.Request, context Context) {
150 session, err := checkCookie(r, context)
151 if err != nil {
152 if err == ErrNoSession || err == ErrInvalidSession {
153 redir := buildLoginRedirect(r, context)
154 if redir == "" {
155 log.Println("No login URL configured.")
156 w.WriteHeader(http.StatusInternalServerError)
157 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
158 "internal_error": template.HTML("Missing login URL."),
159 })
160 return
161 }
162 http.Redirect(w, r, redir, http.StatusFound)
163 return
164 }
165 log.Println(err.Error())
166 w.WriteHeader(http.StatusInternalServerError)
167 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
168 "internal_error": template.HTML(err.Error()),
169 })
170 return
171 }
172 if r.URL.Query().Get("client_id") == "" {
173 w.WriteHeader(http.StatusBadRequest)
174 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
175 "error": template.HTML("Client ID must be specified in the request."),
176 })
177 return
178 }
179 clientID, err := uuid.Parse(r.URL.Query().Get("client_id"))
180 if err != nil {
181 w.WriteHeader(http.StatusBadRequest)
182 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
183 "error": template.HTML("client_id is not a valid Client ID."),
184 })
185 return
186 }
187 redirectURI := r.URL.Query().Get("redirect_uri")
188 client, err := context.GetClient(clientID)
189 if err != nil {
190 if err == ErrClientNotFound {
191 w.WriteHeader(http.StatusBadRequest)
192 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
193 "error": template.HTML("The specified Client couldn’t be found."),
194 })
195 } else {
196 log.Println(err.Error())
197 w.WriteHeader(http.StatusInternalServerError)
198 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
199 "internal_error": template.HTML(err.Error()),
200 })
201 }
202 return
203 }
204 // BUG(paddy): Checking if the redirect URI is valid should be a helper function.
206 // whether a redirect URI is valid or not depends on the number of endpoints
207 // the client has registered
208 numEndpoints, err := context.CountEndpoints(clientID)
209 if err != nil {
210 log.Println(err.Error())
211 w.WriteHeader(http.StatusInternalServerError)
212 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
213 "internal_error": template.HTML(err.Error()),
214 })
215 return
216 }
217 var validURI bool
218 if redirectURI != "" {
219 validURI, err = context.CheckEndpoint(clientID, redirectURI)
220 if err != nil {
221 if err == ErrEndpointURINotURL {
222 w.WriteHeader(http.StatusBadRequest)
223 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
224 "error": template.HTML("The redirect_uri specified is not valid."),
225 })
226 return
227 }
228 log.Println(err.Error())
229 w.WriteHeader(http.StatusInternalServerError)
230 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
231 "internal_error": template.HTML(err.Error()),
232 })
233 return
234 }
235 } else if redirectURI == "" && numEndpoints == 1 {
236 // if we don't specify the endpoint and there's only one endpoint, the
237 // request is valid, and we're redirecting to that one endpoint
238 validURI = true
239 endpoints, err := context.ListEndpoints(clientID, 1, 0)
240 if err != nil {
241 log.Println(err.Error())
242 w.WriteHeader(http.StatusInternalServerError)
243 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
244 "internal_error": template.HTML(err.Error()),
245 })
246 return
247 }
248 if len(endpoints) != 1 {
249 validURI = false
250 } else {
251 redirectURI = endpoints[0].URI
252 }
253 } else {
254 validURI = false
255 }
256 if !validURI {
257 w.WriteHeader(http.StatusBadRequest)
258 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
259 "error": template.HTML("The redirect_uri specified is not valid."),
260 })
261 return
262 }
263 redirectURL, err := url.Parse(redirectURI)
264 if err != nil {
265 w.WriteHeader(http.StatusBadRequest)
266 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
267 "error": template.HTML("The redirect_uri specified is not valid."),
268 })
269 return
270 }
271 state := r.URL.Query().Get("state")
272 responseType := r.URL.Query().Get("response_type")
273 q := redirectURL.Query()
274 q.Add("state", state)
275 if responseType != "code" && responseType != "token" {
276 q.Add("error", "invalid_request")
277 redirectURL.RawQuery = q.Encode()
278 http.Redirect(w, r, redirectURL.String(), http.StatusFound)
279 return
280 }
281 scopeParams := strings.Split(r.URL.Query().Get("scope"), " ")
282 scopes, err := context.GetScopes(scopeParams)
283 if err != nil {
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: scopeParams,
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 AccessToken: uuid.NewID().String(),
336 Created: time.Now(),
337 CreatedFrom: "implicit",
338 ExpiresIn: defaultTokenExpiration,
339 TokenType: "bearer",
340 Scopes: scopeParams,
341 ProfileID: session.ProfileID,
342 ClientID: clientID,
343 }
344 err := context.SaveToken(token)
345 if err != nil {
346 log.Println("Error saving token:", err)
347 q.Add("error", "server_error")
348 break
349 }
350 q = url.Values{} // we're not altering the querystring, so don't clone it
351 q.Add("access_token", token.AccessToken)
352 q.Add("token_type", token.TokenType)
353 q.Add("expires_in", strconv.FormatInt(int64(token.ExpiresIn), 10))
354 q.Add("scope", strings.Join(token.Scopes, " "))
355 q.Add("state", state) // we wiped out the old values, so we need to set the state again
356 fragment = true
357 }
358 if fragment {
359 redirectURL.Fragment = q.Encode()
360 } else {
361 redirectURL.RawQuery = q.Encode()
362 }
363 http.Redirect(w, r, redirectURL.String(), http.StatusFound)
364 return
365 }
366 q.Add("error", "access_denied")
367 redirectURL.RawQuery = q.Encode()
368 http.Redirect(w, r, redirectURL.String(), http.StatusFound)
369 return
370 }
371 profile, err := context.GetProfileByID(session.ProfileID)
372 if err != nil {
373 log.Println("Error getting profile from session:", err)
374 q.Add("error", "server_error")
375 redirectURL.RawQuery = q.Encode()
376 http.Redirect(w, r, redirectURL.String(), http.StatusFound)
377 return
378 }
379 w.WriteHeader(http.StatusOK)
380 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
381 "client": client,
382 "redirectURL": redirectURL,
383 "scopes": scopes,
384 "profile": profile,
385 "csrftoken": session.CSRFToken,
386 })
387 }
389 // GetTokenHandler allows a client to exchange an authorization grant for an
390 // access token. See RFC 6749 Section 4.1.3.
391 func GetTokenHandler(w http.ResponseWriter, r *http.Request, context Context) {
392 enc := json.NewEncoder(w)
393 grantType := r.PostFormValue("grant_type")
394 gt, ok := findGrantType(grantType)
395 if !ok {
396 w.WriteHeader(http.StatusBadRequest)
397 renderJSONError(enc, "invalid_request")
398 return
399 }
400 clientID, success := verifyClient(w, r, gt.AllowsPublic, context)
401 if !success {
402 return
403 }
404 scopes, profileID, valid := gt.Validate(w, r, context)
405 if !valid {
406 return
407 }
408 refresh := ""
409 if gt.IssuesRefresh {
410 refresh = uuid.NewID().String()
411 }
412 token := Token{
413 AccessToken: uuid.NewID().String(),
414 RefreshToken: refresh,
415 Created: time.Now(),
416 CreatedFrom: gt.AuditString(r),
417 ExpiresIn: defaultTokenExpiration,
418 TokenType: "bearer",
419 Scopes: scopes,
420 ProfileID: profileID,
421 ClientID: clientID,
422 }
423 err := context.SaveToken(token)
424 if err != nil {
425 w.WriteHeader(http.StatusInternalServerError)
426 renderJSONError(enc, "server_error")
427 return
428 }
429 if gt.ReturnToken(w, r, token, context) && gt.Invalidate != nil {
430 go gt.Invalidate(r, context)
431 }
432 }