auth

Paddy 2014-09-01 Parent:1aa3a85ff853

25:9fc5fd8196b4 Go to Latest

auth/access.go.old

Rough out client. Remove our old client implementation, and start exploring a new ClientStore interface for storing and retrieving Client data. Keep track of a website for clients.

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 == ErrProfileNotFound {
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 }