auth

Paddy 2015-01-04 Parent:c03b5eb3179e Child:e000b1c24fc0

109:9a5999963868 Go to Latest

auth/oauth2.go

Correctly set Content-Type header when obtaining a token. Some OAuth2 clients break if you don't correctly set the Content-Type header when obtaining a token. Also, let's just try to be good Content-Type/Accept citizens, all around.

History
1 package auth
3 import (
4 "encoding/json"
5 "errors"
6 "html/template"
7 "log"
8 "net/http"
9 "net/url"
10 "sync"
11 "time"
13 "code.secondbit.org/uuid.hg"
14 "github.com/gorilla/mux"
15 )
17 const (
18 authCookieName = "auth"
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 // The ReturnToken will be called when a token is created and needs to be returned to the client. If it returns true, the token
59 // was successfully returned and the Invalidate function will be called asynchronously.
60 type GrantType struct {
61 Validate func(w http.ResponseWriter, r *http.Request, context Context) (scope string, profileID uuid.ID, valid bool)
62 Invalidate func(r *http.Request, context Context) error
63 ReturnToken func(w http.ResponseWriter, r *http.Request, token Token, context Context) bool
64 IssuesRefresh bool
65 }
67 type tokenResponse struct {
68 AccessToken string `json:"access_token"`
69 TokenType string `json:"token_type,omitempty"`
70 ExpiresIn int32 `json:"expires_in,omitempty"`
71 RefreshToken string `json:"refresh_token,omitempty"`
72 }
74 type errorResponse struct {
75 Error string `json:"error"`
76 Description string `json:"error_description,omitempty"`
77 URI string `json:"error_uri,omitempty"`
78 }
80 // RegisterGrantType associates a string with a GrantType. When the string is used as the value for "grant_type" when obtaining
81 // an access token, the associated GrantType's properties will be used.
82 //
83 // RegisterGrantType should be called in the `init()` function of packages, much like database/sql registers drivers. It will panic
84 // if a GrantType tries to register under a string that already has a GrantType registered for it.
85 func RegisterGrantType(name string, g GrantType) {
86 grantTypesMap.Lock()
87 defer grantTypesMap.Unlock()
88 if _, ok := grantTypesMap.types[name]; ok {
89 panic("Duplicate registration of grant_type " + name)
90 }
91 grantTypesMap.types[name] = g
92 }
94 func findGrantType(name string) (GrantType, bool) {
95 grantTypesMap.RLock()
96 defer grantTypesMap.RUnlock()
97 t, ok := grantTypesMap.types[name]
98 return t, ok
99 }
101 func renderJSONError(enc *json.Encoder, errorType string) {
102 err := enc.Encode(errorResponse{
103 Error: errorType,
104 })
105 if err != nil {
106 log.Println(err)
107 }
108 }
110 // RenderJSONToken is an implementation of the ReturnToken function for GrantTypes. It returns the token using JSON
111 // according to the spec. See RFC 6479, Section 4.1.4.
112 func RenderJSONToken(w http.ResponseWriter, r *http.Request, token Token, context Context) bool {
113 enc := json.NewEncoder(w)
114 resp := tokenResponse{
115 AccessToken: token.AccessToken,
116 RefreshToken: token.RefreshToken,
117 ExpiresIn: token.ExpiresIn,
118 TokenType: token.TokenType,
119 }
120 w.Header().Set("Content-Type", "application/json")
121 err := enc.Encode(resp)
122 if err != nil {
123 log.Println(err)
124 return false
125 }
126 return true
127 }
129 // RegisterOAuth2 adds handlers to the passed router to handle the OAuth2 endpoints.
130 func RegisterOAuth2(r *mux.Router, context Context) {
131 r.Handle("/authorize", wrap(context, GetAuthorizationCodeHandler))
132 r.Handle("/token", wrap(context, GetTokenHandler))
133 }
135 // GetAuthorizationCodeHandler presents and processes the page for asking a user to grant access
136 // to their data. See RFC 6749, Section 4.1.
137 func GetAuthorizationCodeHandler(w http.ResponseWriter, r *http.Request, context Context) {
138 session, err := checkCookie(r, context)
139 if err != nil {
140 if err == ErrNoSession || err == ErrInvalidSession {
141 redir := buildLoginRedirect(r, context)
142 if redir == "" {
143 log.Println("No login URL configured.")
144 w.WriteHeader(http.StatusInternalServerError)
145 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
146 "internal_error": template.HTML("Missing login URL."),
147 })
148 return
149 }
150 http.Redirect(w, r, redir, http.StatusFound)
151 return
152 }
153 log.Println(err.Error())
154 w.WriteHeader(http.StatusInternalServerError)
155 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
156 "internal_error": template.HTML(err.Error()),
157 })
158 return
159 }
160 if r.URL.Query().Get("client_id") == "" {
161 w.WriteHeader(http.StatusBadRequest)
162 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
163 "error": template.HTML("Client ID must be specified in the request."),
164 })
165 return
166 }
167 clientID, err := uuid.Parse(r.URL.Query().Get("client_id"))
168 if err != nil {
169 w.WriteHeader(http.StatusBadRequest)
170 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
171 "error": template.HTML("client_id is not a valid Client ID."),
172 })
173 return
174 }
175 redirectURI := r.URL.Query().Get("redirect_uri")
176 redirectURL, err := url.Parse(redirectURI)
177 if err != nil {
178 w.WriteHeader(http.StatusBadRequest)
179 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
180 "error": template.HTML("The redirect_uri specified is not valid."),
181 })
182 return
183 }
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 // TODO(paddy): checking if the redirect URI is valid should be a helper function
201 // whether a redirect URI is valid or not depends on the number of endpoints
202 // the client has registered
203 numEndpoints, err := context.CountEndpoints(clientID)
204 if err != nil {
205 log.Println(err.Error())
206 w.WriteHeader(http.StatusInternalServerError)
207 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
208 "internal_error": template.HTML(err.Error()),
209 })
210 return
211 }
212 var validURI bool
213 if redirectURI != "" {
214 // BUG(paddy): We really should normalize URIs before trying to compare them.
215 validURI, err = context.CheckEndpoint(clientID, redirectURI)
216 if err != nil {
217 log.Println(err.Error())
218 w.WriteHeader(http.StatusInternalServerError)
219 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
220 "internal_error": template.HTML(err.Error()),
221 })
222 return
223 }
224 } else if redirectURI == "" && numEndpoints == 1 {
225 // if we don't specify the endpoint and there's only one endpoint, the
226 // request is valid, and we're redirecting to that one endpoint
227 validURI = true
228 endpoints, err := context.ListEndpoints(clientID, 1, 0)
229 if err != nil {
230 log.Println(err.Error())
231 w.WriteHeader(http.StatusInternalServerError)
232 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
233 "internal_error": template.HTML(err.Error()),
234 })
235 return
236 }
237 if len(endpoints) != 1 {
238 validURI = false
239 } else {
240 u := endpoints[0].URI // Copy it here to avoid grabbing a pointer to the memstore
241 redirectURI = u.String()
242 redirectURL = &u
243 }
244 } else {
245 validURI = false
246 }
247 if !validURI {
248 w.WriteHeader(http.StatusBadRequest)
249 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
250 "error": template.HTML("The redirect_uri specified is not valid."),
251 })
252 return
253 }
254 scope := r.URL.Query().Get("scope")
255 state := r.URL.Query().Get("state")
256 if r.URL.Query().Get("response_type") != "code" {
257 q := redirectURL.Query()
258 q.Add("error", "invalid_request")
259 q.Add("state", state)
260 redirectURL.RawQuery = q.Encode()
261 http.Redirect(w, r, redirectURL.String(), http.StatusFound)
262 return
263 }
264 if r.Method == "POST" {
265 // BUG(paddy): We need to implement CSRF protection when obtaining a grant code.
266 if r.PostFormValue("grant") == "approved" {
267 code := uuid.NewID().String()
268 authCode := AuthorizationCode{
269 Code: code,
270 Created: time.Now(),
271 ExpiresIn: defaultAuthorizationCodeExpiration,
272 ClientID: clientID,
273 Scope: scope,
274 RedirectURI: r.URL.Query().Get("redirect_uri"),
275 State: state,
276 ProfileID: session.ProfileID,
277 }
278 err := context.SaveAuthorizationCode(authCode)
279 if err != nil {
280 q := redirectURL.Query()
281 q.Add("error", "server_error")
282 q.Add("state", state)
283 redirectURL.RawQuery = q.Encode()
284 http.Redirect(w, r, redirectURL.String(), http.StatusFound)
285 return
286 }
287 q := redirectURL.Query()
288 q.Add("code", code)
289 q.Add("state", state)
290 redirectURL.RawQuery = q.Encode()
291 http.Redirect(w, r, redirectURL.String(), http.StatusFound)
292 return
293 }
294 q := redirectURL.Query()
295 q.Add("error", "access_denied")
296 q.Add("state", state)
297 redirectURL.RawQuery = q.Encode()
298 http.Redirect(w, r, redirectURL.String(), http.StatusFound)
299 return
300 }
301 profile, err := context.GetProfileByID(session.ProfileID)
302 if err != nil {
303 q := redirectURL.Query()
304 q.Add("error", "server_error")
305 q.Add("state", state)
306 redirectURL.RawQuery = q.Encode()
307 http.Redirect(w, r, redirectURL.String(), http.StatusFound)
308 return
309 }
310 w.WriteHeader(http.StatusOK)
311 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
312 "client": client,
313 "redirectURL": redirectURL,
314 "scope": scope,
315 "profile": profile,
316 })
317 }
319 // GetTokenHandler allows a client to exchange an authorization grant for an
320 // access token. See RFC 6749 Section 4.1.3.
321 func GetTokenHandler(w http.ResponseWriter, r *http.Request, context Context) {
322 enc := json.NewEncoder(w)
323 grantType := r.PostFormValue("grant_type")
324 gt, ok := findGrantType(grantType)
325 if !ok {
326 w.WriteHeader(http.StatusBadRequest)
327 renderJSONError(enc, "invalid_request")
328 return
329 }
330 scope, profileID, valid := gt.Validate(w, r, context)
331 if !valid {
332 return
333 }
334 refresh := ""
335 if gt.IssuesRefresh {
336 refresh = uuid.NewID().String()
337 }
338 token := Token{
339 AccessToken: uuid.NewID().String(),
340 RefreshToken: refresh,
341 Created: time.Now(),
342 ExpiresIn: defaultTokenExpiration,
343 RefreshExpiresIn: defaultRefreshTokenExpiration,
344 TokenType: "bearer",
345 Scope: scope,
346 ProfileID: profileID,
347 }
348 err := context.SaveToken(token)
349 if err != nil {
350 w.WriteHeader(http.StatusInternalServerError)
351 renderJSONError(enc, "server_error")
352 return
353 }
354 if gt.ReturnToken(w, r, token, context) && gt.Invalidate != nil {
355 go gt.Invalidate(r, context)
356 }
357 }
359 // TODO(paddy): exchange user credentials for access token
360 // TODO(paddy): exchange client credentials for access token
361 // TODO(paddy): implicit grant for access token
362 // TODO(paddy): exchange refresh token for access token