auth

Paddy 2014-12-14 Parent:09c47387e455 Child:bc77a315f823

103:0b45e6b9cb94 Go to Latest

auth/oauth2.go

Store salts and passphrases as hex-encoded strings. Update our passphraseScheme.create function signature to return strings. Hex encode our passphrases and salts when encrypthing them so they're easier to store safely. Decode our salt before using it to check candidate passphrases.

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"
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