auth

Paddy 2014-11-11 Parent:752c2fb9731c Child:c8b0208c9e5d

69:42bc3e44f4fe Go to Latest

auth/http.go

Stub out sessions. Stop using the Login type when getting profile by Login, removing Logins, or recording Login use. The Login value has to be unique, anyways, and we don't actually know the Login type when getting a profile by Login. That's sort of the point. Create the concept of Sessions and a sessionStore type to manage our authentication sessions with the server. As per OWASP, we're basically just going to use a transparent, SHA256-generated random string as an ID, and store it client-side and server-side and just pass it back and forth. Add the ProfileID to the Grant type, because we need to remember who granted access. That's sort of important. Set a defaultGrantExpiration constant to an hour, so we have that one constant when creating new Grants. Create a helper that pulls the session ID out of an auth cookie, checks it against the sessionStore, and returns the Session if it's valid. Create a helper that pulls the username and password out of a basic auth header. Create a helper that authenticates a user's login and passphrase, checking them against the profileStore securely. Stub out how the cookie checking is going to work for getting grant approval. Fix the stored Grant RedirectURI to be the passed in redirect URI, not the RedirectURI that we ultimately redirect to. This is in accordance with the spec. Store the profile ID from our session in the created Grant. Stub out a GetTokenHandler that will allow users to exchange a Grant for a Token. Set a constant for the current passphrase scheme, which we will increment for each revision to the passphrase scheme, for backwards compatibility. Change the Profile iterations property to an int, not an int64, to match the code.secondbit.org/pass library (which is matching the PBKDF2 library).

