auth
auth/oauth2.go
Enable the implict grant flow. Add the implicit grant flow. This can't be done in a grant type, because it's not specified through the grant_type parameter, for some absurd reason. Whatever. We basically achieved this by refactoring how we respond to the authorization endpoint, keying off the "response_type" parameter.
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 authCookieName = "auth"
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 // The ReturnToken will be called when a token is created and needs to be returned to the client. If it returns true, the token
60 // was successfully returned and the Invalidate function will be called asynchronously.
61 type GrantType struct {
62 Validate func(w http.ResponseWriter, r *http.Request, context Context) (scope string, profileID uuid.ID, valid bool)
63 Invalidate func(r *http.Request, context Context) error
64 ReturnToken func(w http.ResponseWriter, r *http.Request, token Token, context Context) bool
65 IssuesRefresh bool
66 }
68 type tokenResponse struct {
69 AccessToken string `json:"access_token"`
70 TokenType string `json:"token_type,omitempty"`
71 ExpiresIn int32 `json:"expires_in,omitempty"`
72 RefreshToken string `json:"refresh_token,omitempty"`
73 }
75 type errorResponse struct {
76 Error string `json:"error"`
77 Description string `json:"error_description,omitempty"`
78 URI string `json:"error_uri,omitempty"`
79 }
81 // RegisterGrantType associates a string with a GrantType. When the string is used as the value for "grant_type" when obtaining
82 // an access token, the associated GrantType's properties will be used.
83 //
84 // RegisterGrantType should be called in the `init()` function of packages, much like database/sql registers drivers. It will panic
85 // if a GrantType tries to register under a string that already has a GrantType registered for it.
86 func RegisterGrantType(name string, g GrantType) {
87 grantTypesMap.Lock()
88 defer grantTypesMap.Unlock()
89 if _, ok := grantTypesMap.types[name]; ok {
90 panic("Duplicate registration of grant_type " + name)
91 }
92 grantTypesMap.types[name] = g
93 }
95 func findGrantType(name string) (GrantType, bool) {
96 grantTypesMap.RLock()
97 defer grantTypesMap.RUnlock()
98 t, ok := grantTypesMap.types[name]
99 return t, ok
100 }
102 func renderJSONError(enc *json.Encoder, errorType string) {
103 err := enc.Encode(errorResponse{
104 Error: errorType,
105 })
106 if err != nil {
107 log.Println(err)
108 }
109 }
111 // RenderJSONToken is an implementation of the ReturnToken function for GrantTypes. It returns the token using JSON
112 // according to the spec. See RFC 6479, Section 4.1.4.
113 func RenderJSONToken(w http.ResponseWriter, r *http.Request, token Token, context Context) bool {
114 enc := json.NewEncoder(w)
115 resp := tokenResponse{
116 AccessToken: token.AccessToken,
117 RefreshToken: token.RefreshToken,
118 ExpiresIn: token.ExpiresIn,
119 TokenType: token.TokenType,
120 }
121 w.Header().Set("Content-Type", "application/json")
122 err := enc.Encode(resp)
123 if err != nil {
124 log.Println(err)
125 return false
126 }
127 return true
128 }
130 // RegisterOAuth2 adds handlers to the passed router to handle the OAuth2 endpoints.
131 func RegisterOAuth2(r *mux.Router, context Context) {
132 r.Handle("/authorize", wrap(context, GetAuthorizationCodeHandler))
133 r.Handle("/token", wrap(context, GetTokenHandler))
134 }
136 // GetAuthorizationCodeHandler presents and processes the page for asking a user to grant access
137 // to their data. See RFC 6749, Section 4.1.
138 func GetAuthorizationCodeHandler(w http.ResponseWriter, r *http.Request, context Context) {
139 session, err := checkCookie(r, context)
140 if err != nil {
141 if err == ErrNoSession || err == ErrInvalidSession {
142 redir := buildLoginRedirect(r, context)
143 if redir == "" {
144 log.Println("No login URL configured.")
145 w.WriteHeader(http.StatusInternalServerError)
146 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
147 "internal_error": template.HTML("Missing login URL."),
148 })
149 return
150 }
151 http.Redirect(w, r, redir, http.StatusFound)
152 return
153 }
154 log.Println(err.Error())
155 w.WriteHeader(http.StatusInternalServerError)
156 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
157 "internal_error": template.HTML(err.Error()),
158 })
159 return
160 }
161 if r.URL.Query().Get("client_id") == "" {
162 w.WriteHeader(http.StatusBadRequest)
163 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
164 "error": template.HTML("Client ID must be specified in the request."),
165 })
166 return
167 }
168 clientID, err := uuid.Parse(r.URL.Query().Get("client_id"))
169 if err != nil {
170 w.WriteHeader(http.StatusBadRequest)
171 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
172 "error": template.HTML("client_id is not a valid Client ID."),
173 })
174 return
175 }
176 redirectURI := r.URL.Query().Get("redirect_uri")
177 client, err := context.GetClient(clientID)
178 if err != nil {
179 if err == ErrClientNotFound {
180 w.WriteHeader(http.StatusBadRequest)
181 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
182 "error": template.HTML("The specified Client couldn’t be found."),
183 })
184 } else {
185 log.Println(err.Error())
186 w.WriteHeader(http.StatusInternalServerError)
187 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
188 "internal_error": template.HTML(err.Error()),
189 })
190 }
191 return
192 }
193 // BUG(paddy): checking if the redirect URI is valid should be a helper function
194 // whether a redirect URI is valid or not depends on the number of endpoints
195 // the client has registered
196 numEndpoints, err := context.CountEndpoints(clientID)
197 if err != nil {
198 log.Println(err.Error())
199 w.WriteHeader(http.StatusInternalServerError)
200 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
201 "internal_error": template.HTML(err.Error()),
202 })
203 return
204 }
205 var validURI bool
206 if redirectURI != "" {
207 validURI, err = context.CheckEndpoint(clientID, redirectURI)
208 if err != nil {
209 if err == ErrEndpointURINotURL {
210 w.WriteHeader(http.StatusBadRequest)
211 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
212 "error": template.HTML("The redirect_uri specified is not valid."),
213 })
214 return
215 }
216 log.Println(err.Error())
217 w.WriteHeader(http.StatusInternalServerError)
218 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
219 "internal_error": template.HTML(err.Error()),
220 })
221 return
222 }
223 } else if redirectURI == "" && numEndpoints == 1 {
224 // if we don't specify the endpoint and there's only one endpoint, the
225 // request is valid, and we're redirecting to that one endpoint
226 validURI = true
227 endpoints, err := context.ListEndpoints(clientID, 1, 0)
228 if err != nil {
229 log.Println(err.Error())
230 w.WriteHeader(http.StatusInternalServerError)
231 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
232 "internal_error": template.HTML(err.Error()),
233 })
234 return
235 }
236 if len(endpoints) != 1 {
237 validURI = false
238 } else {
239 redirectURI = endpoints[0].URI
240 }
241 } else {
242 validURI = false
243 }
244 if !validURI {
245 w.WriteHeader(http.StatusBadRequest)
246 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
247 "error": template.HTML("The redirect_uri specified is not valid."),
248 })
249 return
250 }
251 redirectURL, err := url.Parse(redirectURI)
252 if err != nil {
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 scope := r.URL.Query().Get("scope")
260 state := r.URL.Query().Get("state")
261 responseType := r.URL.Query().Get("response_type")
262 q := redirectURL.Query()
263 q.Add("state", state)
264 if responseType != "code" && responseType != "token" {
265 q.Add("error", "invalid_request")
266 redirectURL.RawQuery = q.Encode()
267 http.Redirect(w, r, redirectURL.String(), http.StatusFound)
268 return
269 }
270 if r.Method == "POST" {
271 // BUG(paddy): We need to implement CSRF protection when obtaining a grant code.
272 if r.PostFormValue("grant") == "approved" {
273 var fragment bool
274 switch responseType {
275 case "code":
276 code := uuid.NewID().String()
277 authCode := AuthorizationCode{
278 Code: code,
279 Created: time.Now(),
280 ExpiresIn: defaultAuthorizationCodeExpiration,
281 ClientID: clientID,
282 Scope: scope,
283 RedirectURI: r.URL.Query().Get("redirect_uri"),
284 State: state,
285 ProfileID: session.ProfileID,
286 }
287 err := context.SaveAuthorizationCode(authCode)
288 if err != nil {
289 log.Println("Error saving authorization code:", err)
290 q.Add("error", "server_error")
291 break
292 }
293 q.Add("code", code)
294 case "token":
295 token := Token{
296 AccessToken: uuid.NewID().String(),
297 Created: time.Now(),
298 CreatedFrom: "",
299 ExpiresIn: defaultTokenExpiration,
300 TokenType: "bearer",
301 Scope: scope,
302 ProfileID: session.ProfileID,
303 }
304 err := context.SaveToken(token)
305 if err != nil {
306 log.Println("Error saving token:", err)
307 q.Add("error", "server_error")
308 break
309 }
310 q = url.Values{} // we're not altering the querystring, so don't clone it
311 q.Add("access_token", token.AccessToken)
312 q.Add("token_type", token.TokenType)
313 q.Add("expires_in", strconv.FormatInt(int64(token.ExpiresIn), 10))
314 q.Add("scope", token.Scope)
315 q.Add("state", state) // we wiped out the old values, so we need to set the state again
316 fragment = true
317 }
318 if fragment {
319 redirectURL.Fragment = q.Encode()
320 } else {
321 redirectURL.RawQuery = q.Encode()
322 }
323 http.Redirect(w, r, redirectURL.String(), http.StatusFound)
324 return
325 }
326 q.Add("error", "access_denied")
327 redirectURL.RawQuery = q.Encode()
328 http.Redirect(w, r, redirectURL.String(), http.StatusFound)
329 return
330 }
331 profile, err := context.GetProfileByID(session.ProfileID)
332 if err != nil {
333 log.Println("Error getting profile from session:", err)
334 q.Add("error", "server_error")
335 redirectURL.RawQuery = q.Encode()
336 http.Redirect(w, r, redirectURL.String(), http.StatusFound)
337 return
338 }
339 w.WriteHeader(http.StatusOK)
340 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
341 "client": client,
342 "redirectURL": redirectURL,
343 "scope": scope,
344 "profile": profile,
345 })
346 }
348 // GetTokenHandler allows a client to exchange an authorization grant for an
349 // access token. See RFC 6749 Section 4.1.3.
350 func GetTokenHandler(w http.ResponseWriter, r *http.Request, context Context) {
351 enc := json.NewEncoder(w)
352 grantType := r.PostFormValue("grant_type")
353 gt, ok := findGrantType(grantType)
354 if !ok {
355 w.WriteHeader(http.StatusBadRequest)
356 renderJSONError(enc, "invalid_request")
357 return
358 }
359 scope, profileID, valid := gt.Validate(w, r, context)
360 if !valid {
361 return
362 }
363 refresh := ""
364 if gt.IssuesRefresh {
365 refresh = uuid.NewID().String()
366 }
367 token := Token{
368 AccessToken: uuid.NewID().String(),
369 RefreshToken: refresh,
370 Created: time.Now(),
371 ExpiresIn: defaultTokenExpiration,
372 RefreshExpiresIn: defaultRefreshTokenExpiration,
373 TokenType: "bearer",
374 Scope: scope,
375 ProfileID: profileID,
376 }
377 err := context.SaveToken(token)
378 if err != nil {
379 w.WriteHeader(http.StatusInternalServerError)
380 renderJSONError(enc, "server_error")
381 return
382 }
383 if gt.ReturnToken(w, r, token, context) && gt.Invalidate != nil {
384 go gt.Invalidate(r, context)
385 }
386 }
388 // TODO(paddy): implicit grant for access token
389 // TODO(paddy): exchange refresh token for access token