auth

Paddy 2014-11-11 Parent:42bc3e44f4fe Child:c8b0208c9e5d

70:8398c3e4b3d9 Go to Latest

auth/http.go

Actually define Sessions. That last commit (42bc3e44f4fe) didn't actually include Sessions. Oops. Add the file and include our Session definition and sessionStore.

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