auth
auth/access.go
Render JSON errors. Start rendering JSON errors when obtaining an access token doesn't succeed.
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 // TODO: write 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 // TODO: write 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 // TODO: return error
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 // TODO: return error
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 }
266 // TODO: write data
267 }
269 func handleClientCredentialsRequest(w http.ResponseWriter, r *http.Request, ctx Context) {
270 // get client authentication
271 auth, err := getClientAuth(r, ctx.Config.AllowClientSecretInParams)
272 if err != nil {
273 // TODO: return error
274 return
275 }
277 scope := r.Form.Get("scope")
279 // must have a valid client
280 client, err := getClient(auth, ctx)
281 if err != nil {
282 // TODO: return error
283 return
284 }
286 // set redirect uri
287 redirectURI := r.Form.Get("redirect_uri")
288 if redirectURI == "" {
289 redirectURI = client.RedirectURI
290 }
292 data := AccessData{
293 AuthRequest: AuthRequest{
294 Client: client,
295 RedirectURI: redirectURI,
296 Scope: scope,
297 },
298 }
300 err = fillTokens(&data, true, ctx)
301 if err != nil {
302 // TODO: return error
303 return
304 }
306 // TODO: write data
307 }
309 func fillTokens(data *AccessData, includeRefresh bool, ctx Context) error {
310 var err error
312 // generate access token
313 data.AccessToken = newToken()
314 if includeRefresh {
315 data.RefreshToken = newToken()
316 }
318 // save access token
319 err = ctx.Tokens.SaveAccess(*data)
320 if err != nil {
321 // TODO: abstract out error
322 return err
323 }
325 // remove authorization token
326 if data.PreviousAuthorizeData != nil {
327 err = ctx.Tokens.RemoveAuthorization(data.PreviousAuthorizeData.Code)
328 if err != nil {
329 // TODO: log error
330 }
331 }
333 // remove previous access token
334 if data.PreviousAccessData != nil {
335 if data.PreviousAccessData.RefreshToken != "" {
336 err = ctx.Tokens.RemoveRefresh(data.PreviousAccessData.RefreshToken)
337 if err != nil {
338 // TODO: log error
339 }
340 }
341 err = ctx.Tokens.RemoveAccess(data.PreviousAccessData.AccessToken)
342 if err != nil {
343 // TODO: log error
344 }
345 }
347 data.TokenType = ctx.Config.TokenType
348 data.ExpiresIn = ctx.Config.AccessExpiration
349 data.CreatedAt = time.Now()
350 return nil
351 }
353 func (data AccessData) GetRedirect(fragment bool) (string, error) {
354 u, err := url.Parse(data.RedirectURI)
355 if err != nil {
356 return "", err
357 }
359 // add parameters
360 q := u.Query()
361 q.Set("access_token", data.AccessToken)
362 q.Set("token_type", data.TokenType)
363 q.Set("expires_in", strconv.FormatInt(int64(data.ExpiresIn), 10))
364 if data.RefreshToken != "" {
365 q.Set("refresh_token", data.RefreshToken)
366 }
367 if data.Scope != "" {
368 q.Set("scope", data.Scope)
369 }
370 if len(data.ProfileID) > 0 {
371 q.Set("profile", data.ProfileID.String())
372 }
373 if fragment {
374 u.RawQuery = ""
375 u.Fragment = q.Encode()
376 } else {
377 u.RawQuery = q.Encode()
378 }
380 return u.String(), nil
381 }
383 // getClient looks up and authenticates the basic auth using the given
384 // storage. Sets an error on the response if auth fails or a server error occurs.
385 func getClient(auth BasicAuth, ctx Context) (Client, error) {
386 id, err := uuid.Parse(auth.Username)
387 if err != nil {
388 return Client{}, err
389 }
390 client, err := ctx.Clients.GetClient(id)
391 if err != nil {
392 // TODO: abstract out errors
393 return Client{}, err
394 }
395 if client.Secret != auth.Password {
396 // TODO: return E_UNAUTHORIZED_CLIENT error
397 return Client{}, nil
398 }
399 if client.RedirectURI == "" {
400 // TODO: return E_UNAUTHORIZED_CLIENT error
401 return Client{}, nil
402 }
403 return client, nil
404 }