auth

Paddy 2014-11-11 Parent:c8b0208c9e5d

72:ade4f4afd898 Go to Latest

auth/http.go

Already pulled client ID out of basic auth. Remove another finished TODO.

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