auth
auth/access.go
Handle more errors. Handle client errors. Match redirect handling with the spec.
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 if err == ClientNotFoundError || err == InvalidClientError {
92 ctx.RenderJSONError(w, ErrorInvalidClient, "Invalid client auth.", ctx.Config.DocumentationDomain)
93 } else {
94 ctx.RenderJSONError(w, ErrorServerError, "Internal server error.", ctx.Config.DocumentationDomain)
95 }
96 return
97 }
99 // must be a valid authorization code
100 authData, err := ctx.Tokens.GetAuthorization(code)
101 if err != nil {
102 // TODO: return error
103 return
104 }
105 if authData.RedirectURI == "" {
106 ctx.RenderJSONError(w, ErrorInvalidGrant, "Invalid redirect on grant.", ctx.Config.DocumentationDomain)
107 return
108 }
109 if authData.IsExpired() {
110 ctx.RenderJSONError(w, ErrorInvalidGrant, "Authorization is expired.", ctx.Config.DocumentationDomain)
111 return
112 }
114 // code must be from the client
115 if !authData.Client.ID.Equal(client.ID) {
116 ctx.RenderJSONError(w, ErrorInvalidGrant, "Grant issued to another client.", ctx.Config.DocumentationDomain)
117 return
118 }
120 // check redirect uri
121 redirectURI := r.Form.Get("redirect_uri")
122 if redirectURI == "" {
123 redirectURI = client.RedirectURI
124 }
125 if err = validateURI(client.RedirectURI, redirectURI); err != nil {
126 ctx.RenderJSONError(w, ErrorInvalidGrant, "Redirect URI doesn't match client.", ctx.Config.DocumentationDomain)
127 return
128 }
129 if authData.RedirectURI != redirectURI {
130 ctx.RenderJSONError(w, ErrorInvalidGrant, "Redirect URI doesn't match auth redirect.", ctx.Config.DocumentationDomain)
131 return
132 }
134 data := AccessData{
135 AuthRequest: AuthRequest{
136 Client: client,
137 RedirectURI: redirectURI,
138 Scope: authData.Scope,
139 },
140 PreviousAuthorizeData: &authData,
141 }
143 err = fillTokens(&data, true, ctx)
144 if err != nil {
145 // TODO: return error
146 return
147 }
148 ctx.RenderJSONToken(w, data)
149 }
151 func handleRefreshTokenRequest(w http.ResponseWriter, r *http.Request, ctx Context) {
152 // get client authentication
153 auth, err := getClientAuth(r, ctx.Config.AllowClientSecretInParams)
155 if err != nil {
156 ctx.RenderJSONError(w, ErrorInvalidClient, "Invalid client auth.", ctx.Config.DocumentationDomain)
157 return
158 }
160 code := r.Form.Get("refresh_token")
162 // "refresh_token" is required
163 if code == "" {
164 ctx.RenderJSONError(w, ErrorInvalidRequest, "Missing refresh token.", ctx.Config.DocumentationDomain)
165 return
166 }
168 // must have a valid client
169 client, err := getClient(auth, ctx)
170 if err != nil {
171 if err == ClientNotFoundError || err == InvalidClientError {
172 ctx.RenderJSONError(w, ErrorInvalidClient, "Invalid client auth.", ctx.Config.DocumentationDomain)
173 } else {
174 ctx.RenderJSONError(w, ErrorServerError, "Internal server error.", ctx.Config.DocumentationDomain)
175 }
176 return
177 }
179 // must be a valid refresh code
180 refreshData, err := ctx.Tokens.GetRefresh(code)
181 if err != nil {
182 // TODO: return error
183 return
184 }
186 // client must be the same as the previous token
187 if !refreshData.Client.ID.Equal(client.ID) {
188 ctx.RenderJSONError(w, ErrorInvalidGrant, "Refresh token issued to another client.", ctx.Config.DocumentationDomain)
189 return
190 }
192 scope := r.Form.Get("scope")
193 if scope == "" {
194 scope = refreshData.Scope
195 }
197 data := AccessData{
198 AuthRequest: AuthRequest{
199 Client: client,
200 Scope: scope,
201 },
202 PreviousAccessData: &refreshData,
203 }
204 err = fillTokens(&data, true, ctx)
205 if err != nil {
206 // TODO: return error
207 return
208 }
209 ctx.RenderJSONToken(w, data)
210 }
212 func handlePasswordRequest(w http.ResponseWriter, r *http.Request, ctx Context) {
213 // get client authentication
214 auth, err := getClientAuth(r, ctx.Config.AllowClientSecretInParams)
215 if err != nil {
216 ctx.RenderJSONError(w, ErrorInvalidClient, "Invalid client auth.", ctx.Config.DocumentationDomain)
217 return
218 }
220 username := r.Form.Get("username")
221 password := r.Form.Get("password")
222 scope := r.Form.Get("scope")
224 // "username" and "password" is required
225 if username == "" || password == "" {
226 ctx.RenderJSONError(w, ErrorInvalidRequest, "Missing credentials.", ctx.Config.DocumentationDomain)
227 return
228 }
230 // must have a valid client
231 client, err := getClient(auth, ctx)
232 if err != nil {
233 if err == ClientNotFoundError || err == InvalidClientError {
234 ctx.RenderJSONError(w, ErrorInvalidClient, "Invalid client auth.", ctx.Config.DocumentationDomain)
235 } else {
236 ctx.RenderJSONError(w, ErrorServerError, "Internal server error.", ctx.Config.DocumentationDomain)
237 }
238 return
239 }
241 _, err = ctx.Profiles.GetProfile(username, password)
242 if err != nil {
243 // TODO: return error
244 return
245 }
247 data := AccessData{
248 AuthRequest: AuthRequest{
249 Client: client,
250 Scope: scope,
251 },
252 }
254 err = fillTokens(&data, true, ctx)
255 if err != nil {
256 // TODO: return error
257 return
258 }
259 ctx.RenderJSONToken(w, data)
260 }
262 func handleClientCredentialsRequest(w http.ResponseWriter, r *http.Request, ctx Context) {
263 // get client authentication
264 auth, err := getClientAuth(r, ctx.Config.AllowClientSecretInParams)
265 if err != nil {
266 ctx.RenderJSONError(w, ErrorInvalidClient, "Invalid client auth.", ctx.Config.DocumentationDomain)
267 return
268 }
270 scope := r.Form.Get("scope")
272 // must have a valid client
273 client, err := getClient(auth, ctx)
274 if err != nil {
275 if err == ClientNotFoundError || err == InvalidClientError {
276 ctx.RenderJSONError(w, ErrorInvalidClient, "Invalid client auth.", ctx.Config.DocumentationDomain)
277 } else {
278 ctx.RenderJSONError(w, ErrorServerError, "Internal server error.", ctx.Config.DocumentationDomain)
279 }
280 return
281 }
283 data := AccessData{
284 AuthRequest: AuthRequest{
285 Client: client,
286 Scope: scope,
287 },
288 }
290 err = fillTokens(&data, true, ctx)
291 if err != nil {
292 // TODO: return error
293 return
294 }
295 ctx.RenderJSONToken(w, data)
296 }
298 func fillTokens(data *AccessData, includeRefresh bool, ctx Context) error {
299 var err error
301 // generate access token
302 data.AccessToken = newToken()
303 if includeRefresh {
304 data.RefreshToken = newToken()
305 }
307 // save access token
308 err = ctx.Tokens.SaveAccess(*data)
309 if err != nil {
310 // TODO: abstract out error
311 return err
312 }
314 // remove authorization token
315 if data.PreviousAuthorizeData != nil {
316 err = ctx.Tokens.RemoveAuthorization(data.PreviousAuthorizeData.Code)
317 if err != nil {
318 // TODO: log error
319 }
320 }
322 // remove previous access token
323 if data.PreviousAccessData != nil {
324 if data.PreviousAccessData.RefreshToken != "" {
325 err = ctx.Tokens.RemoveRefresh(data.PreviousAccessData.RefreshToken)
326 if err != nil {
327 // TODO: log error
328 }
329 }
330 err = ctx.Tokens.RemoveAccess(data.PreviousAccessData.AccessToken)
331 if err != nil {
332 // TODO: log error
333 }
334 }
336 data.TokenType = ctx.Config.TokenType
337 data.ExpiresIn = ctx.Config.AccessExpiration
338 data.CreatedAt = time.Now()
339 return nil
340 }
342 func (data AccessData) GetRedirect(fragment bool) (string, error) {
343 u, err := url.Parse(data.RedirectURI)
344 if err != nil {
345 return "", err
346 }
348 // add parameters
349 q := u.Query()
350 q.Set("access_token", data.AccessToken)
351 q.Set("token_type", data.TokenType)
352 q.Set("expires_in", strconv.FormatInt(int64(data.ExpiresIn), 10))
353 if data.RefreshToken != "" {
354 q.Set("refresh_token", data.RefreshToken)
355 }
356 if data.Scope != "" {
357 q.Set("scope", data.Scope)
358 }
359 if len(data.ProfileID) > 0 {
360 q.Set("profile", data.ProfileID.String())
361 }
362 if fragment {
363 u.RawQuery = ""
364 u.Fragment = q.Encode()
365 } else {
366 u.RawQuery = q.Encode()
367 }
369 return u.String(), nil
370 }
372 // getClient looks up and authenticates the basic auth using the given
373 // storage. Sets an error on the response if auth fails or a server error occurs.
374 func getClient(auth BasicAuth, ctx Context) (Client, error) {
375 id, err := uuid.Parse(auth.Username)
376 if err != nil {
377 return Client{}, err
378 }
379 client, err := ctx.Clients.GetClient(id)
380 if err != nil {
381 if err == ClientNotFoundError {
382 return Client{}, err
383 }
384 // TODO: log error
385 return Client{}, InternalServerError
386 }
387 if client.Secret != auth.Password {
388 return Client{}, InvalidClientError
389 }
390 if client.RedirectURI == "" {
391 return Client{}, InvalidClientError
392 }
393 return client, nil
394 }