auth

Paddy 2014-08-13 Parent:edf7ffe90517 Child:244ac84003b3

9:10b84165df41 Go to Latest

auth/access.go

Handle remaining access errors. Fill out remaining TODOs about returning errors when trying to obtain an access token.

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
25 PreviousAccessData *AccessData // previous access data, when refreshing
26 AccessToken string
27 RefreshToken string
28 ExpiresIn int32
29 CreatedAt time.Time
30 TokenType string
31 ProfileID uuid.ID
32 AuthRequest
33 }
35 // IsExpired returns true if access expired
36 func (d *AccessData) IsExpired() bool {
37 return d.CreatedAt.Add(time.Duration(d.ExpiresIn) * time.Second).Before(time.Now())
38 }
40 // ExpireAt returns the expiration date
41 func (d *AccessData) ExpireAt() time.Time {
42 return d.CreatedAt.Add(time.Duration(d.ExpiresIn) * time.Second)
43 }
45 // HandleOAuth2AccessRequest is the http.HandlerFunc for handling access token requests.
46 func HandleOAuth2AccessRequest(w http.ResponseWriter, r *http.Request, ctx Context) {
47 // Only allow GET or POST
48 if r.Method != "POST" {
49 if r.Method != "GET" || !ctx.Config.AllowGetAccessRequest {
50 ctx.RenderJSONError(w, ErrorInvalidRequest, "Invalid request method.", ctx.Config.DocumentationDomain)
51 return
52 }
53 }
55 grantType := GrantType(r.Form.Get("grant_type"))
56 if ctx.Config.AllowedAccessTypes.Exists(grantType) {
57 switch grantType {
58 case AuthorizationCodeGrant:
59 handleAuthorizationCodeRequest(w, r, ctx)
60 case RefreshTokenGrant:
61 handleRefreshTokenRequest(w, r, ctx)
62 case PasswordGrant:
63 handlePasswordRequest(w, r, ctx)
64 case ClientCredentialsGrant:
65 handleClientCredentialsRequest(w, r, ctx)
66 default:
67 ctx.RenderJSONError(w, ErrorUnsupportedGrantType, "Unsupported grant type.", ctx.Config.DocumentationDomain)
68 return
69 }
70 }
71 }
73 func handleAuthorizationCodeRequest(w http.ResponseWriter, r *http.Request, ctx Context) {
74 // get client authentication
75 auth, err := getClientAuth(r, ctx.Config.AllowClientSecretInParams)
76 if err != nil {
77 ctx.RenderJSONError(w, ErrorInvalidClient, "Invalid client auth.", ctx.Config.DocumentationDomain)
78 return
79 }
81 code := r.Form.Get("code")
82 // "code" is required
83 if code == "" {
84 ctx.RenderJSONError(w, ErrorInvalidRequest, "Code must be supplied.", ctx.Config.DocumentationDomain)
85 return
86 }
88 // must have a valid client
89 client, err := getClient(auth, ctx)
90 if err != nil {
91 if err == ClientNotFoundError || err == InvalidClientError {
92 ctx.RenderJSONError(w, ErrorInvalidClient, "Invalid client auth.", ctx.Config.DocumentationDomain)
93 return
94 }
95 ctx.RenderJSONError(w, ErrorServerError, "Internal server error.", ctx.Config.DocumentationDomain)
96 return
97 }
99 // must be a valid authorization code
100 authData, err := ctx.Tokens.GetAuthorization(code)
101 if err != nil {
102 if err == AuthorizationNotFoundError {
103 ctx.RenderJSONError(w, ErrorInvalidGrant, "Invalid authorization.", ctx.Config.DocumentationDomain)
104 return
105 }
106 ctx.RenderJSONError(w, ErrorServerError, "Internal server error.", ctx.Config.DocumentationDomain)
107 return
108 }
109 if authData.RedirectURI == "" {
110 ctx.RenderJSONError(w, ErrorInvalidGrant, "Invalid redirect on grant.", ctx.Config.DocumentationDomain)
111 return
112 }
113 if authData.IsExpired() {
114 ctx.RenderJSONError(w, ErrorInvalidGrant, "Authorization is expired.", ctx.Config.DocumentationDomain)
115 return
116 }
118 // code must be from the client
119 if !authData.Client.ID.Equal(client.ID) {
120 ctx.RenderJSONError(w, ErrorInvalidGrant, "Grant issued to another client.", ctx.Config.DocumentationDomain)
121 return
122 }
124 // check redirect uri
125 redirectURI := r.Form.Get("redirect_uri")
126 if redirectURI == "" {
127 redirectURI = client.RedirectURI
128 }
129 if err = validateURI(client.RedirectURI, redirectURI); err != nil {
130 ctx.RenderJSONError(w, ErrorInvalidGrant, "Redirect URI doesn't match client.", ctx.Config.DocumentationDomain)
131 return
132 }
133 if authData.RedirectURI != redirectURI {
134 ctx.RenderJSONError(w, ErrorInvalidGrant, "Redirect URI doesn't match auth redirect.", ctx.Config.DocumentationDomain)
135 return
136 }
138 data := AccessData{
139 AuthRequest: AuthRequest{
140 Client: client,
141 RedirectURI: redirectURI,
142 Scope: authData.Scope,
143 },
144 PreviousAuthorizeData: &authData,
145 }
147 err = fillTokens(&data, true, ctx)
148 if err != nil {
149 ctx.RenderJSONError(w, ErrorServerError, "Internal server error.", ctx.Config.DocumentationDomain)
150 return
151 }
152 ctx.RenderJSONToken(w, data)
153 }
155 func handleRefreshTokenRequest(w http.ResponseWriter, r *http.Request, ctx Context) {
156 // get client authentication
157 auth, err := getClientAuth(r, ctx.Config.AllowClientSecretInParams)
159 if err != nil {
160 ctx.RenderJSONError(w, ErrorInvalidClient, "Invalid client auth.", ctx.Config.DocumentationDomain)
161 return
162 }
164 code := r.Form.Get("refresh_token")
166 // "refresh_token" is required
167 if code == "" {
168 ctx.RenderJSONError(w, ErrorInvalidRequest, "Missing refresh token.", ctx.Config.DocumentationDomain)
169 return
170 }
172 // must have a valid client
173 client, err := getClient(auth, ctx)
174 if err != nil {
175 if err == ClientNotFoundError || err == InvalidClientError {
176 ctx.RenderJSONError(w, ErrorInvalidClient, "Invalid client auth.", ctx.Config.DocumentationDomain)
177 return
178 }
179 ctx.RenderJSONError(w, ErrorServerError, "Internal server error.", ctx.Config.DocumentationDomain)
180 return
181 }
183 // must be a valid refresh code
184 refreshData, err := ctx.Tokens.GetRefresh(code)
185 if err != nil {
186 if err == TokenNotFoundError {
187 ctx.RenderJSONError(w, ErrorInvalidGrant, "Refresh token not valid.", ctx.Config.DocumentationDomain)
188 return
189 }
190 ctx.RenderJSONError(w, ErrorServerError, "Internal server error.", ctx.Config.DocumentationDomain)
191 return
192 }
194 // client must be the same as the previous token
195 if !refreshData.Client.ID.Equal(client.ID) {
196 ctx.RenderJSONError(w, ErrorInvalidGrant, "Refresh token issued to another client.", ctx.Config.DocumentationDomain)
197 return
198 }
200 scope := r.Form.Get("scope")
201 if scope == "" {
202 scope = refreshData.Scope
203 }
205 data := AccessData{
206 AuthRequest: AuthRequest{
207 Client: client,
208 Scope: scope,
209 },
210 PreviousAccessData: &refreshData,
211 }
212 err = fillTokens(&data, true, ctx)
213 if err != nil {
214 ctx.RenderJSONError(w, ErrorServerError, "Internal server error.", ctx.Config.DocumentationDomain)
215 return
216 }
217 ctx.RenderJSONToken(w, data)
218 }
220 func handlePasswordRequest(w http.ResponseWriter, r *http.Request, ctx Context) {
221 // get client authentication
222 auth, err := getClientAuth(r, ctx.Config.AllowClientSecretInParams)
223 if err != nil {
224 ctx.RenderJSONError(w, ErrorInvalidClient, "Invalid client auth.", ctx.Config.DocumentationDomain)
225 return
226 }
228 username := r.Form.Get("username")
229 password := r.Form.Get("password")
230 scope := r.Form.Get("scope")
232 // "username" and "password" is required
233 if username == "" || password == "" {
234 ctx.RenderJSONError(w, ErrorInvalidRequest, "Missing credentials.", ctx.Config.DocumentationDomain)
235 return
236 }
238 // must have a valid client
239 client, err := getClient(auth, ctx)
240 if err != nil {
241 if err == ClientNotFoundError || err == InvalidClientError {
242 ctx.RenderJSONError(w, ErrorInvalidClient, "Invalid client auth.", ctx.Config.DocumentationDomain)
243 return
244 }
245 ctx.RenderJSONError(w, ErrorServerError, "Internal server error.", ctx.Config.DocumentationDomain)
246 return
247 }
249 _, err = ctx.Profiles.GetProfile(username, password)
250 if err != nil {
251 if err == ProfileNotFoundError {
252 ctx.RenderJSONError(w, ErrorInvalidGrant, "Invalid credentials.", ctx.Config.DocumentationDomain)
253 return
254 }
255 ctx.RenderJSONError(w, ErrorServerError, "Internal server error.", ctx.Config.DocumentationDomain)
256 return
257 }
259 data := AccessData{
260 AuthRequest: AuthRequest{
261 Client: client,
262 Scope: scope,
263 },
264 }
266 err = fillTokens(&data, true, ctx)
267 if err != nil {
268 ctx.RenderJSONError(w, ErrorServerError, "Internal server error.", ctx.Config.DocumentationDomain)
269 return
270 }
271 ctx.RenderJSONToken(w, data)
272 }
274 func handleClientCredentialsRequest(w http.ResponseWriter, r *http.Request, ctx Context) {
275 // get client authentication
276 auth, err := getClientAuth(r, ctx.Config.AllowClientSecretInParams)
277 if err != nil {
278 ctx.RenderJSONError(w, ErrorInvalidClient, "Invalid client auth.", ctx.Config.DocumentationDomain)
279 return
280 }
282 scope := r.Form.Get("scope")
284 // must have a valid client
285 client, err := getClient(auth, ctx)
286 if err != nil {
287 if err == ClientNotFoundError || err == InvalidClientError {
288 ctx.RenderJSONError(w, ErrorInvalidClient, "Invalid client auth.", ctx.Config.DocumentationDomain)
289 return
290 }
291 ctx.RenderJSONError(w, ErrorServerError, "Internal server error.", ctx.Config.DocumentationDomain)
292 return
293 }
295 data := AccessData{
296 AuthRequest: AuthRequest{
297 Client: client,
298 Scope: scope,
299 },
300 }
302 err = fillTokens(&data, true, ctx)
303 if err != nil {
304 ctx.RenderJSONError(w, ErrorServerError, "Internal server error.", ctx.Config.DocumentationDomain)
305 return
306 }
307 ctx.RenderJSONToken(w, data)
308 }
310 func fillTokens(data *AccessData, includeRefresh bool, ctx Context) error {
311 var err error
313 // generate access token
314 data.AccessToken = newToken()
315 if includeRefresh {
316 data.RefreshToken = newToken()
317 }
319 // save access token
320 err = ctx.Tokens.SaveAccess(*data)
321 if err != nil {
322 // TODO: log error
323 return InternalServerError
324 }
326 // remove authorization token
327 if data.PreviousAuthorizeData != nil {
328 err = ctx.Tokens.RemoveAuthorization(data.PreviousAuthorizeData.Code)
329 if err != nil {
330 // TODO: log error
331 }
332 }
334 // remove previous access token
335 if data.PreviousAccessData != nil {
336 if data.PreviousAccessData.RefreshToken != "" {
337 err = ctx.Tokens.RemoveRefresh(data.PreviousAccessData.RefreshToken)
338 if err != nil {
339 // TODO: log error
340 }
341 }
342 err = ctx.Tokens.RemoveAccess(data.PreviousAccessData.AccessToken)
343 if err != nil {
344 // TODO: log error
345 }
346 }
348 data.TokenType = ctx.Config.TokenType
349 data.ExpiresIn = ctx.Config.AccessExpiration
350 data.CreatedAt = time.Now()
351 return nil
352 }
354 func (data AccessData) GetRedirect(fragment bool) (string, error) {
355 u, err := url.Parse(data.RedirectURI)
356 if err != nil {
357 return "", err
358 }
360 // add parameters
361 q := u.Query()
362 q.Set("access_token", data.AccessToken)
363 q.Set("token_type", data.TokenType)
364 q.Set("expires_in", strconv.FormatInt(int64(data.ExpiresIn), 10))
365 if data.RefreshToken != "" {
366 q.Set("refresh_token", data.RefreshToken)
367 }
368 if data.Scope != "" {
369 q.Set("scope", data.Scope)
370 }
371 if len(data.ProfileID) > 0 {
372 q.Set("profile", data.ProfileID.String())
373 }
374 if fragment {
375 u.RawQuery = ""
376 u.Fragment = q.Encode()
377 } else {
378 u.RawQuery = q.Encode()
379 }
381 return u.String(), nil
382 }
384 // getClient looks up and authenticates the basic auth using the given
385 // storage. Sets an error on the response if auth fails or a server error occurs.
386 func getClient(auth BasicAuth, ctx Context) (Client, error) {
387 id, err := uuid.Parse(auth.Username)
388 if err != nil {
389 return Client{}, err
390 }
391 client, err := ctx.Clients.GetClient(id)
392 if err != nil {
393 if err == ClientNotFoundError {
394 return Client{}, err
395 }
396 // TODO: log error
397 return Client{}, InternalServerError
398 }
399 if client.Secret != auth.Password {
400 return Client{}, InvalidClientError
401 }
402 if client.RedirectURI == "" {
403 return Client{}, InvalidClientError
404 }
405 return client, nil
406 }