auth
auth/access.go
Write responses. Start writing JSON responses when access tokens are requested.
1 package oauth2
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 // TODO: return error
92 return
93 }
95 // must be a valid authorization code
96 authData, err := ctx.Tokens.GetAuthorization(code)
97 if err != nil {
98 // TODO: return error
99 return
100 }
101 /*if authData.Client.RedirectURI == "" {
102 return
103 }*/ // TODO: should this even be checked?
104 if authData.IsExpired() {
105 ctx.RenderJSONError(w, ErrorInvalidGrant, "Authorization is expired.", ctx.Config.DocumentationDomain)
106 return
107 }
109 // code must be from the client
110 if !authData.Client.ID.Equal(client.ID) {
111 ctx.RenderJSONError(w, ErrorInvalidGrant, "Grant issued to another client.", ctx.Config.DocumentationDomain)
112 return
113 }
115 // check redirect uri
116 redirectURI := r.Form.Get("redirect_uri")
117 if redirectURI == "" {
118 redirectURI = client.RedirectURI
119 }
120 if err = validateURI(client.RedirectURI, redirectURI); err != nil {
121 ctx.RenderJSONError(w, ErrorInvalidGrant, "Redirect URI doesn't match client.", ctx.Config.DocumentationDomain)
122 return
123 }
124 if authData.RedirectURI != redirectURI {
125 ctx.RenderJSONError(w, ErrorInvalidGrant, "Redirect URI doesn't match auth redirect.", ctx.Config.DocumentationDomain)
126 return
127 }
129 data := AccessData{
130 AuthRequest: AuthRequest{
131 Client: client,
132 RedirectURI: redirectURI,
133 Scope: authData.Scope,
134 },
135 PreviousAuthorizeData: &authData,
136 }
138 err = fillTokens(&data, true, ctx)
139 if err != nil {
140 // TODO: return error
141 return
142 }
143 ctx.RenderJSONToken(w, data)
144 }
146 func handleRefreshTokenRequest(w http.ResponseWriter, r *http.Request, ctx Context) {
147 // get client authentication
148 auth, err := getClientAuth(r, ctx.Config.AllowClientSecretInParams)
150 if err != nil {
151 ctx.RenderJSONError(w, ErrorInvalidClient, "Invalid client auth.", ctx.Config.DocumentationDomain)
152 return
153 }
155 code := r.Form.Get("refresh_token")
157 // "refresh_token" is required
158 if code == "" {
159 ctx.RenderJSONError(w, ErrorInvalidRequest, "Missing refresh token.", ctx.Config.DocumentationDomain)
160 return
161 }
163 // must have a valid client
164 client, err := getClient(auth, ctx)
165 if err != nil {
166 // TODO: return error
167 return
168 }
170 // must be a valid refresh code
171 refreshData, err := ctx.Tokens.GetRefresh(code)
172 if err != nil {
173 // TODO: return error
174 return
175 }
176 if refreshData.Client.RedirectURI == "" {
177 // TODO: should this even be checked?
178 return
179 }
181 // client must be the same as the previous token
182 if !refreshData.Client.ID.Equal(client.ID) {
183 ctx.RenderJSONError(w, ErrorInvalidGrant, "Refresh token issued to another client.", ctx.Config.DocumentationDomain)
184 return
185 }
187 // set rest of data
188 redirectURI := r.Form.Get("redirect_uri")
189 if redirectURI == "" {
190 redirectURI = refreshData.RedirectURI
191 }
192 // TODO: check redirect URI?
194 scope := r.Form.Get("scope")
195 if scope == "" {
196 scope = refreshData.Scope
197 }
199 data := AccessData{
200 AuthRequest: AuthRequest{
201 Client: client,
202 RedirectURI: redirectURI,
203 Scope: scope,
204 },
205 PreviousAccessData: &refreshData,
206 }
207 err = fillTokens(&data, true, ctx)
208 if err != nil {
209 // TODO: return error
210 return
211 }
212 ctx.RenderJSONToken(w, data)
213 }
215 func handlePasswordRequest(w http.ResponseWriter, r *http.Request, ctx Context) {
216 // get client authentication
217 auth, err := getClientAuth(r, ctx.Config.AllowClientSecretInParams)
218 if err != nil {
219 ctx.RenderJSONError(w, ErrorInvalidClient, "Invalid client auth.", ctx.Config.DocumentationDomain)
220 return
221 }
223 username := r.Form.Get("username")
224 password := r.Form.Get("password")
225 scope := r.Form.Get("scope")
227 // "username" and "password" is required
228 if username == "" || password == "" {
229 ctx.RenderJSONError(w, ErrorInvalidRequest, "Missing credentials.", ctx.Config.DocumentationDomain)
230 return
231 }
233 // must have a valid client
234 client, err := getClient(auth, ctx)
235 if err != nil {
236 // TODO: return error
237 return
238 }
240 // set redirect uri
241 redirectURI := r.Form.Get("redirect_uri")
242 if redirectURI == "" {
243 redirectURI = client.RedirectURI
244 }
246 _, err = ctx.Profiles.GetProfile(username, password)
247 if err != nil {
248 // TODO: return error
249 return
250 }
252 data := AccessData{
253 AuthRequest: AuthRequest{
254 Client: client,
255 RedirectURI: redirectURI,
256 Scope: scope,
257 },
258 }
260 err = fillTokens(&data, true, ctx)
261 if err != nil {
262 // TODO: return error
263 return
264 }
265 ctx.RenderJSONToken(w, data)
266 }
268 func handleClientCredentialsRequest(w http.ResponseWriter, r *http.Request, ctx Context) {
269 // get client authentication
270 auth, err := getClientAuth(r, ctx.Config.AllowClientSecretInParams)
271 if err != nil {
272 ctx.RenderJSONError(w, ErrorInvalidClient, "Invalid client auth.", ctx.Config.DocumentationDomain)
273 return
274 }
276 scope := r.Form.Get("scope")
278 // must have a valid client
279 client, err := getClient(auth, ctx)
280 if err != nil {
281 // TODO: return error
282 return
283 }
285 // set redirect uri
286 redirectURI := r.Form.Get("redirect_uri")
287 if redirectURI == "" {
288 redirectURI = client.RedirectURI
289 }
291 data := AccessData{
292 AuthRequest: AuthRequest{
293 Client: client,
294 RedirectURI: redirectURI,
295 Scope: scope,
296 },
297 }
299 err = fillTokens(&data, true, ctx)
300 if err != nil {
301 // TODO: return error
302 return
303 }
304 ctx.RenderJSONToken(w, data)
305 }
307 func fillTokens(data *AccessData, includeRefresh bool, ctx Context) error {
308 var err error
310 // generate access token
311 data.AccessToken = newToken()
312 if includeRefresh {
313 data.RefreshToken = newToken()
314 }
316 // save access token
317 err = ctx.Tokens.SaveAccess(*data)
318 if err != nil {
319 // TODO: abstract out error
320 return err
321 }
323 // remove authorization token
324 if data.PreviousAuthorizeData != nil {
325 err = ctx.Tokens.RemoveAuthorization(data.PreviousAuthorizeData.Code)
326 if err != nil {
327 // TODO: log error
328 }
329 }
331 // remove previous access token
332 if data.PreviousAccessData != nil {
333 if data.PreviousAccessData.RefreshToken != "" {
334 err = ctx.Tokens.RemoveRefresh(data.PreviousAccessData.RefreshToken)
335 if err != nil {
336 // TODO: log error
337 }
338 }
339 err = ctx.Tokens.RemoveAccess(data.PreviousAccessData.AccessToken)
340 if err != nil {
341 // TODO: log error
342 }
343 }
345 data.TokenType = ctx.Config.TokenType
346 data.ExpiresIn = ctx.Config.AccessExpiration
347 data.CreatedAt = time.Now()
348 return nil
349 }
351 func (data AccessData) GetRedirect(fragment bool) (string, error) {
352 u, err := url.Parse(data.RedirectURI)
353 if err != nil {
354 return "", err
355 }
357 // add parameters
358 q := u.Query()
359 q.Set("access_token", data.AccessToken)
360 q.Set("token_type", data.TokenType)
361 q.Set("expires_in", strconv.FormatInt(int64(data.ExpiresIn), 10))
362 if data.RefreshToken != "" {
363 q.Set("refresh_token", data.RefreshToken)
364 }
365 if data.Scope != "" {
366 q.Set("scope", data.Scope)
367 }
368 if len(data.ProfileID) > 0 {
369 q.Set("profile", data.ProfileID.String())
370 }
371 if fragment {
372 u.RawQuery = ""
373 u.Fragment = q.Encode()
374 } else {
375 u.RawQuery = q.Encode()
376 }
378 return u.String(), nil
379 }
381 // getClient looks up and authenticates the basic auth using the given
382 // storage. Sets an error on the response if auth fails or a server error occurs.
383 func getClient(auth BasicAuth, ctx Context) (Client, error) {
384 id, err := uuid.Parse(auth.Username)
385 if err != nil {
386 return Client{}, err
387 }
388 client, err := ctx.Clients.GetClient(id)
389 if err != nil {
390 // TODO: abstract out errors
391 return Client{}, err
392 }
393 if client.Secret != auth.Password {
394 // TODO: return E_UNAUTHORIZED_CLIENT error
395 return Client{}, nil
396 }
397 if client.RedirectURI == "" {
398 // TODO: return E_UNAUTHORIZED_CLIENT error
399 return Client{}, nil
400 }
401 return client, nil
402 }