History
1 package auth
3 import (
4 "encoding/base64"
5 "encoding/json"
6 "errors"
7 "html/template"
8 "net/http"
9 "net/url"
10 "strings"
11 "time"
13 "crypto/sha256"
14 "code.secondbit.org/pass"
15 "code.secondbit.org/uuid"
16 )
18 const (
19 authCookieName = "auth"
20 defaultGrantExpiration = 600 // default to ten minute grant expirations
21 getGrantTemplateName = "get_grant"
22 )
24 var (
25 // ErrNoAuth is returned when an Authorization header is not present or is empty.
26 ErrNoAuth = errors.New("no authorization header supplied")
27 // ErrInvalidAuthFormat is returned when an Authorization header is present but not the correct format.
28 ErrInvalidAuthFormat = errors.New("authorization header is not in a valid format")
29 // ErrIncorrectAuth is returned when a user authentication attempt does not match the stored values.
30 ErrIncorrectAuth = errors.New("invalid authentication")
31 // ErrInvalidPassphraseScheme is returned when an undefined passphrase scheme is used.
32 ErrInvalidPassphraseScheme = errors.New("invalid passphrase scheme")
33 // ErrNoSession is returned when no session ID is passed with a request.
34 ErrNoSession = errors.New("no session ID found")
35 )
37 type tokenResponse struct {
38 AccessToken string `json:"access_token"`
39 TokenType string `json:"token_type,omitempty"`
40 ExpiresIn int32 `json:"expires_in,omitempty"`
41 RefreshToken string `json:"refresh_token,omitempty"`
42 }
44 func getBasicAuth(r *http.Request) (un, pass string, err error) {
45 auth := r.Header.Get("Authorization")
46 if auth == "" {
47 return "", "", ErrNoAuth
48 }
49 pieces := strings.SplitN(auth, " ", 2)
50 if pieces[0] != "Basic" {
51 return "", "", ErrInvalidAuthFormat
52 }
53 decoded, err := base64.StdEncoding.DecodeString(pieces[1])
54 if err != nil {
55 // TODO(paddy): should probably log this...
56 return "", "", ErrInvalidAuthFormat
57 }
58 info := strings.SplitN(string(decoded), ":", 2)
59 return info[0], info[1], nil
60 }
62 func checkCookie(r *http.Request, context Context) (Session, error) {
63 cookie, err := r.Cookie(authCookieName)
64 if err != nil {
65 if err == http.ErrNoCookie {
66 return Session{}, ErrNoSession
67 }
68 return Session{}, err
69 }
70 if cookie.Name != authCookieName || !cookie.Expires.After(time.Now()) ||
71 !cookie.Secure || !cookie.HttpOnly {
72 return Session{}, ErrInvalidSession
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 authenticate(user, passphrase string, context Context) (Profile, error) {
87 profile, err := context.GetProfileByLogin(user)
88 if err != nil {
89 if err == ErrProfileNotFound {
90 return Profile{}, ErrIncorrectAuth
91 }
92 return Profile{}, err
93 }
94 switch profile.PassphraseScheme {
95 case 1:
96 candidate := pass.Check(sha256.New, profile.Iterations, []byte(passphrase), []byte(profile.Salt))
97 if !pass.Compare(candidate, []byte(profile.Passphrase)) {
98 return Profile{}, ErrIncorrectAuth
99 }
100 default:
101 // TODO(paddy): return some error
102 return Profile{}, ErrInvalidPassphraseScheme
103 }
104 return profile, nil
105 }
107 // GetGrantHandler presents and processes the page for asking a user to grant access
108 // to their data. See RFC 6749, Section 4.1.
109 func GetGrantHandler(w http.ResponseWriter, r *http.Request, context Context) {
110 session, err := checkCookie(r, context)
111 if err != nil {
112 if err == ErrNoSession {
113 // TODO(paddy): redirect to login screen
114 //return
115 }
116 if err == ErrInvalidSession {
117 // TODO(paddy): return an access denied error
118 //return
119 }
120 // TODO(paddy): return a server error
121 //return
122 }
123 if r.URL.Query().Get("client_id") == "" {
124 w.WriteHeader(http.StatusBadRequest)
125 context.Render(w, getGrantTemplateName, map[string]interface{}{
126 "error": template.HTML("Client ID must be specified in the request."),
127 })
128 return
129 }
130 clientID, err := uuid.Parse(r.URL.Query().Get("client_id"))
131 if err != nil {
132 w.WriteHeader(http.StatusBadRequest)
133 context.Render(w, getGrantTemplateName, map[string]interface{}{
134 "error": template.HTML("client_id is not a valid Client ID."),
135 })
136 return
137 }
138 redirectURI := r.URL.Query().Get("redirect_uri")
139 redirectURL, err := url.Parse(redirectURI)
140 if err != nil {
141 w.WriteHeader(http.StatusBadRequest)
142 context.Render(w, getGrantTemplateName, map[string]interface{}{
143 "error": template.HTML("The redirect_uri specified is not valid."),
144 })
145 return
146 }
147 client, err := context.GetClient(clientID)
148 if err != nil {
149 if err == ErrClientNotFound {
150 w.WriteHeader(http.StatusBadRequest)
151 context.Render(w, getGrantTemplateName, map[string]interface{}{
152 "error": template.HTML("The specified Client couldn’t be found."),
153 })
154 } else {
155 w.WriteHeader(http.StatusInternalServerError)
156 context.Render(w, getGrantTemplateName, map[string]interface{}{
157 "internal_error": template.HTML(err.Error()),
158 })
159 }
160 return
161 }
162 // whether a redirect URI is valid or not depends on the number of endpoints
163 // the client has registered
164 numEndpoints, err := context.CountEndpoints(clientID)
165 if err != nil {
166 w.WriteHeader(http.StatusInternalServerError)
167 context.Render(w, getGrantTemplateName, map[string]interface{}{
168 "internal_error": template.HTML(err.Error()),
169 })
170 return
171 }
172 var validURI bool
173 if redirectURI != "" {
174 // BUG(paddy): We really should normalize URIs before trying to compare them.
175 validURI, err = context.CheckEndpoint(clientID, redirectURI)
176 if err != nil {
177 w.WriteHeader(http.StatusInternalServerError)
178 context.Render(w, getGrantTemplateName, map[string]interface{}{
179 "internal_error": template.HTML(err.Error()),
180 })
181 return
182 }
183 } else if redirectURI == "" && numEndpoints == 1 {
184 // if we don't specify the endpoint and there's only one endpoint, the
185 // request is valid, and we're redirecting to that one endpoint
186 validURI = true
187 endpoints, err := context.ListEndpoints(clientID, 1, 0)
188 if err != nil {
189 w.WriteHeader(http.StatusInternalServerError)
190 context.Render(w, getGrantTemplateName, map[string]interface{}{
191 "internal_error": template.HTML(err.Error()),
192 })
193 return
194 }
195 if len(endpoints) != 1 {
196 validURI = false
197 } else {
198 u := endpoints[0].URI // Copy it here to avoid grabbing a pointer to the memstore
199 redirectURI = u.String()
200 redirectURL = &u
201 }
202 } else {
203 validURI = false
204 }
205 if !validURI {
206 w.WriteHeader(http.StatusBadRequest)
207 context.Render(w, getGrantTemplateName, map[string]interface{}{
208 "error": template.HTML("The redirect_uri specified is not valid."),
209 })
210 return
211 }
212 scope := r.URL.Query().Get("scope")
213 state := r.URL.Query().Get("state")
214 if r.URL.Query().Get("response_type") != "code" {
215 q := redirectURL.Query()
216 q.Add("error", "invalid_request")
217 q.Add("state", state)
218 redirectURL.RawQuery = q.Encode()
219 http.Redirect(w, r, redirectURL.String(), http.StatusFound)
220 return
221 }
222 if r.Method == "POST" {
223 // BUG(paddy): We need to implement CSRF protection when obtaining a grant code.
224 if r.PostFormValue("grant") == "approved" {
225 code := uuid.NewID().String()
226 grant := Grant{
227 Code: code,
228 Created: time.Now(),
229 ExpiresIn: defaultGrantExpiration,
230 ClientID: clientID,
231 Scope: scope,
232 RedirectURI: r.URL.Query().Get("redirect_uri"),
233 State: state,
234 ProfileID: session.ProfileID,
235 }
236 err := context.SaveGrant(grant)
237 if err != nil {
238 q := redirectURL.Query()
239 q.Add("error", "server_error")
240 q.Add("state", state)
241 redirectURL.RawQuery = q.Encode()
242 http.Redirect(w, r, redirectURL.String(), http.StatusFound)
243 return
244 }
245 q := redirectURL.Query()
246 q.Add("code", code)
247 q.Add("state", state)
248 redirectURL.RawQuery = q.Encode()
249 http.Redirect(w, r, redirectURL.String(), http.StatusFound)
250 return
251 }
252 q := redirectURL.Query()
253 q.Add("error", "access_denied")
254 q.Add("state", state)
255 redirectURL.RawQuery = q.Encode()
256 http.Redirect(w, r, redirectURL.String(), http.StatusFound)
257 return
258 }
259 w.WriteHeader(http.StatusOK)
260 context.Render(w, getGrantTemplateName, map[string]interface{}{
261 "client": client,
262 })
263 }
265 // GetTokenHandler allows a client to exchange an authorization grant for an
266 // access token. See RFC 6749 Section 4.1.3.
267 func GetTokenHandler(w http.ResponseWriter, r *http.Request, context Context) {
268 enc := json.NewEncoder(w)
269 grantType := r.PostFormValue("grant_type")
270 if grantType != "authorization_code" {
271 // TODO(paddy): render invalid request JSON
272 return
273 }
274 code := r.PostFormValue("code")
275 if code == "" {
276 // TODO(paddy): render invalid request JSON
277 return
278 }
279 redirectURI := r.PostFormValue("redirect_uri")
280 clientIDStr, clientSecret, err := getBasicAuth(r)
281 if err != nil {
282 // TODO(paddy): render access denied
283 return
284 }
285 if clientIDStr == "" && err == nil {
286 clientIDStr = r.PostFormValue("client_id")
287 }
288 // TODO(paddy): client ID can also come from Basic auth
289 clientID, err := uuid.Parse(clientIDStr)
290 if err != nil {
291 // TODO(paddy): render invalid request JSON
292 return
293 }
294 client, err := context.GetClient(clientID)
295 if err != nil {
296 if err == ErrClientNotFound {
297 // TODO(paddy): render invalid request JSON
298 } else {
299 // TODO(paddy): render internal server error JSON
300 }
301 return
302 }
303 if client.Secret != clientSecret {
304 // TODO(paddy): render invalid request JSON
305 return
306 }
307 grant, err := context.GetGrant(code)
308 if err != nil {
309 if err == ErrGrantNotFound {
310 // TODO(paddy): return error
311 return
312 }
313 // TODO(paddy): return error
314 }
315 if grant.RedirectURI != redirectURI {
316 // TODO(paddy): return error
317 }
318 if !grant.ClientID.Equal(clientID) {
319 // TODO(paddy): return error
320 }
321 token := Token{
322 AccessToken: uuid.NewID().String(),
323 RefreshToken: uuid.NewID().String(),
324 Created: time.Now(),
325 ExpiresIn: defaultTokenExpiration,
326 TokenType: "", // TODO(paddy): fill in token type
327 Scope: grant.Scope,
328 ProfileID: grant.ProfileID,
329 }
330 err = context.SaveToken(token)
331 if err != nil {
332 // TODO(paddy): return error
333 }
334 resp := tokenResponse{
335 AccessToken: token.AccessToken,
336 RefreshToken: token.RefreshToken,
337 ExpiresIn: token.ExpiresIn,
338 TokenType: token.TokenType,
339 }
340 err = enc.Encode(resp)
341 if err != nil {
342 // TODO(paddy): log this or something
343 return
344 }
345 }
347 // TODO(paddy): exchange user credentials for access token
348 // TODO(paddy): exchange client credentials for access token
349 // TODO(paddy): implicit grant for access token
350 // TODO(paddy): exchange refresh token for access token