auth
auth/token.go
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.
1.1 --- a/token.go Tue May 12 21:14:21 2015 -0400 1.2 +++ b/token.go Fri May 15 19:44:40 2015 -0400 1.3 @@ -3,16 +3,18 @@ 1.4 import ( 1.5 "encoding/json" 1.6 "errors" 1.7 - "github.com/gorilla/mux" 1.8 "log" 1.9 "net/http" 1.10 + "strings" 1.11 "time" 1.12 1.13 "code.secondbit.org/uuid.hg" 1.14 + 1.15 + "github.com/dgrijalva/jwt-go" 1.16 ) 1.17 1.18 const ( 1.19 - defaultTokenExpiration = 3600 // one hour 1.20 + defaultTokenExpiration = 900 // fifteen minutes 1.21 ) 1.22 1.23 func init() { 1.24 @@ -38,23 +40,35 @@ 1.25 // Token represents an access and/or refresh token that the Client can use to access user data 1.26 // or obtain a new access token. 1.27 type Token struct { 1.28 - AccessToken string `json:"access_token"` 1.29 - RefreshToken string `json:"refresh_token,omitempty"` 1.30 - Created time.Time `json:"-"` 1.31 - CreatedFrom string `json:"created_from"` 1.32 - ExpiresIn int32 `json:"expires_in"` 1.33 - TokenType string `json:"token_type"` 1.34 - Scopes Scopes `json:"-"` 1.35 - ProfileID uuid.ID `json:"profile_id"` 1.36 - ClientID uuid.ID `json:"client_id"` 1.37 - Revoked bool `json:"revoked,omitempty"` 1.38 - RefreshRevoked bool `json:"refresh_revoked,omitempty"` 1.39 + AccessToken string 1.40 + RefreshToken string 1.41 + Created time.Time 1.42 + CreatedFrom string 1.43 + ExpiresIn int32 1.44 + TokenType string 1.45 + Scopes Scopes 1.46 + ProfileID uuid.ID 1.47 + ClientID uuid.ID 1.48 + Revoked bool 1.49 } 1.50 1.51 +func (t Token) GenerateAccessToken(privateKey []byte) (string, error) { 1.52 + access := jwt.New(jwt.SigningMethodHS256) 1.53 + access.Claims["iss"] = t.ClientID 1.54 + access.Claims["sub"] = t.ProfileID 1.55 + access.Claims["exp"] = t.Created.Add(defaultTokenExpiration * time.Second).Unix() 1.56 + access.Claims["nbf"] = t.Created.Add(-2 * time.Minute).Unix() 1.57 + access.Claims["iat"] = t.Created.Unix() 1.58 + access.Claims["scope"] = strings.Join(t.Scopes.Strings(), " ") 1.59 + return access.SignedString(privateKey) 1.60 +} 1.61 + 1.62 +// BUG(paddy): Now that access tokens are generated and have a meaning, refresh tokens should be the primary key 1.63 + 1.64 type tokenStore interface { 1.65 getToken(token string, refresh bool) (Token, error) 1.66 saveToken(token Token) error 1.67 - revokeToken(token string, refresh bool) error 1.68 + revokeToken(token string) error 1.69 getTokensByProfileID(profileID uuid.ID, num, offset int) ([]Token, error) 1.70 revokeTokensByProfileID(profileID uuid.ID) error 1.71 revokeTokensByClientID(clientID uuid.ID) error 1.72 @@ -96,13 +110,10 @@ 1.73 return nil 1.74 } 1.75 1.76 -func (m *memstore) revokeToken(token string, refresh bool) error { 1.77 - if refresh { 1.78 - t, err := m.lookupTokenByRefresh(token) 1.79 - if err != nil { 1.80 - return err 1.81 - } 1.82 - token = t 1.83 +func (m *memstore) revokeToken(token string) error { 1.84 + token, err := m.lookupTokenByRefresh(token) 1.85 + if err != nil { 1.86 + return err 1.87 } 1.88 m.tokenLock.Lock() 1.89 defer m.tokenLock.Unlock() 1.90 @@ -110,11 +121,7 @@ 1.91 if !ok { 1.92 return ErrTokenNotFound 1.93 } 1.94 - if refresh { 1.95 - t.RefreshRevoked = true 1.96 - } else { 1.97 - t.Revoked = true 1.98 - } 1.99 + t.Revoked = true 1.100 m.tokens[token] = t 1.101 return nil 1.102 } 1.103 @@ -132,7 +139,6 @@ 1.104 for _, id := range ids { 1.105 token := m.tokens[id] 1.106 token.Revoked = true 1.107 - token.RefreshRevoked = true 1.108 m.tokens[id] = token 1.109 } 1.110 return nil 1.111 @@ -146,7 +152,6 @@ 1.112 continue 1.113 } 1.114 token.Revoked = true 1.115 - token.RefreshRevoked = true 1.116 m.tokens[id] = token 1.117 } 1.118 return nil 1.119 @@ -204,7 +209,7 @@ 1.120 renderJSONError(enc, "invalid_grant") 1.121 return 1.122 } 1.123 - if token.RefreshRevoked { 1.124 + if token.Revoked { 1.125 w.WriteHeader(http.StatusBadRequest) 1.126 renderJSONError(enc, "invalid_grant") 1.127 return 1.128 @@ -217,51 +222,9 @@ 1.129 if refresh == "" { 1.130 return ErrTokenNotFound 1.131 } 1.132 - return context.RevokeToken(refresh, true) 1.133 + return context.RevokeToken(refresh) 1.134 } 1.135 1.136 func refreshTokenAuditString(r *http.Request) string { 1.137 return "refresh_token:" + r.PostFormValue("refresh_token") 1.138 } 1.139 - 1.140 -func RegisterTokenHandlers(r *mux.Router, context Context) { 1.141 - r.Handle("/tokens/{id}", wrap(context, GetTokenInfoHandler)).Methods("GET", "OPTIONS") 1.142 - r.Handle("/tokens/{id}", wrap(context, RevokeTokenHandler)).Methods("DELETE", "OPTIONS") 1.143 -} 1.144 - 1.145 -// GetTokenInfoHandler is an HTTP handler for retrieving information about a token. 1.146 -func GetTokenInfoHandler(w http.ResponseWriter, r *http.Request, context Context) { 1.147 - errors := []requestError{} 1.148 - vars := mux.Vars(r) 1.149 - tokenID := vars["id"] 1.150 - if tokenID == "" { 1.151 - errors = append(errors, requestError{Slug: requestErrMissing, Param: "id"}) 1.152 - encode(w, r, http.StatusBadRequest, response{Errors: errors}) 1.153 - return 1.154 - } 1.155 - token, err := context.GetToken(tokenID, false) 1.156 - if err != nil { 1.157 - if err == ErrTokenNotFound { 1.158 - errors = append(errors, requestError{Slug: requestErrNotFound, Param: "id"}) 1.159 - encode(w, r, http.StatusNotFound, response{Errors: errors}) 1.160 - return 1.161 - } 1.162 - encode(w, r, http.StatusInternalServerError, actOfGodResponse) 1.163 - return 1.164 - } 1.165 - token.RefreshToken = "" 1.166 - expired := int64(time.Now().Sub(token.Created) / time.Second) 1.167 - if expired > int64(token.ExpiresIn) { 1.168 - token.ExpiresIn = 0 1.169 - } else { 1.170 - token.ExpiresIn = token.ExpiresIn - int32(expired) 1.171 - } 1.172 - encode(w, r, http.StatusOK, response{Tokens: []Token{token}}) 1.173 - return 1.174 -} 1.175 - 1.176 -// RevokeTokenHandler is an HTTP handler for revoking a Token prematurely. 1.177 -func RevokeTokenHandler(w http.ResponseWriter, r *http.Request, context Context) { 1.178 - //errors := []requestError{} 1.179 - // TODO 1.180 -}