auth
auth/oauth2.go
Require authentication when adding endpoint to client. That seems like a bit of a security hole, doesn't it? Not sure how this got overlooked.
1 package auth
3 import (
4 "encoding/json"
5 "errors"
6 "html/template"
7 "log"
8 "net/http"
9 "net/url"
10 "strconv"
11 "strings"
12 "sync"
13 "time"
15 "code.secondbit.org/uuid.hg"
16 "github.com/gorilla/mux"
17 )
19 const (
20 defaultAuthorizationCodeExpiration = 600 // default to ten minute grant expirations
21 getAuthorizationCodeTemplateName = "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")
36 grantTypesMap = grantTypes{types: map[string]GrantType{}}
37 )
39 type grantTypes struct {
40 types map[string]GrantType
41 sync.RWMutex
42 }
44 // GrantType defines a set of functions and metadata around a specific authorization grant strategy.
45 //
46 // The Validate function will be called when requests are made that match the GrantType, and should write any
47 // errors to the ResponseWriter. It is responsible for determining if the grant is valid and a token should be issued.
48 // It must return the scope the grant was for and the ID of the Profile that issued the grant, as well as if the grant
49 // is valid or not. It must not be nil.
50 //
51 // The Invalidate function will be called when the grant has successfully generated a token and the token has successfully
52 // been conveyed to the user. The Invalidate function is always called asynchronously, outside the request. It should take
53 // care of marking the grant as used, if the GrantType requires grants to be one-time only grants. The Invalidate function
54 // can be nil.
55 //
56 // IssuesRefresh determines whether the GrantType should yield a refresh token as well as an access token. If true, the client
57 // will be issued a refresh token.
58 //
59 // AllowsPublic determines whether the GrantType should allow public clients to use that grant. If true, clients without
60 // credentials will be able to use the grant to obtain a token.
61 //
62 // AuditString should return the string that will be saved in the resulting Token's CreatedFrom field, as an audit log of how
63 // the Token was authorized.
64 //
65 // The ReturnToken will be called when a token is created and needs to be returned to the client. If it returns true, the token
66 // was successfully returned and the Invalidate function will be called asynchronously.
67 type GrantType struct {
68 Validate func(w http.ResponseWriter, r *http.Request, context Context) (scopes []string, profileID uuid.ID, valid bool)
69 Invalidate func(r *http.Request, context Context) error
70 ReturnToken func(w http.ResponseWriter, r *http.Request, token Token, context Context) bool
71 AuditString func(r *http.Request) string
72 IssuesRefresh bool
73 AllowsPublic bool
74 }
76 type tokenResponse struct {
77 AccessToken string `json:"access_token"`
78 TokenType string `json:"token_type,omitempty"`
79 ExpiresIn int32 `json:"expires_in,omitempty"`
80 RefreshToken string `json:"refresh_token,omitempty"`
81 }
83 type errorResponse struct {
84 Error string `json:"error"`
85 Description string `json:"error_description,omitempty"`
86 URI string `json:"error_uri,omitempty"`
87 }
89 // RegisterGrantType associates a string with a GrantType. When the string is used as the value for "grant_type" when obtaining
90 // an access token, the associated GrantType's properties will be used.
91 //
92 // RegisterGrantType should be called in the `init()` function of packages, much like database/sql registers drivers. It will panic
93 // if a GrantType tries to register under a string that already has a GrantType registered for it.
94 func RegisterGrantType(name string, g GrantType) {
95 grantTypesMap.Lock()
96 defer grantTypesMap.Unlock()
97 if _, ok := grantTypesMap.types[name]; ok {
98 panic("Duplicate registration of grant_type " + name)
99 }
100 grantTypesMap.types[name] = g
101 }
103 func findGrantType(name string) (GrantType, bool) {
104 grantTypesMap.RLock()
105 defer grantTypesMap.RUnlock()
106 t, ok := grantTypesMap.types[name]
107 return t, ok
108 }
110 func renderJSONError(enc *json.Encoder, errorType string) {
111 err := enc.Encode(errorResponse{
112 Error: errorType,
113 })
114 if err != nil {
115 log.Println(err)
116 }
117 }
119 // RenderJSONToken is an implementation of the ReturnToken function for GrantTypes. It returns the token using JSON
120 // according to the spec. See RFC 6479, Section 4.1.4.
121 func RenderJSONToken(w http.ResponseWriter, r *http.Request, token Token, context Context) bool {
122 enc := json.NewEncoder(w)
123 resp := tokenResponse{
124 AccessToken: token.AccessToken,
125 RefreshToken: token.RefreshToken,
126 ExpiresIn: token.ExpiresIn,
127 TokenType: token.TokenType,
128 }
129 w.Header().Set("Content-Type", "application/json")
130 err := enc.Encode(resp)
131 if err != nil {
132 log.Println(err)
133 return false
134 }
135 return true
136 }
138 // RegisterOAuth2 adds handlers to the passed router to handle the OAuth2 endpoints.
139 func RegisterOAuth2(r *mux.Router, context Context) {
140 r.Handle("/authorize", wrap(context, GetAuthorizationCodeHandler))
141 r.Handle("/token", wrap(context, GetTokenHandler))
142 }
144 // GetAuthorizationCodeHandler presents and processes the page for asking a user to grant access
145 // to their data. See RFC 6749, Section 4.1.
146 func GetAuthorizationCodeHandler(w http.ResponseWriter, r *http.Request, context Context) {
147 session, err := checkCookie(r, context)
148 if err != nil {
149 if err == ErrNoSession || err == ErrInvalidSession {
150 redir := buildLoginRedirect(r, context)
151 if redir == "" {
152 log.Println("No login URL configured.")
153 w.WriteHeader(http.StatusInternalServerError)
154 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
155 "internal_error": template.HTML("Missing login URL."),
156 })
157 return
158 }
159 http.Redirect(w, r, redir, http.StatusFound)
160 return
161 }
162 log.Println(err.Error())
163 w.WriteHeader(http.StatusInternalServerError)
164 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
165 "internal_error": template.HTML(err.Error()),
166 })
167 return
168 }
169 if r.URL.Query().Get("client_id") == "" {
170 w.WriteHeader(http.StatusBadRequest)
171 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
172 "error": template.HTML("Client ID must be specified in the request."),
173 })
174 return
175 }
176 clientID, err := uuid.Parse(r.URL.Query().Get("client_id"))
177 if err != nil {
178 w.WriteHeader(http.StatusBadRequest)
179 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
180 "error": template.HTML("client_id is not a valid Client ID."),
181 })
182 return
183 }
184 redirectURI := r.URL.Query().Get("redirect_uri")
185 client, err := context.GetClient(clientID)
186 if err != nil {
187 if err == ErrClientNotFound {
188 w.WriteHeader(http.StatusBadRequest)
189 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
190 "error": template.HTML("The specified Client couldn’t be found."),
191 })
192 } else {
193 log.Println(err.Error())
194 w.WriteHeader(http.StatusInternalServerError)
195 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
196 "internal_error": template.HTML(err.Error()),
197 })
198 }
199 return
200 }
201 // BUG(paddy): Checking if the redirect URI is valid should be a helper function.
203 // whether a redirect URI is valid or not depends on the number of endpoints
204 // the client has registered
205 numEndpoints, err := context.CountEndpoints(clientID)
206 if err != nil {
207 log.Println(err.Error())
208 w.WriteHeader(http.StatusInternalServerError)
209 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
210 "internal_error": template.HTML(err.Error()),
211 })
212 return
213 }
214 var validURI bool
215 if redirectURI != "" {
216 validURI, err = context.CheckEndpoint(clientID, redirectURI)
217 if err != nil {
218 if err == ErrEndpointURINotURL {
219 w.WriteHeader(http.StatusBadRequest)
220 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
221 "error": template.HTML("The redirect_uri specified is not valid."),
222 })
223 return
224 }
225 log.Println(err.Error())
226 w.WriteHeader(http.StatusInternalServerError)
227 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
228 "internal_error": template.HTML(err.Error()),
229 })
230 return
231 }
232 } else if redirectURI == "" && numEndpoints == 1 {
233 // if we don't specify the endpoint and there's only one endpoint, the
234 // request is valid, and we're redirecting to that one endpoint
235 validURI = true
236 endpoints, err := context.ListEndpoints(clientID, 1, 0)
237 if err != nil {
238 log.Println(err.Error())
239 w.WriteHeader(http.StatusInternalServerError)
240 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
241 "internal_error": template.HTML(err.Error()),
242 })
243 return
244 }
245 if len(endpoints) != 1 {
246 validURI = false
247 } else {
248 redirectURI = endpoints[0].URI
249 }
250 } else {
251 validURI = false
252 }
253 if !validURI {
254 w.WriteHeader(http.StatusBadRequest)
255 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
256 "error": template.HTML("The redirect_uri specified is not valid."),
257 })
258 return
259 }
260 redirectURL, err := url.Parse(redirectURI)
261 if err != nil {
262 w.WriteHeader(http.StatusBadRequest)
263 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
264 "error": template.HTML("The redirect_uri specified is not valid."),
265 })
266 return
267 }
268 state := r.URL.Query().Get("state")
269 responseType := r.URL.Query().Get("response_type")
270 q := redirectURL.Query()
271 q.Add("state", state)
272 if responseType != "code" && responseType != "token" {
273 q.Add("error", "invalid_request")
274 redirectURL.RawQuery = q.Encode()
275 http.Redirect(w, r, redirectURL.String(), http.StatusFound)
276 return
277 }
278 scopeParams := strings.Split(r.URL.Query().Get("scope"), " ")
279 scopes, err := context.GetScopes(scopeParams)
280 if err != nil {
281 if _, ok := err.(ErrScopeNotFound); ok {
282 q.Add("error", "invalid_scope")
283 redirectURL.RawQuery = q.Encode()
284 http.Redirect(w, r, redirectURL.String(), http.StatusFound)
285 return
286 }
287 log.Println("Error retrieving scopes:", err)
288 q.Add("error", "server_error")
289 redirectURL.RawQuery = q.Encode()
290 http.Redirect(w, r, redirectURL.String(), http.StatusFound)
291 return
292 }
293 if r.Method == "POST" {
294 if checkCSRF(r, session) != nil {
295 log.Println("CSRF attempt detected.")
296 w.WriteHeader(http.StatusInternalServerError)
297 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
298 "error": template.HTML("There was an error authenticating your request."),
299 })
300 return
301 }
302 if r.PostFormValue("grant") == "approved" {
303 var fragment bool
304 switch responseType {
305 case "code":
306 code := uuid.NewID().String()
307 authCode := AuthorizationCode{
308 Code: code,
309 Created: time.Now(),
310 ExpiresIn: defaultAuthorizationCodeExpiration,
311 ClientID: clientID,
312 Scopes: scopeParams,
313 RedirectURI: r.URL.Query().Get("redirect_uri"),
314 State: state,
315 ProfileID: session.ProfileID,
316 }
317 err := context.SaveAuthorizationCode(authCode)
318 if err != nil {
319 log.Println("Error saving authorization code:", err)
320 q.Add("error", "server_error")
321 break
322 }
323 q.Add("code", code)
324 case "token":
325 token := Token{
326 AccessToken: uuid.NewID().String(),
327 Created: time.Now(),
328 CreatedFrom: "implicit",
329 ExpiresIn: defaultTokenExpiration,
330 TokenType: "bearer",
331 Scopes: scopeParams,
332 ProfileID: session.ProfileID,
333 ClientID: clientID,
334 }
335 err := context.SaveToken(token)
336 if err != nil {
337 log.Println("Error saving token:", err)
338 q.Add("error", "server_error")
339 break
340 }
341 q = url.Values{} // we're not altering the querystring, so don't clone it
342 q.Add("access_token", token.AccessToken)
343 q.Add("token_type", token.TokenType)
344 q.Add("expires_in", strconv.FormatInt(int64(token.ExpiresIn), 10))
345 q.Add("scope", strings.Join(token.Scopes, " "))
346 q.Add("state", state) // we wiped out the old values, so we need to set the state again
347 fragment = true
348 }
349 if fragment {
350 redirectURL.Fragment = q.Encode()
351 } else {
352 redirectURL.RawQuery = q.Encode()
353 }
354 http.Redirect(w, r, redirectURL.String(), http.StatusFound)
355 return
356 }
357 q.Add("error", "access_denied")
358 redirectURL.RawQuery = q.Encode()
359 http.Redirect(w, r, redirectURL.String(), http.StatusFound)
360 return
361 }
362 profile, err := context.GetProfileByID(session.ProfileID)
363 if err != nil {
364 log.Println("Error getting profile from session:", err)
365 q.Add("error", "server_error")
366 redirectURL.RawQuery = q.Encode()
367 http.Redirect(w, r, redirectURL.String(), http.StatusFound)
368 return
369 }
370 w.WriteHeader(http.StatusOK)
371 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
372 "client": client,
373 "redirectURL": redirectURL,
374 "scopes": scopes,
375 "profile": profile,
376 "csrftoken": session.CSRFToken,
377 })
378 }
380 // GetTokenHandler allows a client to exchange an authorization grant for an
381 // access token. See RFC 6749 Section 4.1.3.
382 func GetTokenHandler(w http.ResponseWriter, r *http.Request, context Context) {
383 enc := json.NewEncoder(w)
384 grantType := r.PostFormValue("grant_type")
385 gt, ok := findGrantType(grantType)
386 if !ok {
387 w.WriteHeader(http.StatusBadRequest)
388 renderJSONError(enc, "invalid_request")
389 return
390 }
391 clientID, success := verifyClient(w, r, gt.AllowsPublic, context)
392 if !success {
393 return
394 }
395 scopes, profileID, valid := gt.Validate(w, r, context)
396 if !valid {
397 return
398 }
399 refresh := ""
400 if gt.IssuesRefresh {
401 refresh = uuid.NewID().String()
402 }
403 token := Token{
404 AccessToken: uuid.NewID().String(),
405 RefreshToken: refresh,
406 Created: time.Now(),
407 CreatedFrom: gt.AuditString(r),
408 ExpiresIn: defaultTokenExpiration,
409 TokenType: "bearer",
410 Scopes: scopes,
411 ProfileID: profileID,
412 ClientID: clientID,
413 }
414 err := context.SaveToken(token)
415 if err != nil {
416 w.WriteHeader(http.StatusInternalServerError)
417 renderJSONError(enc, "server_error")
418 return
419 }
420 if gt.ReturnToken(w, r, token, context) && gt.Invalidate != nil {
421 go gt.Invalidate(r, context)
422 }
423 }