auth

Paddy 2014-12-06 Parent:8630b108ce35 Child:1dc4e152e3b0

84:4cb65cf90217 Go to Latest

auth/oauth2.go

Start supporting our pluggable grant_type. Define GrantType as a way to bundle information that can be used to validate requests based on their grant_type parameter. Move our validation of the authorization_code grant_type out of GetTokenHandler and into its own function. Define RegisterGrantType as a way to register new grant_type bundles and associate them with the string passed to grant_type. This enables other packages to define RegisterGrantType in their init() functions and plug in new grant types without forking this code. Implement RegisterGrantType for our authorization_code grant type.

History
1 package auth
3 import (
4 "crypto/sha256"
5 "encoding/hex"
6 "encoding/json"
7 "errors"
8 "html/template"
9 "log"
10 "net/http"
11 "net/url"
12 "sync"
13 "time"
15 "code.secondbit.org/pass"
16 "code.secondbit.org/uuid"
18 "github.com/gorilla/mux"
19 )
21 const (
22 authCookieName = "auth"
23 defaultGrantExpiration = 600 // default to ten minute grant expirations
24 getGrantTemplateName = "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 type GrantType struct {
62 Validate func(w http.ResponseWriter, r *http.Request, context Context) (scope string, profileID uuid.ID, valid bool)
63 Invalidate func(r *http.Request, context Context) bool
64 IssuesRefresh bool
65 }
67 type tokenResponse struct {
68 AccessToken string `json:"access_token"`
69 TokenType string `json:"token_type,omitempty"`
70 ExpiresIn int32 `json:"expires_in,omitempty"`
71 RefreshToken string `json:"refresh_token,omitempty"`
72 }
74 type errorResponse struct {
75 Error string `json:"error"`
76 Description string `json:"error_description,omitempty"`
77 URI string `json:"error_uri,omitempty"`
78 }
80 // RegisterGrantType associates a string with a GrantType. When the string is used as the value for "grant_type" when obtaining
81 // an access token, the associated GrantType's properties will be used.
82 //
83 // RegisterGrantType should be called in the `init()` function of packages, much like database/sql registers drivers. It will panic
84 // if a GrantType tries to register under a string that already has a GrantType registered for it.
85 func RegisterGrantType(name string, g GrantType) {
86 grantTypesMap.Lock()
87 defer grantTypesMap.Unlock()
88 if _, ok := grantTypesMap.types[name]; ok {
89 panic("Duplicate registration of grant_type " + name)
90 }
91 grantTypesMap.types[name] = g
92 }
94 func findGrantType(name string) (GrantType, bool) {
95 grantTypesMap.RLock()
96 defer grantTypesMap.RUnlock()
97 t, ok := grantTypesMap.types[name]
98 return t, ok
99 }
101 func renderJSONError(enc *json.Encoder, errorType string) {
102 err := enc.Encode(errorResponse{
103 Error: errorType,
104 })
105 if err != nil {
106 // TODO(paddy): log this or something
107 }
108 }
110 func checkCookie(r *http.Request, context Context) (Session, error) {
111 cookie, err := r.Cookie(authCookieName)
112 if err == http.ErrNoCookie {
113 return Session{}, ErrNoSession
114 } else if err != nil {
115 log.Println(err)
116 return Session{}, err
117 }
118 sess, err := context.GetSession(cookie.Value)
119 if err == ErrSessionNotFound {
120 return Session{}, ErrInvalidSession
121 } else if err != nil {
122 return Session{}, err
123 }
124 if !sess.Active {
125 return Session{}, ErrInvalidSession
126 }
127 return sess, nil
128 }
130 func buildLoginRedirect(r *http.Request, context Context) string {
131 if context.loginURI == nil {
132 return ""
133 }
134 uri := *context.loginURI
135 q := uri.Query()
136 q.Set("from", r.URL.String())
137 uri.RawQuery = q.Encode()
138 return uri.String()
139 }
141 func authenticate(user, passphrase string, context Context) (Profile, error) {
142 profile, err := context.GetProfileByLogin(user)
143 if err != nil {
144 if err == ErrProfileNotFound || err == ErrLoginNotFound {
145 return Profile{}, ErrIncorrectAuth
146 }
147 return Profile{}, err
148 }
149 switch profile.PassphraseScheme {
150 case 1:
151 realPass, err := hex.DecodeString(profile.Passphrase)
152 if err != nil {
153 return Profile{}, err
154 }
155 candidate := pass.Check(sha256.New, profile.Iterations, []byte(passphrase), []byte(profile.Salt))
156 if !pass.Compare(candidate, realPass) {
157 return Profile{}, ErrIncorrectAuth
158 }
159 default:
160 return Profile{}, ErrInvalidPassphraseScheme
161 }
162 return profile, nil
163 }
165 func wrap(context Context, f func(w http.ResponseWriter, r *http.Request, context Context)) http.Handler {
166 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
167 f(w, r, context)
168 })
169 }
171 // RegisterOAuth2 adds handlers to the passed router to handle the OAuth2 endpoints.
172 func RegisterOAuth2(r *mux.Router, context Context) {
173 r.Handle("/authorize", wrap(context, GetGrantHandler))
174 r.Handle("/token", wrap(context, GetTokenHandler))
175 }
177 // GetGrantHandler presents and processes the page for asking a user to grant access
178 // to their data. See RFC 6749, Section 4.1.
179 func GetGrantHandler(w http.ResponseWriter, r *http.Request, context Context) {
180 session, err := checkCookie(r, context)
181 if err != nil {
182 if err == ErrNoSession || err == ErrInvalidSession {
183 redir := buildLoginRedirect(r, context)
184 if redir == "" {
185 log.Println("No login URL configured.")
186 w.WriteHeader(http.StatusInternalServerError)
187 context.Render(w, getGrantTemplateName, map[string]interface{}{
188 "internal_error": template.HTML("Missing login URL."),
189 })
190 return
191 }
192 http.Redirect(w, r, redir, http.StatusFound)
193 return
194 }
195 log.Println(err.Error())
196 w.WriteHeader(http.StatusInternalServerError)
197 context.Render(w, getGrantTemplateName, map[string]interface{}{
198 "internal_error": template.HTML(err.Error()),
199 })
200 return
201 }
202 if r.URL.Query().Get("client_id") == "" {
203 w.WriteHeader(http.StatusBadRequest)
204 context.Render(w, getGrantTemplateName, map[string]interface{}{
205 "error": template.HTML("Client ID must be specified in the request."),
206 })
207 return
208 }
209 clientID, err := uuid.Parse(r.URL.Query().Get("client_id"))
210 if err != nil {
211 w.WriteHeader(http.StatusBadRequest)
212 context.Render(w, getGrantTemplateName, map[string]interface{}{
213 "error": template.HTML("client_id is not a valid Client ID."),
214 })
215 return
216 }
217 redirectURI := r.URL.Query().Get("redirect_uri")
218 redirectURL, err := url.Parse(redirectURI)
219 if err != nil {
220 w.WriteHeader(http.StatusBadRequest)
221 context.Render(w, getGrantTemplateName, map[string]interface{}{
222 "error": template.HTML("The redirect_uri specified is not valid."),
223 })
224 return
225 }
226 client, err := context.GetClient(clientID)
227 if err != nil {
228 if err == ErrClientNotFound {
229 w.WriteHeader(http.StatusBadRequest)
230 context.Render(w, getGrantTemplateName, map[string]interface{}{
231 "error": template.HTML("The specified Client couldn’t be found."),
232 })
233 } else {
234 log.Println(err.Error())
235 w.WriteHeader(http.StatusInternalServerError)
236 context.Render(w, getGrantTemplateName, map[string]interface{}{
237 "internal_error": template.HTML(err.Error()),
238 })
239 }
240 return
241 }
242 // whether a redirect URI is valid or not depends on the number of endpoints
243 // the client has registered
244 numEndpoints, err := context.CountEndpoints(clientID)
245 if err != nil {
246 log.Println(err.Error())
247 w.WriteHeader(http.StatusInternalServerError)
248 context.Render(w, getGrantTemplateName, map[string]interface{}{
249 "internal_error": template.HTML(err.Error()),
250 })
251 return
252 }
253 var validURI bool
254 if redirectURI != "" {
255 // BUG(paddy): We really should normalize URIs before trying to compare them.
256 validURI, err = context.CheckEndpoint(clientID, redirectURI)
257 if err != nil {
258 log.Println(err.Error())
259 w.WriteHeader(http.StatusInternalServerError)
260 context.Render(w, getGrantTemplateName, map[string]interface{}{
261 "internal_error": template.HTML(err.Error()),
262 })
263 return
264 }
265 } else if redirectURI == "" && numEndpoints == 1 {
266 // if we don't specify the endpoint and there's only one endpoint, the
267 // request is valid, and we're redirecting to that one endpoint
268 validURI = true
269 endpoints, err := context.ListEndpoints(clientID, 1, 0)
270 if err != nil {
271 log.Println(err.Error())
272 w.WriteHeader(http.StatusInternalServerError)
273 context.Render(w, getGrantTemplateName, map[string]interface{}{
274 "internal_error": template.HTML(err.Error()),
275 })
276 return
277 }
278 if len(endpoints) != 1 {
279 validURI = false
280 } else {
281 u := endpoints[0].URI // Copy it here to avoid grabbing a pointer to the memstore
282 redirectURI = u.String()
283 redirectURL = &u
284 }
285 } else {
286 validURI = false
287 }
288 if !validURI {
289 w.WriteHeader(http.StatusBadRequest)
290 context.Render(w, getGrantTemplateName, map[string]interface{}{
291 "error": template.HTML("The redirect_uri specified is not valid."),
292 })
293 return
294 }
295 scope := r.URL.Query().Get("scope")
296 state := r.URL.Query().Get("state")
297 if r.URL.Query().Get("response_type") != "code" {
298 q := redirectURL.Query()
299 q.Add("error", "invalid_request")
300 q.Add("state", state)
301 redirectURL.RawQuery = q.Encode()
302 http.Redirect(w, r, redirectURL.String(), http.StatusFound)
303 return
304 }
305 if r.Method == "POST" {
306 // BUG(paddy): We need to implement CSRF protection when obtaining a grant code.
307 if r.PostFormValue("grant") == "approved" {
308 code := uuid.NewID().String()
309 grant := Grant{
310 Code: code,
311 Created: time.Now(),
312 ExpiresIn: defaultGrantExpiration,
313 ClientID: clientID,
314 Scope: scope,
315 RedirectURI: r.URL.Query().Get("redirect_uri"),
316 State: state,
317 ProfileID: session.ProfileID,
318 }
319 err := context.SaveGrant(grant)
320 if err != nil {
321 q := redirectURL.Query()
322 q.Add("error", "server_error")
323 q.Add("state", state)
324 redirectURL.RawQuery = q.Encode()
325 http.Redirect(w, r, redirectURL.String(), http.StatusFound)
326 return
327 }
328 q := redirectURL.Query()
329 q.Add("code", code)
330 q.Add("state", state)
331 redirectURL.RawQuery = q.Encode()
332 http.Redirect(w, r, redirectURL.String(), http.StatusFound)
333 return
334 }
335 q := redirectURL.Query()
336 q.Add("error", "access_denied")
337 q.Add("state", state)
338 redirectURL.RawQuery = q.Encode()
339 http.Redirect(w, r, redirectURL.String(), http.StatusFound)
340 return
341 }
342 w.WriteHeader(http.StatusOK)
343 context.Render(w, getGrantTemplateName, map[string]interface{}{
344 "client": client,
345 })
346 }
348 // GetTokenHandler allows a client to exchange an authorization grant for an
349 // access token. See RFC 6749 Section 4.1.3.
350 func GetTokenHandler(w http.ResponseWriter, r *http.Request, context Context) {
351 enc := json.NewEncoder(w)
352 grantType := r.PostFormValue("grant_type")
353 gt, ok := findGrantType(grantType)
354 if !ok {
355 w.WriteHeader(http.StatusBadRequest)
356 renderJSONError(enc, "invalid_request")
357 return
358 }
359 scope, profileID, valid := gt.Validate(w, r, context)
360 if !valid {
361 return
362 }
363 refresh := ""
364 if gt.IssuesRefresh {
365 refresh = uuid.NewID().String()
366 }
367 token := Token{
368 AccessToken: uuid.NewID().String(),
369 RefreshToken: refresh,
370 Created: time.Now(),
371 ExpiresIn: defaultTokenExpiration,
372 TokenType: "bearer",
373 Scope: scope,
374 ProfileID: profileID,
375 }
376 err := context.SaveToken(token)
377 if err != nil {
378 w.WriteHeader(http.StatusInternalServerError)
379 renderJSONError(enc, "server_error")
380 return
381 }
382 resp := tokenResponse{
383 AccessToken: token.AccessToken,
384 RefreshToken: token.RefreshToken,
385 ExpiresIn: token.ExpiresIn,
386 TokenType: token.TokenType,
387 }
388 err = enc.Encode(resp)
389 if err != nil {
390 // TODO(paddy): log this or something
391 return
392 }
393 // BUG(paddy): we need to invalidate the grant for future requests
394 }
396 // TODO(paddy): exchange user credentials for access token
397 // TODO(paddy): exchange client credentials for access token
398 // TODO(paddy): implicit grant for access token
399 // TODO(paddy): exchange refresh token for access token