auth
auth/access.go
Consistently handle context in client storage interface. Let's at least be consistent about passing or not passing context to the client storage interface. Most our methods don't take the context, so let's just remove it.
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 if ctx.Log != nil {
323 ctx.Log.Printf("Error writing access token: %s\n", err)
324 }
325 return InternalServerError
326 }
328 // remove authorization token
329 if data.PreviousAuthorizeData != nil {
330 err = ctx.Tokens.RemoveAuthorization(data.PreviousAuthorizeData.Code)
331 if err != nil && ctx.Log != nil {
332 ctx.Log.Printf("Error removing previous auth data (%s): %s\n", data.PreviousAuthorizeData.Code, err)
333 }
334 }
336 // remove previous access token
337 if data.PreviousAccessData != nil {
338 if data.PreviousAccessData.RefreshToken != "" {
339 err = ctx.Tokens.RemoveRefresh(data.PreviousAccessData.RefreshToken)
340 if err != nil && ctx.Log != nil {
341 ctx.Log.Printf("Error removing previous refresh token (%s): %s\n", data.PreviousAccessData.RefreshToken, err)
342 }
343 }
344 err = ctx.Tokens.RemoveAccess(data.PreviousAccessData.AccessToken)
345 if err != nil && ctx.Log != nil {
346 ctx.Log.Printf("Error removing previous access token (%s): %s\n", data.PreviousAccessData.AccessToken, err)
347 }
348 }
350 data.TokenType = ctx.Config.TokenType
351 data.ExpiresIn = ctx.Config.AccessExpiration
352 data.CreatedAt = time.Now()
353 return nil
354 }
356 func (data AccessData) GetRedirect(fragment bool) (string, error) {
357 u, err := url.Parse(data.RedirectURI)
358 if err != nil {
359 return "", err
360 }
362 // add parameters
363 q := u.Query()
364 q.Set("access_token", data.AccessToken)
365 q.Set("token_type", data.TokenType)
366 q.Set("expires_in", strconv.FormatInt(int64(data.ExpiresIn), 10))
367 if data.RefreshToken != "" {
368 q.Set("refresh_token", data.RefreshToken)
369 }
370 if data.Scope != "" {
371 q.Set("scope", data.Scope)
372 }
373 if len(data.ProfileID) > 0 {
374 q.Set("profile", data.ProfileID.String())
375 }
376 if fragment {
377 u.RawQuery = ""
378 u.Fragment = q.Encode()
379 } else {
380 u.RawQuery = q.Encode()
381 }
383 return u.String(), nil
384 }
386 // getClient looks up and authenticates the basic auth using the given
387 // storage. Sets an error on the response if auth fails or a server error occurs.
388 func getClient(auth BasicAuth, ctx Context) (Client, error) {
389 id, err := uuid.Parse(auth.Username)
390 if err != nil {
391 return Client{}, err
392 }
393 client, err := ctx.Clients.GetClient(id)
394 if err != nil {
395 if err == ClientNotFoundError {
396 return Client{}, err
397 }
398 if ctx.Log != nil {
399 ctx.Log.Printf("Error retrieving client %s: %s", id, err)
400 }
401 return Client{}, InternalServerError
402 }
403 if client.Secret != auth.Password {
404 return Client{}, InvalidClientError
405 }
406 if client.RedirectURI == "" {
407 return Client{}, InvalidClientError
408 }
409 return client, nil
410 }