auth

Paddy 2015-05-15 Parent:0ff23f3a4ede Child:b7e685839a1b

168:581c60f8dd23 Go to Latest

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.

History
     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 -}