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