auth

Paddy 2014-11-11 Parent:4ae226929e92 Child:8bc51f76e717

74:ff7bf5bd0df3 Go to Latest

auth/oauth2.go

Combine ErrNoSession and ErrInvalidSession. We want to redirect to a login screen for both, right? So let's just handle them at the same time.

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