auth

Paddy 2014-11-11 Parent:http.go@ade4f4afd898 Child:ff7bf5bd0df3

73:4ae226929e92 Go to Latest

auth/oauth2.go

Rename http.go. We're going to have a lot of HTTP handlers, and I'd rather make it clear that this is taking care of our OAuth2 HTTP logic. So rename the file, and we'll put the API handlers in their files, or something.

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