auth

Paddy 2014-12-06 Parent:11ad5eca2f82 Child:8630b108ce35

82:0a6e3f14b054 Go to Latest

auth/oauth2.go

Fix go vet, fix imports, render JSON errors, deprecate getBasicAuth. Fix logging functions in our test file that were causing go vet to report errors (and which would have obscured the test output). Move some imports around to make the imports more consistent and our pre-commit hook happy. Create a helper to render JSON errors, and actually render those errors when obtaining a token using a grant. Deprecate our custom getBasicAuth in favour of the new BasicAuth() method on net/http.*Request objects, which was introduced in Go 1.4 (meaning Go 1.4 is now a requirement for compiling this.)

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 "time"
14 "code.secondbit.org/pass"
15 "code.secondbit.org/uuid"
17 "github.com/gorilla/mux"
18 )
20 const (
21 authCookieName = "auth"
22 defaultGrantExpiration = 600 // default to ten minute grant expirations
23 getGrantTemplateName = "get_grant"
24 )
26 var (
27 // ErrNoAuth is returned when an Authorization header is not present or is empty.
28 ErrNoAuth = errors.New("no authorization header supplied")
29 // ErrInvalidAuthFormat is returned when an Authorization header is present but not the correct format.
30 ErrInvalidAuthFormat = errors.New("authorization header is not in a valid format")
31 // ErrIncorrectAuth is returned when a user authentication attempt does not match the stored values.
32 ErrIncorrectAuth = errors.New("invalid authentication")
33 // ErrInvalidPassphraseScheme is returned when an undefined passphrase scheme is used.
34 ErrInvalidPassphraseScheme = errors.New("invalid passphrase scheme")
35 // ErrNoSession is returned when no session ID is passed with a request.
36 ErrNoSession = errors.New("no session ID found")
37 )
39 type tokenResponse struct {
40 AccessToken string `json:"access_token"`
41 TokenType string `json:"token_type,omitempty"`
42 ExpiresIn int32 `json:"expires_in,omitempty"`
43 RefreshToken string `json:"refresh_token,omitempty"`
44 }
46 type errorResponse struct {
47 Error string `json:"error"`
48 Description string `json:"error_description,omitempty"`
49 URI string `json:"error_uri,omitempty"`
50 }
52 func renderJSONError(enc *json.Encoder, errorType string) {
53 err := enc.Encode(errorResponse{
54 Error: errorType,
55 })
56 if err != nil {
57 // TODO(paddy): log this or something
58 }
59 }
61 func checkCookie(r *http.Request, context Context) (Session, error) {
62 cookie, err := r.Cookie(authCookieName)
63 if err == http.ErrNoCookie {
64 return Session{}, ErrNoSession
65 } else if err != nil {
66 log.Println(err)
67 return Session{}, err
68 }
69 sess, err := context.GetSession(cookie.Value)
70 if err == ErrSessionNotFound {
71 return Session{}, ErrInvalidSession
72 } else if err != nil {
73 return Session{}, err
74 }
75 if !sess.Active {
76 return Session{}, ErrInvalidSession
77 }
78 return sess, nil
79 }
81 func buildLoginRedirect(r *http.Request, context Context) string {
82 if context.loginURI == nil {
83 return ""
84 }
85 uri := *context.loginURI
86 q := uri.Query()
87 q.Set("from", r.URL.String())
88 uri.RawQuery = q.Encode()
89 return uri.String()
90 }
92 func authenticate(user, passphrase string, context Context) (Profile, error) {
93 profile, err := context.GetProfileByLogin(user)
94 if err != nil {
95 if err == ErrProfileNotFound || err == ErrLoginNotFound {
96 return Profile{}, ErrIncorrectAuth
97 }
98 return Profile{}, err
99 }
100 switch profile.PassphraseScheme {
101 case 1:
102 realPass, err := hex.DecodeString(profile.Passphrase)
103 if err != nil {
104 return Profile{}, err
105 }
106 candidate := pass.Check(sha256.New, profile.Iterations, []byte(passphrase), []byte(profile.Salt))
107 if !pass.Compare(candidate, realPass) {
108 return Profile{}, ErrIncorrectAuth
109 }
110 default:
111 return Profile{}, ErrInvalidPassphraseScheme
112 }
113 return profile, nil
114 }
116 func wrap(context Context, f func(w http.ResponseWriter, r *http.Request, context Context)) http.Handler {
117 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
118 f(w, r, context)
119 })
120 }
122 // RegisterOAuth2 adds handlers to the passed router to handle the OAuth2 endpoints.
123 func RegisterOAuth2(r *mux.Router, context Context) {
124 r.Handle("/authorize", wrap(context, GetGrantHandler))
125 r.Handle("/token", wrap(context, GetTokenHandler))
126 }
128 // GetGrantHandler presents and processes the page for asking a user to grant access
129 // to their data. See RFC 6749, Section 4.1.
130 func GetGrantHandler(w http.ResponseWriter, r *http.Request, context Context) {
131 session, err := checkCookie(r, context)
132 if err != nil {
133 if err == ErrNoSession || err == ErrInvalidSession {
134 redir := buildLoginRedirect(r, context)
135 if redir == "" {
136 log.Println("No login URL configured.")
137 w.WriteHeader(http.StatusInternalServerError)
138 context.Render(w, getGrantTemplateName, map[string]interface{}{
139 "internal_error": template.HTML("Missing login URL."),
140 })
141 return
142 }
143 http.Redirect(w, r, redir, http.StatusFound)
144 return
145 }
146 log.Println(err.Error())
147 w.WriteHeader(http.StatusInternalServerError)
148 context.Render(w, getGrantTemplateName, map[string]interface{}{
149 "internal_error": template.HTML(err.Error()),
150 })
151 return
152 }
153 if r.URL.Query().Get("client_id") == "" {
154 w.WriteHeader(http.StatusBadRequest)
155 context.Render(w, getGrantTemplateName, map[string]interface{}{
156 "error": template.HTML("Client ID must be specified in the request."),
157 })
158 return
159 }
160 clientID, err := uuid.Parse(r.URL.Query().Get("client_id"))
161 if err != nil {
162 w.WriteHeader(http.StatusBadRequest)
163 context.Render(w, getGrantTemplateName, map[string]interface{}{
164 "error": template.HTML("client_id is not a valid Client ID."),
165 })
166 return
167 }
168 redirectURI := r.URL.Query().Get("redirect_uri")
169 redirectURL, err := url.Parse(redirectURI)
170 if err != nil {
171 w.WriteHeader(http.StatusBadRequest)
172 context.Render(w, getGrantTemplateName, map[string]interface{}{
173 "error": template.HTML("The redirect_uri specified is not valid."),
174 })
175 return
176 }
177 client, err := context.GetClient(clientID)
178 if err != nil {
179 if err == ErrClientNotFound {
180 w.WriteHeader(http.StatusBadRequest)
181 context.Render(w, getGrantTemplateName, map[string]interface{}{
182 "error": template.HTML("The specified Client couldn’t be found."),
183 })
184 } else {
185 log.Println(err.Error())
186 w.WriteHeader(http.StatusInternalServerError)
187 context.Render(w, getGrantTemplateName, map[string]interface{}{
188 "internal_error": template.HTML(err.Error()),
189 })
190 }
191 return
192 }
193 // whether a redirect URI is valid or not depends on the number of endpoints
194 // the client has registered
195 numEndpoints, err := context.CountEndpoints(clientID)
196 if err != nil {
197 log.Println(err.Error())
198 w.WriteHeader(http.StatusInternalServerError)
199 context.Render(w, getGrantTemplateName, map[string]interface{}{
200 "internal_error": template.HTML(err.Error()),
201 })
202 return
203 }
204 var validURI bool
205 if redirectURI != "" {
206 // BUG(paddy): We really should normalize URIs before trying to compare them.
207 validURI, err = context.CheckEndpoint(clientID, redirectURI)
208 if err != nil {
209 log.Println(err.Error())
210 w.WriteHeader(http.StatusInternalServerError)
211 context.Render(w, getGrantTemplateName, map[string]interface{}{
212 "internal_error": template.HTML(err.Error()),
213 })
214 return
215 }
216 } else if redirectURI == "" && numEndpoints == 1 {
217 // if we don't specify the endpoint and there's only one endpoint, the
218 // request is valid, and we're redirecting to that one endpoint
219 validURI = true
220 endpoints, err := context.ListEndpoints(clientID, 1, 0)
221 if err != nil {
222 log.Println(err.Error())
223 w.WriteHeader(http.StatusInternalServerError)
224 context.Render(w, getGrantTemplateName, map[string]interface{}{
225 "internal_error": template.HTML(err.Error()),
226 })
227 return
228 }
229 if len(endpoints) != 1 {
230 validURI = false
231 } else {
232 u := endpoints[0].URI // Copy it here to avoid grabbing a pointer to the memstore
233 redirectURI = u.String()
234 redirectURL = &u
235 }
236 } else {
237 validURI = false
238 }
239 if !validURI {
240 w.WriteHeader(http.StatusBadRequest)
241 context.Render(w, getGrantTemplateName, map[string]interface{}{
242 "error": template.HTML("The redirect_uri specified is not valid."),
243 })
244 return
245 }
246 scope := r.URL.Query().Get("scope")
247 state := r.URL.Query().Get("state")
248 if r.URL.Query().Get("response_type") != "code" {
249 q := redirectURL.Query()
250 q.Add("error", "invalid_request")
251 q.Add("state", state)
252 redirectURL.RawQuery = q.Encode()
253 http.Redirect(w, r, redirectURL.String(), http.StatusFound)
254 return
255 }
256 if r.Method == "POST" {
257 // BUG(paddy): We need to implement CSRF protection when obtaining a grant code.
258 if r.PostFormValue("grant") == "approved" {
259 code := uuid.NewID().String()
260 grant := Grant{
261 Code: code,
262 Created: time.Now(),
263 ExpiresIn: defaultGrantExpiration,
264 ClientID: clientID,
265 Scope: scope,
266 RedirectURI: r.URL.Query().Get("redirect_uri"),
267 State: state,
268 ProfileID: session.ProfileID,
269 }
270 err := context.SaveGrant(grant)
271 if err != nil {
272 q := redirectURL.Query()
273 q.Add("error", "server_error")
274 q.Add("state", state)
275 redirectURL.RawQuery = q.Encode()
276 http.Redirect(w, r, redirectURL.String(), http.StatusFound)
277 return
278 }
279 q := redirectURL.Query()
280 q.Add("code", code)
281 q.Add("state", state)
282 redirectURL.RawQuery = q.Encode()
283 http.Redirect(w, r, redirectURL.String(), http.StatusFound)
284 return
285 }
286 q := redirectURL.Query()
287 q.Add("error", "access_denied")
288 q.Add("state", state)
289 redirectURL.RawQuery = q.Encode()
290 http.Redirect(w, r, redirectURL.String(), http.StatusFound)
291 return
292 }
293 w.WriteHeader(http.StatusOK)
294 context.Render(w, getGrantTemplateName, map[string]interface{}{
295 "client": client,
296 })
297 }
299 // GetTokenHandler allows a client to exchange an authorization grant for an
300 // access token. See RFC 6749 Section 4.1.3.
301 func GetTokenHandler(w http.ResponseWriter, r *http.Request, context Context) {
302 enc := json.NewEncoder(w)
303 grantType := r.PostFormValue("grant_type")
304 if grantType != "authorization_code" {
305 w.WriteHeader(http.StatusBadRequest)
306 renderJSONError(enc, "invalid_request")
307 return
308 }
309 code := r.PostFormValue("code")
310 if code == "" {
311 w.WriteHeader(http.StatusBadRequest)
312 renderJSONError(enc, "invalid_request")
313 return
314 }
315 redirectURI := r.PostFormValue("redirect_uri")
316 clientIDStr, clientSecret, fromAuthHeader := r.BasicAuth()
317 if !fromAuthHeader {
318 clientIDStr = r.PostFormValue("client_id")
319 }
320 clientID, err := uuid.Parse(clientIDStr)
321 if err != nil {
322 w.WriteHeader(http.StatusUnauthorized)
323 if fromAuthHeader {
324 w.Header().Set("WWW-Authenticate", "Basic")
325 }
326 renderJSONError(enc, "invalid_client")
327 return
328 }
329 client, err := context.GetClient(clientID)
330 if err != nil {
331 if err == ErrClientNotFound {
332 w.WriteHeader(http.StatusUnauthorized)
333 renderJSONError(enc, "invalid_client")
334 } else {
335 w.WriteHeader(http.StatusInternalServerError)
336 renderJSONError(enc, "server_error")
337 }
338 return
339 }
340 if client.Secret != clientSecret {
341 w.WriteHeader(http.StatusUnauthorized)
342 if fromAuthHeader {
343 w.Header().Set("WWW-Authenticate", "Basic")
344 }
345 renderJSONError(enc, "invalid_client")
346 return
347 }
348 grant, err := context.GetGrant(code)
349 if err != nil {
350 if err == ErrGrantNotFound {
351 w.WriteHeader(http.StatusBadRequest)
352 renderJSONError(enc, "invalid_grant")
353 return
354 }
355 w.WriteHeader(http.StatusInternalServerError)
356 renderJSONError(enc, "server_error")
357 return
358 }
359 if grant.RedirectURI != redirectURI {
360 w.WriteHeader(http.StatusBadRequest)
361 renderJSONError(enc, "invalid_grant")
362 return
363 }
364 if !grant.ClientID.Equal(clientID) {
365 w.WriteHeader(http.StatusBadRequest)
366 renderJSONError(enc, "invalid_grant")
367 return
368 }
369 token := Token{
370 AccessToken: uuid.NewID().String(),
371 RefreshToken: uuid.NewID().String(),
372 Created: time.Now(),
373 ExpiresIn: defaultTokenExpiration,
374 TokenType: "bearer",
375 Scope: grant.Scope,
376 ProfileID: grant.ProfileID,
377 }
378 err = context.SaveToken(token)
379 if err != nil {
380 w.WriteHeader(http.StatusInternalServerError)
381 renderJSONError(enc, "server_error")
382 return
383 }
384 resp := tokenResponse{
385 AccessToken: token.AccessToken,
386 RefreshToken: token.RefreshToken,
387 ExpiresIn: token.ExpiresIn,
388 TokenType: token.TokenType,
389 }
390 err = enc.Encode(resp)
391 if err != nil {
392 // TODO(paddy): log this or something
393 return
394 }
395 // BUG(paddy): we need to invalidate the grant for future requests
396 }
398 // TODO(paddy): exchange user credentials for access token
399 // TODO(paddy): exchange client credentials for access token
400 // TODO(paddy): implicit grant for access token
401 // TODO(paddy): exchange refresh token for access token