auth

Paddy 2014-08-16 Parent:fc5df8e68c7b Child:9fe684b33b3d

17:1f04b1146cad Go to Latest

auth/access.go

Implement CSRF prevention and pass info to confirmation. Implement CSRF prevention using the nosurf package. Note that the handler still needs to be wrapped before this will work. Pass info on the authorization being requested (namely the client and the scope) to the RenderConfirmation page so that the user can make an educated decision.

History
1 package auth
3 import (
4 "net/http"
5 "net/url"
6 "time"
8 "strconv"
9 "secondbit.org/uuid"
10 )
12 // GrantType is the type for OAuth param `grant_type`
13 type GrantType string
15 const (
16 AuthorizationCodeGrant GrantType = "authorization_code"
17 RefreshTokenGrant = "refresh_token"
18 PasswordGrant = "password"
19 ClientCredentialsGrant = "client_credentials"
20 )
22 // AccessData represents an access grant (tokens, expiration, client, etc)
23 type AccessData struct {
24 PreviousAuthorizeData *AuthorizeData `json:"-"`
25 PreviousAccessData *AccessData `json:"-"` // previous access data, when refreshing
26 AccessToken string `json:"access_token"`
27 RefreshToken string `json:"refresh_token,omitempty"`
28 ExpiresIn int32 `json:"expires_in"`
29 CreatedAt time.Time `json:"-"`
30 TokenType string `json:"token_type"`
31 Scope string `json:"scope,omitempty"`
32 ProfileID uuid.ID `json:"-"`
33 AuthRequest `json:"-"`
34 }
36 // IsExpired returns true if access expired
37 func (d *AccessData) IsExpired() bool {
38 return d.CreatedAt.Add(time.Duration(d.ExpiresIn) * time.Second).Before(time.Now())
39 }
41 // ExpireAt returns the expiration date
42 func (d *AccessData) ExpireAt() time.Time {
43 return d.CreatedAt.Add(time.Duration(d.ExpiresIn) * time.Second)
44 }
46 // HandleOAuth2AccessRequest is the http.HandlerFunc for handling access token requests.
47 func HandleOAuth2AccessRequest(w http.ResponseWriter, r *http.Request, ctx Context) {
48 // Only allow GET or POST
49 if r.Method != "POST" {
50 if r.Method != "GET" || !ctx.Config.AllowGetAccessRequest {
51 ctx.RenderJSONError(w, ErrorInvalidRequest, "Invalid request method.", ctx.Config.DocumentationDomain)
52 return
53 }
54 }
56 grantType := GrantType(r.Form.Get("grant_type"))
57 if ctx.Config.AllowedAccessTypes.Exists(grantType) {
58 switch grantType {
59 case AuthorizationCodeGrant:
60 handleAuthorizationCodeRequest(w, r, ctx)
61 case RefreshTokenGrant:
62 handleRefreshTokenRequest(w, r, ctx)
63 case PasswordGrant:
64 handlePasswordRequest(w, r, ctx)
65 case ClientCredentialsGrant:
66 handleClientCredentialsRequest(w, r, ctx)
67 default:
68 ctx.RenderJSONError(w, ErrorUnsupportedGrantType, "Unsupported grant type.", ctx.Config.DocumentationDomain)
69 return
70 }
71 }
72 }
74 func handleAuthorizationCodeRequest(w http.ResponseWriter, r *http.Request, ctx Context) {
75 // get client authentication
76 auth, err := getClientAuth(r, ctx.Config.AllowClientSecretInParams)
77 if err != nil {
78 ctx.RenderJSONError(w, ErrorInvalidClient, "Invalid client auth.", ctx.Config.DocumentationDomain)
79 return
80 }
82 code := r.Form.Get("code")
83 // "code" is required
84 if code == "" {
85 ctx.RenderJSONError(w, ErrorInvalidRequest, "Code must be supplied.", ctx.Config.DocumentationDomain)
86 return
87 }
89 // must have a valid client
90 client, err := getClient(auth, ctx)
91 if err != nil {
92 if err == ClientNotFoundError || err == InvalidClientError {
93 ctx.RenderJSONError(w, ErrorInvalidClient, "Invalid client auth.", ctx.Config.DocumentationDomain)
94 return
95 }
96 ctx.RenderJSONError(w, ErrorServerError, "Internal server error.", ctx.Config.DocumentationDomain)
97 return
98 }
100 // must be a valid authorization code
101 authData, err := ctx.Tokens.GetAuthorization(code)
102 if err != nil {
103 if err == AuthorizationNotFoundError {
104 ctx.RenderJSONError(w, ErrorInvalidGrant, "Invalid authorization.", ctx.Config.DocumentationDomain)
105 return
106 }
107 ctx.RenderJSONError(w, ErrorServerError, "Internal server error.", ctx.Config.DocumentationDomain)
108 return
109 }
110 if authData.RedirectURI == "" {
111 ctx.RenderJSONError(w, ErrorInvalidGrant, "Invalid redirect on grant.", ctx.Config.DocumentationDomain)
112 return
113 }
114 if authData.IsExpired() {
115 ctx.RenderJSONError(w, ErrorInvalidGrant, "Authorization is expired.", ctx.Config.DocumentationDomain)
116 return
117 }
119 // code must be from the client
120 if !authData.Client.ID.Equal(client.ID) {
121 ctx.RenderJSONError(w, ErrorInvalidGrant, "Grant issued to another client.", ctx.Config.DocumentationDomain)
122 return
123 }
125 // check redirect uri
126 redirectURI := r.Form.Get("redirect_uri")
127 if redirectURI == "" {
128 redirectURI = client.RedirectURI
129 }
130 if err = validateURI(client.RedirectURI, redirectURI); err != nil {
131 ctx.RenderJSONError(w, ErrorInvalidGrant, "Redirect URI doesn't match client.", ctx.Config.DocumentationDomain)
132 return
133 }
134 if authData.RedirectURI != redirectURI {
135 ctx.RenderJSONError(w, ErrorInvalidGrant, "Redirect URI doesn't match auth redirect.", ctx.Config.DocumentationDomain)
136 return
137 }
139 data := AccessData{
140 AuthRequest: AuthRequest{
141 Client: client,
142 RedirectURI: redirectURI,
143 Scope: authData.Scope,
144 },
145 Scope: authData.Scope,
146 PreviousAuthorizeData: &authData,
147 }
149 err = fillTokens(&data, true, ctx)
150 if err != nil {
151 ctx.RenderJSONError(w, ErrorServerError, "Internal server error.", ctx.Config.DocumentationDomain)
152 return
153 }
154 ctx.RenderJSONToken(w, data)
155 }
157 func handleRefreshTokenRequest(w http.ResponseWriter, r *http.Request, ctx Context) {
158 // get client authentication
159 auth, err := getClientAuth(r, ctx.Config.AllowClientSecretInParams)
161 if err != nil {
162 ctx.RenderJSONError(w, ErrorInvalidClient, "Invalid client auth.", ctx.Config.DocumentationDomain)
163 return
164 }
166 code := r.Form.Get("refresh_token")
168 // "refresh_token" is required
169 if code == "" {
170 ctx.RenderJSONError(w, ErrorInvalidRequest, "Missing refresh token.", ctx.Config.DocumentationDomain)
171 return
172 }
174 // must have a valid client
175 client, err := getClient(auth, ctx)
176 if err != nil {
177 if err == ClientNotFoundError || err == InvalidClientError {
178 ctx.RenderJSONError(w, ErrorInvalidClient, "Invalid client auth.", ctx.Config.DocumentationDomain)
179 return
180 }
181 ctx.RenderJSONError(w, ErrorServerError, "Internal server error.", ctx.Config.DocumentationDomain)
182 return
183 }
185 // must be a valid refresh code
186 refreshData, err := ctx.Tokens.GetRefresh(code)
187 if err != nil {
188 if err == TokenNotFoundError {
189 ctx.RenderJSONError(w, ErrorInvalidGrant, "Refresh token not valid.", ctx.Config.DocumentationDomain)
190 return
191 }
192 ctx.RenderJSONError(w, ErrorServerError, "Internal server error.", ctx.Config.DocumentationDomain)
193 return
194 }
196 // client must be the same as the previous token
197 if !refreshData.Client.ID.Equal(client.ID) {
198 ctx.RenderJSONError(w, ErrorInvalidGrant, "Refresh token issued to another client.", ctx.Config.DocumentationDomain)
199 return
200 }
202 scope := r.Form.Get("scope")
203 if scope == "" {
204 scope = refreshData.Scope
205 }
207 data := AccessData{
208 AuthRequest: AuthRequest{
209 Client: client,
210 Scope: scope,
211 },
212 Scope: scope,
213 PreviousAccessData: &refreshData,
214 }
215 err = fillTokens(&data, true, ctx)
216 if err != nil {
217 ctx.RenderJSONError(w, ErrorServerError, "Internal server error.", ctx.Config.DocumentationDomain)
218 return
219 }
220 ctx.RenderJSONToken(w, data)
221 }
223 func handlePasswordRequest(w http.ResponseWriter, r *http.Request, ctx Context) {
224 // get client authentication
225 auth, err := getClientAuth(r, ctx.Config.AllowClientSecretInParams)
226 if err != nil {
227 ctx.RenderJSONError(w, ErrorInvalidClient, "Invalid client auth.", ctx.Config.DocumentationDomain)
228 return
229 }
231 username := r.Form.Get("username")
232 password := r.Form.Get("password")
233 scope := r.Form.Get("scope")
235 // "username" and "password" is required
236 if username == "" || password == "" {
237 ctx.RenderJSONError(w, ErrorInvalidRequest, "Missing credentials.", ctx.Config.DocumentationDomain)
238 return
239 }
241 // must have a valid client
242 client, err := getClient(auth, ctx)
243 if err != nil {
244 if err == ClientNotFoundError || err == InvalidClientError {
245 ctx.RenderJSONError(w, ErrorInvalidClient, "Invalid client auth.", ctx.Config.DocumentationDomain)
246 return
247 }
248 ctx.RenderJSONError(w, ErrorServerError, "Internal server error.", ctx.Config.DocumentationDomain)
249 return
250 }
252 _, err = ctx.Profiles.GetProfile(username, password)
253 if err != nil {
254 if err == ProfileNotFoundError {
255 ctx.RenderJSONError(w, ErrorInvalidGrant, "Invalid credentials.", ctx.Config.DocumentationDomain)
256 return
257 }
258 ctx.RenderJSONError(w, ErrorServerError, "Internal server error.", ctx.Config.DocumentationDomain)
259 return
260 }
262 data := AccessData{
263 AuthRequest: AuthRequest{
264 Client: client,
265 Scope: scope,
266 },
267 Scope: scope,
268 }
270 err = fillTokens(&data, true, ctx)
271 if err != nil {
272 ctx.RenderJSONError(w, ErrorServerError, "Internal server error.", ctx.Config.DocumentationDomain)
273 return
274 }
275 ctx.RenderJSONToken(w, data)
276 }
278 func handleClientCredentialsRequest(w http.ResponseWriter, r *http.Request, ctx Context) {
279 // get client authentication
280 auth, err := getClientAuth(r, ctx.Config.AllowClientSecretInParams)
281 if err != nil {
282 ctx.RenderJSONError(w, ErrorInvalidClient, "Invalid client auth.", ctx.Config.DocumentationDomain)
283 return
284 }
286 scope := r.Form.Get("scope")
288 // must have a valid client
289 client, err := getClient(auth, ctx)
290 if err != nil {
291 if err == ClientNotFoundError || err == InvalidClientError {
292 ctx.RenderJSONError(w, ErrorInvalidClient, "Invalid client auth.", ctx.Config.DocumentationDomain)
293 return
294 }
295 ctx.RenderJSONError(w, ErrorServerError, "Internal server error.", ctx.Config.DocumentationDomain)
296 return
297 }
299 data := AccessData{
300 AuthRequest: AuthRequest{
301 Client: client,
302 Scope: scope,
303 },
304 Scope: scope,
305 }
307 err = fillTokens(&data, true, ctx)
308 if err != nil {
309 ctx.RenderJSONError(w, ErrorServerError, "Internal server error.", ctx.Config.DocumentationDomain)
310 return
311 }
312 ctx.RenderJSONToken(w, data)
313 }
315 func fillTokens(data *AccessData, includeRefresh bool, ctx Context) error {
316 var err error
318 // generate access token
319 data.AccessToken = newToken()
320 if includeRefresh {
321 data.RefreshToken = newToken()
322 }
324 // save access token
325 err = ctx.Tokens.SaveAccess(*data)
326 if err != nil {
327 if ctx.Log != nil {
328 ctx.Log.Printf("Error writing access token: %s\n", err)
329 }
330 return InternalServerError
331 }
333 // remove authorization token
334 if data.PreviousAuthorizeData != nil {
335 err = ctx.Tokens.RemoveAuthorization(data.PreviousAuthorizeData.Code)
336 if err != nil && ctx.Log != nil {
337 ctx.Log.Printf("Error removing previous auth data (%s): %s\n", data.PreviousAuthorizeData.Code, err)
338 }
339 }
341 // remove previous access token
342 if data.PreviousAccessData != nil {
343 if data.PreviousAccessData.RefreshToken != "" {
344 err = ctx.Tokens.RemoveRefresh(data.PreviousAccessData.RefreshToken)
345 if err != nil && ctx.Log != nil {
346 ctx.Log.Printf("Error removing previous refresh token (%s): %s\n", data.PreviousAccessData.RefreshToken, err)
347 }
348 }
349 err = ctx.Tokens.RemoveAccess(data.PreviousAccessData.AccessToken)
350 if err != nil && ctx.Log != nil {
351 ctx.Log.Printf("Error removing previous access token (%s): %s\n", data.PreviousAccessData.AccessToken, err)
352 }
353 }
355 data.TokenType = ctx.Config.TokenType
356 data.ExpiresIn = ctx.Config.AccessExpiration
357 data.CreatedAt = time.Now()
358 return nil
359 }
361 func (data AccessData) GetRedirect(fragment bool) (string, error) {
362 u, err := url.Parse(data.RedirectURI)
363 if err != nil {
364 return "", err
365 }
367 // add parameters
368 q := u.Query()
369 q.Set("access_token", data.AccessToken)
370 q.Set("token_type", data.TokenType)
371 q.Set("expires_in", strconv.FormatInt(int64(data.ExpiresIn), 10))
372 if data.RefreshToken != "" {
373 q.Set("refresh_token", data.RefreshToken)
374 }
375 if data.Scope != "" {
376 q.Set("scope", data.Scope)
377 }
378 if len(data.ProfileID) > 0 {
379 q.Set("profile", data.ProfileID.String())
380 }
381 if fragment {
382 u.RawQuery = ""
383 u.Fragment = q.Encode()
384 } else {
385 u.RawQuery = q.Encode()
386 }
388 return u.String(), nil
389 }
391 // getClient looks up and authenticates the basic auth using the given
392 // storage. Sets an error on the response if auth fails or a server error occurs.
393 func getClient(auth BasicAuth, ctx Context) (Client, error) {
394 id, err := uuid.Parse(auth.Username)
395 if err != nil {
396 return Client{}, err
397 }
398 client, err := ctx.Clients.GetClient(id)
399 if err != nil {
400 if err == ClientNotFoundError {
401 return Client{}, err
402 }
403 if ctx.Log != nil {
404 ctx.Log.Printf("Error retrieving client %s: %s", id, err)
405 }
406 return Client{}, InternalServerError
407 }
408 if client.Secret != auth.Password {
409 return Client{}, InvalidClientError
410 }
411 if client.RedirectURI == "" {
412 return Client{}, InvalidClientError
413 }
414 return client, nil
415 }