auth
auth/oauth2.go
Require config.Init in Context, add comment. Add comment to config.Init explaining what it is, to make golint happy. Create an ErrConfigNotInitialized error to use when a Config object is used to create a Context before it is initialized. Check for Config initialization in NewContext and throw ErrConfigNotInitialized if the Config hasn't been initialized.
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"
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 err := enc.Encode(resp)
122 if err != nil {
123 log.Println(err)
124 return false
125 }
126 return true
127 }
129 func wrap(context Context, f func(w http.ResponseWriter, r *http.Request, context Context)) http.Handler {
130 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
131 f(w, r, context)
132 })
133 }
135 // RegisterOAuth2 adds handlers to the passed router to handle the OAuth2 endpoints.
136 func RegisterOAuth2(r *mux.Router, context Context) {
137 r.Handle("/authorize", wrap(context, GetAuthorizationCodeHandler))
138 r.Handle("/token", wrap(context, GetTokenHandler))
139 }
141 // GetAuthorizationCodeHandler presents and processes the page for asking a user to grant access
142 // to their data. See RFC 6749, Section 4.1.
143 func GetAuthorizationCodeHandler(w http.ResponseWriter, r *http.Request, context Context) {
144 session, err := checkCookie(r, context)
145 if err != nil {
146 if err == ErrNoSession || err == ErrInvalidSession {
147 redir := buildLoginRedirect(r, context)
148 if redir == "" {
149 log.Println("No login URL configured.")
150 w.WriteHeader(http.StatusInternalServerError)
151 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
152 "internal_error": template.HTML("Missing login URL."),
153 })
154 return
155 }
156 http.Redirect(w, r, redir, http.StatusFound)
157 return
158 }
159 log.Println(err.Error())
160 w.WriteHeader(http.StatusInternalServerError)
161 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
162 "internal_error": template.HTML(err.Error()),
163 })
164 return
165 }
166 if r.URL.Query().Get("client_id") == "" {
167 w.WriteHeader(http.StatusBadRequest)
168 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
169 "error": template.HTML("Client ID must be specified in the request."),
170 })
171 return
172 }
173 clientID, err := uuid.Parse(r.URL.Query().Get("client_id"))
174 if err != nil {
175 w.WriteHeader(http.StatusBadRequest)
176 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
177 "error": template.HTML("client_id is not a valid Client ID."),
178 })
179 return
180 }
181 redirectURI := r.URL.Query().Get("redirect_uri")
182 redirectURL, err := url.Parse(redirectURI)
183 if err != nil {
184 w.WriteHeader(http.StatusBadRequest)
185 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
186 "error": template.HTML("The redirect_uri specified is not valid."),
187 })
188 return
189 }
190 client, err := context.GetClient(clientID)
191 if err != nil {
192 if err == ErrClientNotFound {
193 w.WriteHeader(http.StatusBadRequest)
194 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
195 "error": template.HTML("The specified Client couldn’t be found."),
196 })
197 } else {
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 }
204 return
205 }
206 // TODO(paddy): checking if the redirect URI is valid should be a helper function
207 // whether a redirect URI is valid or not depends on the number of endpoints
208 // the client has registered
209 numEndpoints, err := context.CountEndpoints(clientID)
210 if err != nil {
211 log.Println(err.Error())
212 w.WriteHeader(http.StatusInternalServerError)
213 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
214 "internal_error": template.HTML(err.Error()),
215 })
216 return
217 }
218 var validURI bool
219 if redirectURI != "" {
220 // BUG(paddy): We really should normalize URIs before trying to compare them.
221 validURI, err = context.CheckEndpoint(clientID, redirectURI)
222 if err != nil {
223 log.Println(err.Error())
224 w.WriteHeader(http.StatusInternalServerError)
225 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
226 "internal_error": template.HTML(err.Error()),
227 })
228 return
229 }
230 } else if redirectURI == "" && numEndpoints == 1 {
231 // if we don't specify the endpoint and there's only one endpoint, the
232 // request is valid, and we're redirecting to that one endpoint
233 validURI = true
234 endpoints, err := context.ListEndpoints(clientID, 1, 0)
235 if err != nil {
236 log.Println(err.Error())
237 w.WriteHeader(http.StatusInternalServerError)
238 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
239 "internal_error": template.HTML(err.Error()),
240 })
241 return
242 }
243 if len(endpoints) != 1 {
244 validURI = false
245 } else {
246 u := endpoints[0].URI // Copy it here to avoid grabbing a pointer to the memstore
247 redirectURI = u.String()
248 redirectURL = &u
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 scope := r.URL.Query().Get("scope")
261 state := r.URL.Query().Get("state")
262 if r.URL.Query().Get("response_type") != "code" {
263 q := redirectURL.Query()
264 q.Add("error", "invalid_request")
265 q.Add("state", state)
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 code := uuid.NewID().String()
274 authCode := AuthorizationCode{
275 Code: code,
276 Created: time.Now(),
277 ExpiresIn: defaultAuthorizationCodeExpiration,
278 ClientID: clientID,
279 Scope: scope,
280 RedirectURI: r.URL.Query().Get("redirect_uri"),
281 State: state,
282 ProfileID: session.ProfileID,
283 }
284 err := context.SaveAuthorizationCode(authCode)
285 if err != nil {
286 q := redirectURL.Query()
287 q.Add("error", "server_error")
288 q.Add("state", state)
289 redirectURL.RawQuery = q.Encode()
290 http.Redirect(w, r, redirectURL.String(), http.StatusFound)
291 return
292 }
293 q := redirectURL.Query()
294 q.Add("code", code)
295 q.Add("state", state)
296 redirectURL.RawQuery = q.Encode()
297 http.Redirect(w, r, redirectURL.String(), http.StatusFound)
298 return
299 }
300 q := redirectURL.Query()
301 q.Add("error", "access_denied")
302 q.Add("state", state)
303 redirectURL.RawQuery = q.Encode()
304 http.Redirect(w, r, redirectURL.String(), http.StatusFound)
305 return
306 }
307 profile, err := context.GetProfileByID(session.ProfileID)
308 if err != nil {
309 q := redirectURL.Query()
310 q.Add("error", "server_error")
311 q.Add("state", state)
312 redirectURL.RawQuery = q.Encode()
313 http.Redirect(w, r, redirectURL.String(), http.StatusFound)
314 return
315 }
316 w.WriteHeader(http.StatusOK)
317 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
318 "client": client,
319 "redirectURL": redirectURL,
320 "scope": scope,
321 "profile": profile,
322 })
323 }
325 // GetTokenHandler allows a client to exchange an authorization grant for an
326 // access token. See RFC 6749 Section 4.1.3.
327 func GetTokenHandler(w http.ResponseWriter, r *http.Request, context Context) {
328 enc := json.NewEncoder(w)
329 grantType := r.PostFormValue("grant_type")
330 gt, ok := findGrantType(grantType)
331 if !ok {
332 w.WriteHeader(http.StatusBadRequest)
333 renderJSONError(enc, "invalid_request")
334 return
335 }
336 scope, profileID, valid := gt.Validate(w, r, context)
337 if !valid {
338 return
339 }
340 refresh := ""
341 if gt.IssuesRefresh {
342 refresh = uuid.NewID().String()
343 }
344 token := Token{
345 AccessToken: uuid.NewID().String(),
346 RefreshToken: refresh,
347 Created: time.Now(),
348 ExpiresIn: defaultTokenExpiration,
349 RefreshExpiresIn: defaultRefreshTokenExpiration,
350 TokenType: "bearer",
351 Scope: scope,
352 ProfileID: profileID,
353 }
354 err := context.SaveToken(token)
355 if err != nil {
356 w.WriteHeader(http.StatusInternalServerError)
357 renderJSONError(enc, "server_error")
358 return
359 }
360 if gt.ReturnToken(w, r, token, context) && gt.Invalidate != nil {
361 go gt.Invalidate(r, context)
362 }
363 }
365 // TODO(paddy): exchange user credentials for access token
366 // TODO(paddy): exchange client credentials for access token
367 // TODO(paddy): implicit grant for access token
368 // TODO(paddy): exchange refresh token for access token