auth
auth/oauth2.go
Update import paths. Let's just use import paths that don't need aliasing, by specifying that they're mercurial repos.
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 err := enc.Encode(resp)
121 if err != nil {
122 log.Println(err)
123 return false
124 }
125 return true
126 }
128 // RegisterOAuth2 adds handlers to the passed router to handle the OAuth2 endpoints.
129 func RegisterOAuth2(r *mux.Router, context Context) {
130 r.Handle("/authorize", wrap(context, GetAuthorizationCodeHandler))
131 r.Handle("/token", wrap(context, GetTokenHandler))
132 }
134 // GetAuthorizationCodeHandler presents and processes the page for asking a user to grant access
135 // to their data. See RFC 6749, Section 4.1.
136 func GetAuthorizationCodeHandler(w http.ResponseWriter, r *http.Request, context Context) {
137 session, err := checkCookie(r, context)
138 if err != nil {
139 if err == ErrNoSession || err == ErrInvalidSession {
140 redir := buildLoginRedirect(r, context)
141 if redir == "" {
142 log.Println("No login URL configured.")
143 w.WriteHeader(http.StatusInternalServerError)
144 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
145 "internal_error": template.HTML("Missing login URL."),
146 })
147 return
148 }
149 http.Redirect(w, r, redir, http.StatusFound)
150 return
151 }
152 log.Println(err.Error())
153 w.WriteHeader(http.StatusInternalServerError)
154 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
155 "internal_error": template.HTML(err.Error()),
156 })
157 return
158 }
159 if r.URL.Query().Get("client_id") == "" {
160 w.WriteHeader(http.StatusBadRequest)
161 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
162 "error": template.HTML("Client ID must be specified in the request."),
163 })
164 return
165 }
166 clientID, err := uuid.Parse(r.URL.Query().Get("client_id"))
167 if err != nil {
168 w.WriteHeader(http.StatusBadRequest)
169 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
170 "error": template.HTML("client_id is not a valid Client ID."),
171 })
172 return
173 }
174 redirectURI := r.URL.Query().Get("redirect_uri")
175 redirectURL, err := url.Parse(redirectURI)
176 if err != nil {
177 w.WriteHeader(http.StatusBadRequest)
178 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
179 "error": template.HTML("The redirect_uri specified is not valid."),
180 })
181 return
182 }
183 client, err := context.GetClient(clientID)
184 if err != nil {
185 if err == ErrClientNotFound {
186 w.WriteHeader(http.StatusBadRequest)
187 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
188 "error": template.HTML("The specified Client couldn’t be found."),
189 })
190 } else {
191 log.Println(err.Error())
192 w.WriteHeader(http.StatusInternalServerError)
193 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
194 "internal_error": template.HTML(err.Error()),
195 })
196 }
197 return
198 }
199 // TODO(paddy): checking if the redirect URI is valid should be a helper function
200 // whether a redirect URI is valid or not depends on the number of endpoints
201 // the client has registered
202 numEndpoints, err := context.CountEndpoints(clientID)
203 if err != nil {
204 log.Println(err.Error())
205 w.WriteHeader(http.StatusInternalServerError)
206 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
207 "internal_error": template.HTML(err.Error()),
208 })
209 return
210 }
211 var validURI bool
212 if redirectURI != "" {
213 // BUG(paddy): We really should normalize URIs before trying to compare them.
214 validURI, err = context.CheckEndpoint(clientID, redirectURI)
215 if err != nil {
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 u := endpoints[0].URI // Copy it here to avoid grabbing a pointer to the memstore
240 redirectURI = u.String()
241 redirectURL = &u
242 }
243 } else {
244 validURI = false
245 }
246 if !validURI {
247 w.WriteHeader(http.StatusBadRequest)
248 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
249 "error": template.HTML("The redirect_uri specified is not valid."),
250 })
251 return
252 }
253 scope := r.URL.Query().Get("scope")
254 state := r.URL.Query().Get("state")
255 if r.URL.Query().Get("response_type") != "code" {
256 q := redirectURL.Query()
257 q.Add("error", "invalid_request")
258 q.Add("state", state)
259 redirectURL.RawQuery = q.Encode()
260 http.Redirect(w, r, redirectURL.String(), http.StatusFound)
261 return
262 }
263 if r.Method == "POST" {
264 // BUG(paddy): We need to implement CSRF protection when obtaining a grant code.
265 if r.PostFormValue("grant") == "approved" {
266 code := uuid.NewID().String()
267 authCode := AuthorizationCode{
268 Code: code,
269 Created: time.Now(),
270 ExpiresIn: defaultAuthorizationCodeExpiration,
271 ClientID: clientID,
272 Scope: scope,
273 RedirectURI: r.URL.Query().Get("redirect_uri"),
274 State: state,
275 ProfileID: session.ProfileID,
276 }
277 err := context.SaveAuthorizationCode(authCode)
278 if err != nil {
279 q := redirectURL.Query()
280 q.Add("error", "server_error")
281 q.Add("state", state)
282 redirectURL.RawQuery = q.Encode()
283 http.Redirect(w, r, redirectURL.String(), http.StatusFound)
284 return
285 }
286 q := redirectURL.Query()
287 q.Add("code", code)
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("error", "access_denied")
295 q.Add("state", state)
296 redirectURL.RawQuery = q.Encode()
297 http.Redirect(w, r, redirectURL.String(), http.StatusFound)
298 return
299 }
300 profile, err := context.GetProfileByID(session.ProfileID)
301 if err != nil {
302 q := redirectURL.Query()
303 q.Add("error", "server_error")
304 q.Add("state", state)
305 redirectURL.RawQuery = q.Encode()
306 http.Redirect(w, r, redirectURL.String(), http.StatusFound)
307 return
308 }
309 w.WriteHeader(http.StatusOK)
310 context.Render(w, getAuthorizationCodeTemplateName, map[string]interface{}{
311 "client": client,
312 "redirectURL": redirectURL,
313 "scope": scope,
314 "profile": profile,
315 })
316 }
318 // GetTokenHandler allows a client to exchange an authorization grant for an
319 // access token. See RFC 6749 Section 4.1.3.
320 func GetTokenHandler(w http.ResponseWriter, r *http.Request, context Context) {
321 enc := json.NewEncoder(w)
322 grantType := r.PostFormValue("grant_type")
323 gt, ok := findGrantType(grantType)
324 if !ok {
325 w.WriteHeader(http.StatusBadRequest)
326 renderJSONError(enc, "invalid_request")
327 return
328 }
329 scope, profileID, valid := gt.Validate(w, r, context)
330 if !valid {
331 return
332 }
333 refresh := ""
334 if gt.IssuesRefresh {
335 refresh = uuid.NewID().String()
336 }
337 token := Token{
338 AccessToken: uuid.NewID().String(),
339 RefreshToken: refresh,
340 Created: time.Now(),
341 ExpiresIn: defaultTokenExpiration,
342 RefreshExpiresIn: defaultRefreshTokenExpiration,
343 TokenType: "bearer",
344 Scope: scope,
345 ProfileID: profileID,
346 }
347 err := context.SaveToken(token)
348 if err != nil {
349 w.WriteHeader(http.StatusInternalServerError)
350 renderJSONError(enc, "server_error")
351 return
352 }
353 if gt.ReturnToken(w, r, token, context) && gt.Invalidate != nil {
354 go gt.Invalidate(r, context)
355 }
356 }
358 // TODO(paddy): exchange user credentials for access token
359 // TODO(paddy): exchange client credentials for access token
360 // TODO(paddy): implicit grant for access token
361 // TODO(paddy): exchange refresh token for access token