auth
123:0a1e16b9c141 Browse Files
Refactor verifyClient, implement refresh tokens. Refactor verifyClient into verifyClient and getClientAuth. We moved verifyClient out of each of the GrantType's validation functions and into the access token endpoint, where it will be called before the GrantType's validation function. Yay, less code repetition. And seeing as we always want to verify the client, that seems like a good way to prevent things like 118a69954621 from happening. This did, however, force us to add an AllowsPublic property to the GrantType, so the token endpoint knows whether or not a public Client is valid for any given GrantType. We also implemented the refresh token grant type, which required adding ClientID and RefreshRevoked as properties on the Token type. We need ClientID because we need to constrain refresh tokens to the client that issued them. We also should probably keep track of which tokens belong to which clients, just as a general rule of thumb. RefreshRevoked had to be created, next to Revoked, because the AccessToken could be revoked and the RefreshToken still valid, or vice versa. Notably, when you issue a new refresh token, the old one is revoked, but the access token is still valid. It remains to be seen whether this is a good way to track things or not. The number of duplicated properties lead me to believe our type is not a great representation of the underlying concepts.
authcode.go client.go client_test.go oauth2.go session.go token.go
1.1 --- a/authcode.go Sun Jan 18 03:23:20 2015 -0500 1.2 +++ b/authcode.go Sun Jan 18 04:54:02 2015 -0500 1.3 @@ -15,6 +15,7 @@ 1.4 Invalidate: authCodeGrantInvalidate, 1.5 IssuesRefresh: true, 1.6 ReturnToken: RenderJSONToken, 1.7 + AllowsPublic: true, 1.8 }) 1.9 } 1.10 1.11 @@ -101,8 +102,8 @@ 1.12 renderJSONError(enc, "invalid_request") 1.13 return 1.14 } 1.15 - clientID, success := verifyClient(w, r, true, context) 1.16 - if !success { 1.17 + clientID, _, ok := getClientAuth(w, r, true) 1.18 + if !ok { 1.19 return 1.20 } 1.21 authCode, err := context.GetAuthorizationCode(code)
2.1 --- a/client.go Sun Jan 18 03:23:20 2015 -0500 2.2 +++ b/client.go Sun Jan 18 04:54:02 2015 -0500 2.3 @@ -23,6 +23,7 @@ 2.4 Invalidate: nil, 2.5 IssuesRefresh: true, 2.6 ReturnToken: RenderJSONToken, 2.7 + AllowsPublic: false, 2.8 }) 2.9 } 2.10 2.11 @@ -136,26 +137,45 @@ 2.12 return nil 2.13 } 2.14 2.15 -func verifyClient(w http.ResponseWriter, r *http.Request, allowPublic bool, context Context) (uuid.ID, bool) { 2.16 +func getClientAuth(w http.ResponseWriter, r *http.Request, allowPublic bool) (uuid.ID, string, bool) { 2.17 enc := json.NewEncoder(w) 2.18 clientIDStr, clientSecret, fromAuthHeader := r.BasicAuth() 2.19 if !fromAuthHeader { 2.20 - if !allowPublic { 2.21 - w.WriteHeader(http.StatusBadRequest) 2.22 - renderJSONError(enc, "unauthorized_client") 2.23 - return nil, false 2.24 - } 2.25 clientIDStr = r.PostFormValue("client_id") 2.26 } 2.27 - clientID, err := uuid.Parse(clientIDStr) 2.28 - if err != nil { 2.29 + if clientIDStr == "" { 2.30 w.WriteHeader(http.StatusUnauthorized) 2.31 if fromAuthHeader { 2.32 w.Header().Set("WWW-Authenticate", "Basic") 2.33 } 2.34 renderJSONError(enc, "invalid_client") 2.35 + return nil, "", false 2.36 + } 2.37 + clientID, err := uuid.Parse(clientIDStr) 2.38 + if err != nil { 2.39 + log.Println("Error decoding client ID:", err) 2.40 + w.WriteHeader(http.StatusUnauthorized) 2.41 + if fromAuthHeader { 2.42 + w.Header().Set("WWW-Authenticate", "Basic") 2.43 + } 2.44 + renderJSONError(enc, "invalid_client") 2.45 + return nil, "", false 2.46 + } 2.47 + if !allowPublic && !fromAuthHeader { 2.48 + w.WriteHeader(http.StatusBadRequest) 2.49 + renderJSONError(enc, "unauthorized_client") 2.50 + return nil, "", false 2.51 + } 2.52 + return clientID, clientSecret, true 2.53 +} 2.54 + 2.55 +func verifyClient(w http.ResponseWriter, r *http.Request, allowPublic bool, context Context) (uuid.ID, bool) { 2.56 + enc := json.NewEncoder(w) 2.57 + clientID, clientSecret, ok := getClientAuth(w, r, allowPublic) 2.58 + if !ok { 2.59 return nil, false 2.60 } 2.61 + _, _, fromAuthHeader := r.BasicAuth() 2.62 client, err := context.GetClient(clientID) 2.63 if err == ErrClientNotFound { 2.64 w.WriteHeader(http.StatusUnauthorized) 2.65 @@ -464,10 +484,6 @@ 2.66 2.67 func clientCredentialsValidate(w http.ResponseWriter, r *http.Request, context Context) (scope string, profileID uuid.ID, valid bool) { 2.68 scope = r.PostFormValue("scope") 2.69 - _, success := verifyClient(w, r, true, context) 2.70 - if !success { 2.71 - return 2.72 - } 2.73 valid = true 2.74 return 2.75 }
3.1 --- a/client_test.go Sun Jan 18 03:23:20 2015 -0500 3.2 +++ b/client_test.go Sun Jan 18 04:54:02 2015 -0500 3.3 @@ -515,10 +515,10 @@ 3.4 if resp != nil { 3.5 t.Error("Expected nil client ID, got", resp) 3.6 } 3.7 - if w.Code != http.StatusBadRequest { 3.8 - t.Errorf("Expected status code of %d, got %d", http.StatusBadRequest, w.Code) 3.9 + if w.Code != http.StatusUnauthorized { 3.10 + t.Errorf("Expected status code of %d, got %d", http.StatusUnauthorized, w.Code) 3.11 } 3.12 - expectedBody := `{"error":"unauthorized_client"}` 3.13 + expectedBody := `{"error":"invalid_client"}` 3.14 if expectedBody != strings.TrimSpace(w.Body.String()) { 3.15 t.Errorf("Expected body of `%s`, got `%s`", expectedBody, strings.TrimSpace(w.Body.String())) 3.16 }
4.1 --- a/oauth2.go Sun Jan 18 03:23:20 2015 -0500 4.2 +++ b/oauth2.go Sun Jan 18 04:54:02 2015 -0500 4.3 @@ -56,6 +56,9 @@ 4.4 // IssuesRefresh determines whether the GrantType should yield a refresh token as well as an access token. If true, the client 4.5 // will be issued a refresh token. 4.6 // 4.7 +// AllowsPublic determines whether the GrantType should allow public clients to use that grant. If true, clients without 4.8 +// credentials will be able to use the grant to obtain a token. 4.9 +// 4.10 // The ReturnToken will be called when a token is created and needs to be returned to the client. If it returns true, the token 4.11 // was successfully returned and the Invalidate function will be called asynchronously. 4.12 type GrantType struct { 4.13 @@ -63,6 +66,7 @@ 4.14 Invalidate func(r *http.Request, context Context) error 4.15 ReturnToken func(w http.ResponseWriter, r *http.Request, token Token, context Context) bool 4.16 IssuesRefresh bool 4.17 + AllowsPublic bool 4.18 } 4.19 4.20 type tokenResponse struct { 4.21 @@ -356,6 +360,10 @@ 4.22 renderJSONError(enc, "invalid_request") 4.23 return 4.24 } 4.25 + clientID, success := verifyClient(w, r, gt.AllowsPublic, context) 4.26 + if !success { 4.27 + return 4.28 + } 4.29 scope, profileID, valid := gt.Validate(w, r, context) 4.30 if !valid { 4.31 return 4.32 @@ -373,6 +381,7 @@ 4.33 TokenType: "bearer", 4.34 Scope: scope, 4.35 ProfileID: profileID, 4.36 + ClientID: clientID, 4.37 } 4.38 err := context.SaveToken(token) 4.39 if err != nil { 4.40 @@ -384,6 +393,3 @@ 4.41 go gt.Invalidate(r, context) 4.42 } 4.43 } 4.44 - 4.45 -// TODO(paddy): implicit grant for access token 4.46 -// TODO(paddy): exchange refresh token for access token
5.1 --- a/session.go Sun Jan 18 03:23:20 2015 -0500 5.2 +++ b/session.go Sun Jan 18 04:54:02 2015 -0500 5.3 @@ -286,10 +286,6 @@ 5.4 username := r.PostFormValue("username") 5.5 password := r.PostFormValue("password") 5.6 scope = r.PostFormValue("scope") 5.7 - _, success := verifyClient(w, r, false, context) 5.8 - if !success { 5.9 - return 5.10 - } 5.11 profile, err := authenticate(username, password, context) 5.12 if err != nil { 5.13 if err == ErrIncorrectAuth || err == ErrProfileCompromised || err == ErrProfileLocked {
6.1 --- a/token.go Sun Jan 18 03:23:20 2015 -0500 6.2 +++ b/token.go Sun Jan 18 04:54:02 2015 -0500 6.3 @@ -1,7 +1,10 @@ 6.4 package auth 6.5 6.6 import ( 6.7 + "encoding/json" 6.8 "errors" 6.9 + "log" 6.10 + "net/http" 6.11 "time" 6.12 6.13 "code.secondbit.org/uuid.hg" 6.14 @@ -12,6 +15,15 @@ 6.15 defaultRefreshTokenExpiration = 86400 // one day 6.16 ) 6.17 6.18 +func init() { 6.19 + RegisterGrantType("refresh_token", GrantType{ 6.20 + Validate: refreshTokenValidate, 6.21 + Invalidate: refreshTokenInvalidate, 6.22 + IssuesRefresh: true, 6.23 + ReturnToken: RenderJSONToken, 6.24 + }) 6.25 +} 6.26 + 6.27 var ( 6.28 // ErrNoTokenStore is returned when a Context tries to act on a tokenStore without setting one first. 6.29 ErrNoTokenStore = errors.New("no tokenStore was specified for the Context") 6.30 @@ -34,7 +46,9 @@ 6.31 TokenType string 6.32 Scope string 6.33 ProfileID uuid.ID 6.34 + ClientID uuid.ID 6.35 Revoked bool 6.36 + RefreshRevoked bool 6.37 } 6.38 6.39 type tokenStore interface { 6.40 @@ -119,7 +133,11 @@ 6.41 if !ok { 6.42 return ErrTokenNotFound 6.43 } 6.44 - t.Revoked = true 6.45 + if refresh { 6.46 + t.RefreshRevoked = true 6.47 + } else { 6.48 + t.Revoked = true 6.49 + } 6.50 m.tokens[token] = t 6.51 return nil 6.52 } 6.53 @@ -146,3 +164,54 @@ 6.54 } 6.55 return tokens, nil 6.56 } 6.57 + 6.58 +func refreshTokenValidate(w http.ResponseWriter, r *http.Request, context Context) (scope string, profileID uuid.ID, valid bool) { 6.59 + enc := json.NewEncoder(w) 6.60 + refresh := r.PostFormValue("refresh_token") 6.61 + if refresh == "" { 6.62 + w.WriteHeader(http.StatusBadRequest) 6.63 + renderJSONError(enc, "invalid_request") 6.64 + return 6.65 + } 6.66 + token, err := context.GetToken(refresh, true) 6.67 + if err != nil { 6.68 + if err == ErrTokenNotFound { 6.69 + w.WriteHeader(http.StatusBadRequest) 6.70 + renderJSONError(enc, "invalid_grant") 6.71 + return 6.72 + } 6.73 + log.Println("Error exchanging refresh token:", err) 6.74 + w.WriteHeader(http.StatusInternalServerError) 6.75 + renderJSONError(enc, "server_error") 6.76 + return 6.77 + } 6.78 + clientID, _, ok := getClientAuth(w, r, true) 6.79 + if !ok { 6.80 + return 6.81 + } 6.82 + if !token.ClientID.Equal(clientID) { 6.83 + w.WriteHeader(http.StatusBadRequest) 6.84 + renderJSONError(enc, "invalid_grant") 6.85 + return 6.86 + } 6.87 + if token.RefreshRevoked { 6.88 + w.WriteHeader(http.StatusBadRequest) 6.89 + renderJSONError(enc, "invalid_grant") 6.90 + return 6.91 + } 6.92 + expires := token.Created.Add(time.Duration(token.RefreshExpiresIn) * time.Second) 6.93 + if expires.Before(time.Now()) { 6.94 + w.WriteHeader(http.StatusBadRequest) 6.95 + renderJSONError(enc, "invalid_grant") 6.96 + return 6.97 + } 6.98 + return token.Scope, token.ProfileID, true 6.99 +} 6.100 + 6.101 +func refreshTokenInvalidate(r *http.Request, context Context) error { 6.102 + refresh := r.PostFormValue("refresh_token") 6.103 + if refresh == "" { 6.104 + return ErrTokenNotFound 6.105 + } 6.106 + return context.RevokeToken(refresh, true) 6.107 +}