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