auth

Paddy 2014-11-19 Parent:d43c3fbf00f3 Child:eb3f2938a319

78:a9936cf794ba Go to Latest

auth/oauth2.go

More tests, login redirect bugfix. Add tests for our cookie checking helper and our helper for generating login redirection URIs. Fix a bug where the URL to redirect to was being URL-encoded twice when included in the login redirect URI.

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