auth
168:581c60f8dd23 Browse Files
Switch to a JWT approach. We're going to use a JWT as our access tokens (as discussed in &yet's excellent post https://blog.andyet.com/2015/05/12/micro-services-user-info-and-auth and my ensuing conversation with Fritzy). The benefit of this approach is that we can do authentication and even some authorization without touching the database at all. The drawback is that we can no longer revoke access tokens, only the refresh tokens that grant the access tokens. We need a new config variable to set our private key, used to sign the JWT. We get to remove our token handlers, as we no longer can revoke tokens, so there's no purpose in getting information about it or listing them. Our tokenStore revokeToken gets to be simplified, as it will only ever be used for refresh tokens now. We also updated our postgres and memstore implementations. We added a helper method for generating the signed "access token" (our JWT) and started using it in the places where we're creating a Token. We get to remove the `revoked` SQL column for the tokens table, and rename the `refresh_revoked` column to just be `revoked`. We shortened our access token expiration to 15 minutes instead of an hour, to deal with the token not being revokable.
authd/server.go config.go context.go oauth2.go oauth2_test.go profile.go request.go sql/postgres_init.sql token.go token_postgres.go token_test.go
1.1 --- a/authd/server.go Tue May 12 21:14:21 2015 -0400 1.2 +++ b/authd/server.go Fri May 15 19:44:40 2015 -0400 1.3 @@ -35,6 +35,7 @@ 1.4 } 1.5 config.Template = template.Must(template.New("base").ParseGlob("./templates/*.gotmpl")) 1.6 config.LoginURI = "/login" 1.7 + config.JWTPrivateKey = []byte(`secret`) 1.8 err := config.Init() 1.9 if err != nil { 1.10 log.Fatal(err) 1.11 @@ -55,7 +56,6 @@ 1.12 auth.RegisterSessionHandlers(router, context) 1.13 auth.RegisterProfileHandlers(router, context) 1.14 auth.RegisterClientHandlers(router, context) 1.15 - auth.RegisterTokenHandlers(router, context) 1.16 http.Handle("/", router) 1.17 log.Fatal(http.ListenAndServe(":8080", nil)) 1.18 }
2.1 --- a/config.go Tue May 12 21:14:21 2015 -0400 2.2 +++ b/config.go Fri May 15 19:44:40 2015 -0400 2.3 @@ -24,6 +24,7 @@ 2.4 ScopeStore scopeStore 2.5 Template *template.Template 2.6 LoginURI string 2.7 + JWTPrivateKey []byte 2.8 iterations int 2.9 secureCookie bool 2.10 }
3.1 --- a/context.go Tue May 12 21:14:21 2015 -0400 3.2 +++ b/context.go Fri May 15 19:44:40 2015 -0400 3.3 @@ -349,13 +349,12 @@ 3.4 } 3.5 3.6 // RevokeToken revokes the Token identfied by the passed token string from the tokenStore associated 3.7 -// with the context. If refresh is true, the token input should be compared against the refresh tokens, 3.8 -// not the access tokens. 3.9 -func (c Context) RevokeToken(token string, refresh bool) error { 3.10 +// with the context. 3.11 +func (c Context) RevokeToken(token string) error { 3.12 if c.tokens == nil { 3.13 return ErrNoTokenStore 3.14 } 3.15 - return c.tokens.revokeToken(token, refresh) 3.16 + return c.tokens.revokeToken(token) 3.17 } 3.18 3.19 // RevokeTokensByProfileID revokes the Tokens associated with the Profile identified by the passed ID in
4.1 --- a/oauth2.go Tue May 12 21:14:21 2015 -0400 4.2 +++ b/oauth2.go Fri May 15 19:44:40 2015 -0400 4.3 @@ -332,7 +332,6 @@ 4.4 q.Add("code", authCode.Code) 4.5 case "token": 4.6 token := Token{ 4.7 - AccessToken: uuid.NewID().String(), 4.8 Created: time.Now(), 4.9 CreatedFrom: "implicit", 4.10 ExpiresIn: defaultTokenExpiration, 4.11 @@ -341,7 +340,14 @@ 4.12 ProfileID: session.ProfileID, 4.13 ClientID: clientID, 4.14 } 4.15 - err := context.SaveToken(token) 4.16 + access, err := token.GenerateAccessToken(context.config.JWTPrivateKey) 4.17 + if err != nil { 4.18 + log.Printf("Error signing token: %+v\n", err) 4.19 + q.Add("error", "server_error") 4.20 + break 4.21 + } 4.22 + token.AccessToken = access 4.23 + err = context.SaveToken(token) 4.24 if err != nil { 4.25 log.Println("Error saving token:", err) 4.26 q.Add("error", "server_error") 4.27 @@ -420,7 +426,15 @@ 4.28 ProfileID: profileID, 4.29 ClientID: clientID, 4.30 } 4.31 - err := context.SaveToken(token) 4.32 + access, err := token.GenerateAccessToken(context.config.JWTPrivateKey) 4.33 + if err != nil { 4.34 + log.Printf("Error signing token: %+v\n", err) 4.35 + w.WriteHeader(http.StatusInternalServerError) 4.36 + renderJSONError(enc, "server_error") 4.37 + return 4.38 + } 4.39 + token.AccessToken = access 4.40 + err = context.SaveToken(token) 4.41 if err != nil { 4.42 w.WriteHeader(http.StatusInternalServerError) 4.43 renderJSONError(enc, "server_error")
5.1 --- a/oauth2_test.go Tue May 12 21:14:21 2015 -0400 5.2 +++ b/oauth2_test.go Fri May 15 19:44:40 2015 -0400 5.3 @@ -776,6 +776,9 @@ 5.4 clients: store, 5.5 authCodes: store, 5.6 tokens: store, 5.7 + config: Config{ 5.8 + JWTPrivateKey: []byte("this is totally a secret, secure private key"), 5.9 + }, 5.10 } 5.11 client := Client{ 5.12 ID: uuid.NewID(),
6.1 --- a/profile.go Tue May 12 21:14:21 2015 -0400 6.2 +++ b/profile.go Fri May 15 19:44:40 2015 -0400 6.3 @@ -463,7 +463,6 @@ 6.4 r.Handle("/profiles/{id}", wrap(context, GetProfileHandler)).Methods("GET", "OPTIONS") 6.5 r.Handle("/profiles/{id}", wrap(context, UpdateProfileHandler)).Methods("PATCH", "OPTIONS") 6.6 r.Handle("/profiles/{id}", wrap(context, DeleteProfileHandler)).Methods("DELETE", "OPTIONS") 6.7 - // TODO: r.Handle("/profiles/{id}/tokens", wrap(context, ListTokensHandler)).Methods("GET", "OPTIONS") 6.8 // BUG(paddy): We need to implement a handler that will add a login to a profile. 6.9 // BUG(paddy): We need to implement a handler that will remove a login from a profile. What happens to sessions created with that login? 6.10 // BUG(paddy): We need to implement a handler that will list the logins attached to a profile.
7.1 --- a/request.go Tue May 12 21:14:21 2015 -0400 7.2 +++ b/request.go Fri May 15 19:44:40 2015 -0400 7.3 @@ -34,7 +34,6 @@ 7.4 Clients []Client `json:"clients,omitempty"` 7.5 Endpoints []Endpoint `json:"endpoints,omitempty"` 7.6 Sessions []Session `json:"sessions,omitempty"` 7.7 - Tokens []Token `json:"tokens,omitempty"` 7.8 } 7.9 7.10 type requestError struct {
8.1 --- a/sql/postgres_init.sql Tue May 12 21:14:21 2015 -0400 8.2 +++ b/sql/postgres_init.sql Fri May 15 19:44:40 2015 -0400 8.3 @@ -59,7 +59,7 @@ 8.4 ); 8.5 8.6 CREATE TABLE IF NOT EXISTS tokens ( 8.7 - access_token VARCHAR(36) PRIMARY KEY, 8.8 + access_token TEXT PRIMARY KEY, 8.9 refresh_token VARCHAR(36) UNIQUE NOT NULL, 8.10 created TIMESTAMPTZ NOT NULL, 8.11 created_from VARCHAR(128) NOT NULL, 8.12 @@ -68,7 +68,6 @@ 8.13 profile_id VARCHAR(36) NOT NULL, 8.14 client_id VARCHAR(36) NOT NULL, 8.15 revoked BOOLEAN NOT NULL, 8.16 - refresh_revoked BOOLEAN NOT NULL, 8.17 scopes varchar(64)[] NOT NULL 8.18 ); 8.19
9.1 --- a/token.go Tue May 12 21:14:21 2015 -0400 9.2 +++ b/token.go Fri May 15 19:44:40 2015 -0400 9.3 @@ -3,16 +3,18 @@ 9.4 import ( 9.5 "encoding/json" 9.6 "errors" 9.7 - "github.com/gorilla/mux" 9.8 "log" 9.9 "net/http" 9.10 + "strings" 9.11 "time" 9.12 9.13 "code.secondbit.org/uuid.hg" 9.14 + 9.15 + "github.com/dgrijalva/jwt-go" 9.16 ) 9.17 9.18 const ( 9.19 - defaultTokenExpiration = 3600 // one hour 9.20 + defaultTokenExpiration = 900 // fifteen minutes 9.21 ) 9.22 9.23 func init() { 9.24 @@ -38,23 +40,35 @@ 9.25 // Token represents an access and/or refresh token that the Client can use to access user data 9.26 // or obtain a new access token. 9.27 type Token struct { 9.28 - AccessToken string `json:"access_token"` 9.29 - RefreshToken string `json:"refresh_token,omitempty"` 9.30 - Created time.Time `json:"-"` 9.31 - CreatedFrom string `json:"created_from"` 9.32 - ExpiresIn int32 `json:"expires_in"` 9.33 - TokenType string `json:"token_type"` 9.34 - Scopes Scopes `json:"-"` 9.35 - ProfileID uuid.ID `json:"profile_id"` 9.36 - ClientID uuid.ID `json:"client_id"` 9.37 - Revoked bool `json:"revoked,omitempty"` 9.38 - RefreshRevoked bool `json:"refresh_revoked,omitempty"` 9.39 + AccessToken string 9.40 + RefreshToken string 9.41 + Created time.Time 9.42 + CreatedFrom string 9.43 + ExpiresIn int32 9.44 + TokenType string 9.45 + Scopes Scopes 9.46 + ProfileID uuid.ID 9.47 + ClientID uuid.ID 9.48 + Revoked bool 9.49 } 9.50 9.51 +func (t Token) GenerateAccessToken(privateKey []byte) (string, error) { 9.52 + access := jwt.New(jwt.SigningMethodHS256) 9.53 + access.Claims["iss"] = t.ClientID 9.54 + access.Claims["sub"] = t.ProfileID 9.55 + access.Claims["exp"] = t.Created.Add(defaultTokenExpiration * time.Second).Unix() 9.56 + access.Claims["nbf"] = t.Created.Add(-2 * time.Minute).Unix() 9.57 + access.Claims["iat"] = t.Created.Unix() 9.58 + access.Claims["scope"] = strings.Join(t.Scopes.Strings(), " ") 9.59 + return access.SignedString(privateKey) 9.60 +} 9.61 + 9.62 +// BUG(paddy): Now that access tokens are generated and have a meaning, refresh tokens should be the primary key 9.63 + 9.64 type tokenStore interface { 9.65 getToken(token string, refresh bool) (Token, error) 9.66 saveToken(token Token) error 9.67 - revokeToken(token string, refresh bool) error 9.68 + revokeToken(token string) error 9.69 getTokensByProfileID(profileID uuid.ID, num, offset int) ([]Token, error) 9.70 revokeTokensByProfileID(profileID uuid.ID) error 9.71 revokeTokensByClientID(clientID uuid.ID) error 9.72 @@ -96,13 +110,10 @@ 9.73 return nil 9.74 } 9.75 9.76 -func (m *memstore) revokeToken(token string, refresh bool) error { 9.77 - if refresh { 9.78 - t, err := m.lookupTokenByRefresh(token) 9.79 - if err != nil { 9.80 - return err 9.81 - } 9.82 - token = t 9.83 +func (m *memstore) revokeToken(token string) error { 9.84 + token, err := m.lookupTokenByRefresh(token) 9.85 + if err != nil { 9.86 + return err 9.87 } 9.88 m.tokenLock.Lock() 9.89 defer m.tokenLock.Unlock() 9.90 @@ -110,11 +121,7 @@ 9.91 if !ok { 9.92 return ErrTokenNotFound 9.93 } 9.94 - if refresh { 9.95 - t.RefreshRevoked = true 9.96 - } else { 9.97 - t.Revoked = true 9.98 - } 9.99 + t.Revoked = true 9.100 m.tokens[token] = t 9.101 return nil 9.102 } 9.103 @@ -132,7 +139,6 @@ 9.104 for _, id := range ids { 9.105 token := m.tokens[id] 9.106 token.Revoked = true 9.107 - token.RefreshRevoked = true 9.108 m.tokens[id] = token 9.109 } 9.110 return nil 9.111 @@ -146,7 +152,6 @@ 9.112 continue 9.113 } 9.114 token.Revoked = true 9.115 - token.RefreshRevoked = true 9.116 m.tokens[id] = token 9.117 } 9.118 return nil 9.119 @@ -204,7 +209,7 @@ 9.120 renderJSONError(enc, "invalid_grant") 9.121 return 9.122 } 9.123 - if token.RefreshRevoked { 9.124 + if token.Revoked { 9.125 w.WriteHeader(http.StatusBadRequest) 9.126 renderJSONError(enc, "invalid_grant") 9.127 return 9.128 @@ -217,51 +222,9 @@ 9.129 if refresh == "" { 9.130 return ErrTokenNotFound 9.131 } 9.132 - return context.RevokeToken(refresh, true) 9.133 + return context.RevokeToken(refresh) 9.134 } 9.135 9.136 func refreshTokenAuditString(r *http.Request) string { 9.137 return "refresh_token:" + r.PostFormValue("refresh_token") 9.138 } 9.139 - 9.140 -func RegisterTokenHandlers(r *mux.Router, context Context) { 9.141 - r.Handle("/tokens/{id}", wrap(context, GetTokenInfoHandler)).Methods("GET", "OPTIONS") 9.142 - r.Handle("/tokens/{id}", wrap(context, RevokeTokenHandler)).Methods("DELETE", "OPTIONS") 9.143 -} 9.144 - 9.145 -// GetTokenInfoHandler is an HTTP handler for retrieving information about a token. 9.146 -func GetTokenInfoHandler(w http.ResponseWriter, r *http.Request, context Context) { 9.147 - errors := []requestError{} 9.148 - vars := mux.Vars(r) 9.149 - tokenID := vars["id"] 9.150 - if tokenID == "" { 9.151 - errors = append(errors, requestError{Slug: requestErrMissing, Param: "id"}) 9.152 - encode(w, r, http.StatusBadRequest, response{Errors: errors}) 9.153 - return 9.154 - } 9.155 - token, err := context.GetToken(tokenID, false) 9.156 - if err != nil { 9.157 - if err == ErrTokenNotFound { 9.158 - errors = append(errors, requestError{Slug: requestErrNotFound, Param: "id"}) 9.159 - encode(w, r, http.StatusNotFound, response{Errors: errors}) 9.160 - return 9.161 - } 9.162 - encode(w, r, http.StatusInternalServerError, actOfGodResponse) 9.163 - return 9.164 - } 9.165 - token.RefreshToken = "" 9.166 - expired := int64(time.Now().Sub(token.Created) / time.Second) 9.167 - if expired > int64(token.ExpiresIn) { 9.168 - token.ExpiresIn = 0 9.169 - } else { 9.170 - token.ExpiresIn = token.ExpiresIn - int32(expired) 9.171 - } 9.172 - encode(w, r, http.StatusOK, response{Tokens: []Token{token}}) 9.173 - return 9.174 -} 9.175 - 9.176 -// RevokeTokenHandler is an HTTP handler for revoking a Token prematurely. 9.177 -func RevokeTokenHandler(w http.ResponseWriter, r *http.Request, context Context) { 9.178 - //errors := []requestError{} 9.179 - // TODO 9.180 -}
10.1 --- a/token_postgres.go Tue May 12 21:14:21 2015 -0400 10.2 +++ b/token_postgres.go Fri May 15 19:44:40 2015 -0400 10.3 @@ -69,21 +69,17 @@ 10.4 return err 10.5 } 10.6 10.7 -func (p *postgres) revokeTokenSQL(token string, refresh bool) *pan.Query { 10.8 +func (p *postgres) revokeTokenSQL(token string) *pan.Query { 10.9 var t Token 10.10 query := pan.New(pan.POSTGRES, "UPDATE "+pan.GetTableName(t)+" SET ") 10.11 query.Include(pan.GetUnquotedColumn(t, "Revoked")+" = ?", true) 10.12 query.IncludeWhere() 10.13 - if !refresh { 10.14 - query.Include(pan.GetUnquotedColumn(t, "AccessToken")+" = ?", token) 10.15 - } else { 10.16 - query.Include(pan.GetUnquotedColumn(t, "RefreshToken")+" = ?", token) 10.17 - } 10.18 + query.Include(pan.GetUnquotedColumn(t, "RefreshToken")+" = ?", token) 10.19 return query.FlushExpressions(" ") 10.20 } 10.21 10.22 -func (p *postgres) revokeToken(token string, refresh bool) error { 10.23 - query := p.revokeTokenSQL(token, refresh) 10.24 +func (p *postgres) revokeToken(token string) error { 10.25 + query := p.revokeTokenSQL(token) 10.26 res, err := p.db.Exec(query.String(), query.Args...) 10.27 if err != nil { 10.28 return err
11.1 --- a/token_test.go Tue May 12 21:14:21 2015 -0400 11.2 +++ b/token_test.go Fri May 15 19:44:40 2015 -0400 11.3 @@ -81,11 +81,7 @@ 11.4 } else if err != ErrTokenNotFound { 11.5 t.Errorf("Expected ErrTokenNotFound from %T, got %s", store, err) 11.6 } 11.7 - err = context.RevokeToken(token.AccessToken, false) 11.8 - if err != ErrTokenNotFound { 11.9 - t.Errorf("Expected ErrTokenNotFound from %T, got %s", store, err) 11.10 - } 11.11 - err = context.RevokeToken(token.RefreshToken, true) 11.12 + err = context.RevokeToken(token.RefreshToken) 11.13 if err != ErrTokenNotFound { 11.14 t.Errorf("Expected ErrTokenNotFound from %T, got %s", store, err) 11.15 } 11.16 @@ -124,7 +120,7 @@ 11.17 if !success { 11.18 t.Errorf("Expected field %s to be %v, but got %v from %T", field, expectation, result, store) 11.19 } 11.20 - err = context.RevokeToken(token.AccessToken, false) 11.21 + err = context.RevokeToken(token.RefreshToken) 11.22 if err != nil { 11.23 t.Errorf("Error revoking token in %T: %s", store, err) 11.24 } 11.25 @@ -137,19 +133,6 @@ 11.26 if !success { 11.27 t.Errorf("Expected field %s to be %v, but got %v from %T", field, expectation, result, store) 11.28 } 11.29 - err = context.RevokeToken(token.RefreshToken, true) 11.30 - if err != nil { 11.31 - t.Errorf("Error revoking token in %T: %s", store, err) 11.32 - } 11.33 - retrievedRevoked, err = context.GetToken(token.RefreshToken, true) 11.34 - if err != nil { 11.35 - t.Errorf("Error retrieving token from %T: %s", store, err) 11.36 - } 11.37 - token.RefreshRevoked = true 11.38 - success, field, expectation, result = compareTokens(token, retrievedRevoked) 11.39 - if !success { 11.40 - t.Errorf("Expected field %s to be %v, but got %v from %T", field, expectation, result, store) 11.41 - } 11.42 } 11.43 } 11.44