switched to auth 0.3
This commit is contained in:
Generated
+3
-3
@@ -112,7 +112,7 @@
|
||||
version = "v1.0.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:6a297f738eb2aaca7c18129040ce7ac2af2db13c49fc948c13be4c692b2f5e19"
|
||||
digest = "1:5371050ba40cd7482fa2e9f0ff17fc18e2c37a6353ac012eaaacc6250eee4748"
|
||||
name = "github.com/go-pkgz/auth"
|
||||
packages = [
|
||||
".",
|
||||
@@ -123,8 +123,8 @@
|
||||
"token",
|
||||
]
|
||||
pruneopts = "UT"
|
||||
revision = "3d27762393e5d62d1bd0553d5978226bac2acff7"
|
||||
version = "v0.2.1"
|
||||
revision = "6f889bf1c6eb61c926dbb759897e6dc577b10655"
|
||||
version = "v0.3.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:1212e114344a5cdcc834ea69e19d456eef230f9784659080fee67e02ba2cb574"
|
||||
|
||||
@@ -412,7 +412,7 @@ func (s *ServerCommand) makeAdminStore() (admin.Store, error) {
|
||||
return nil, errors.Wrap(e, "failed to create mongo server")
|
||||
}
|
||||
conn := mongo.NewConnection(mgServer, s.Mongo.DB, "admin")
|
||||
return admin.NewMongoStore(conn), nil
|
||||
return admin.NewMongoStore(conn, s.SharedSecret), nil
|
||||
default:
|
||||
return nil, errors.Errorf("unsupported admin store type %s", s.Admin.Type)
|
||||
}
|
||||
@@ -527,8 +527,8 @@ func (s *ServerCommand) makeAuthenticator(ds *service.DataStore, avas avatar.Sto
|
||||
TokenDuration: s.Auth.TTL.JWT,
|
||||
CookieDuration: s.Auth.TTL.Cookie,
|
||||
SecureCookies: strings.HasPrefix(s.RemarkURL, "https://"),
|
||||
SecretReader: token.SecretFunc(func(id string) (string, error) { // get secret per site
|
||||
return admns.Key(id)
|
||||
SecretReader: token.SecretFunc(func() (string, error) { // get secret per site
|
||||
return admns.Key()
|
||||
}),
|
||||
ClaimsUpd: token.ClaimsUpdFunc(func(c token.Claims) token.Claims { // set attributes, on new token or refresh
|
||||
if c.User == nil {
|
||||
|
||||
@@ -38,7 +38,7 @@ type Migrator struct {
|
||||
|
||||
// KeyStore defines sub-interface for consumers needed just a key
|
||||
type KeyStore interface {
|
||||
Key(siteID string) (key string, err error)
|
||||
Key() (key string, err error)
|
||||
}
|
||||
|
||||
func (m *Migrator) withRoutes(router chi.Router) chi.Router {
|
||||
|
||||
@@ -290,7 +290,7 @@ func prepImportSrv(t *testing.T) (svc *Migrator, ds *service.DataStore, ts *http
|
||||
|
||||
a := auth.NewService(auth.Opts{
|
||||
AdminPasswd: "password",
|
||||
SecretReader: token.SecretFunc(func(id string) (string, error) { return "123456", nil }),
|
||||
SecretReader: token.SecretFunc(func() (string, error) { return "123456", nil }),
|
||||
Issuer: "test",
|
||||
})
|
||||
|
||||
|
||||
@@ -193,7 +193,7 @@ func startupT(t *testing.T) (ts *httptest.Server, srv *Rest, teardown func()) {
|
||||
DataService: dataStore,
|
||||
Authenticator: auth.NewService(auth.Opts{
|
||||
AdminPasswd: "password",
|
||||
SecretReader: token.SecretFunc(func(id string) (string, error) { return "secret", nil }),
|
||||
SecretReader: token.SecretFunc(func() (string, error) { return "secret", nil }),
|
||||
AvatarStore: avatar.NewLocalFS("/tmp/ava-remark42"),
|
||||
}),
|
||||
Cache: &cache.Nop{},
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
|
||||
// Store defines interface returning admins info for given site
|
||||
type Store interface {
|
||||
Key(siteID string) (key string, err error)
|
||||
Key() (key string, err error)
|
||||
Admins(siteID string) (ids []string)
|
||||
Email(siteID string) (email string)
|
||||
}
|
||||
@@ -21,7 +21,7 @@ type StaticStore struct {
|
||||
}
|
||||
|
||||
// Key returns static key for all sites, allows empty site
|
||||
func (s *StaticStore) Key(siteID string) (key string, err error) {
|
||||
func (s *StaticStore) Key() (key string, err error) {
|
||||
if s.key == "" {
|
||||
return "", errors.New("empty key for static key store")
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
func TestStaticStore_Get(t *testing.T) {
|
||||
var ks Store = NewStaticStore("key123", []string{"123", "xyz"}, "aa@example.com")
|
||||
|
||||
k, err := ks.Key("any")
|
||||
k, err := ks.Key()
|
||||
assert.NoError(t, err, "valid store")
|
||||
assert.Equal(t, "key123", k, "valid site")
|
||||
|
||||
@@ -21,20 +21,16 @@ func TestStaticStore_Get(t *testing.T) {
|
||||
|
||||
email := ks.Email("blah")
|
||||
assert.Equal(t, "aa@example.com", email)
|
||||
|
||||
ks = NewStaticStore("", []string{"123", "xyz"}, "aa@example.com")
|
||||
_, err = ks.Key("any")
|
||||
assert.NotNil(t, err, "invalid (empty key) store")
|
||||
}
|
||||
|
||||
func TestMongoStore_Get(t *testing.T) {
|
||||
conn, err := mongo.MakeTestConnection(t)
|
||||
require.NoError(t, err)
|
||||
var ms Store = NewMongoStore(conn)
|
||||
var ms Store = NewMongoStore(conn, "secret")
|
||||
|
||||
recs := []mongoRec{
|
||||
{"site1", "secret1", []string{"i11", "i12"}, "e1"},
|
||||
{"site2", "secret2", []string{"i21", "i22"}, "e2"},
|
||||
{"site1", []string{"i11", "i12"}, "e1"},
|
||||
{"site2", []string{"i21", "i22"}, "e2"},
|
||||
}
|
||||
err = conn.WithCollection(func(coll *mgo.Collection) error {
|
||||
if e1 := coll.Insert(recs[0]); e1 != nil {
|
||||
@@ -48,22 +44,20 @@ func TestMongoStore_Get(t *testing.T) {
|
||||
assert.Equal(t, []string{"i11", "i12"}, admins)
|
||||
email := ms.Email("site1")
|
||||
assert.Equal(t, "e1", email)
|
||||
key, err := ms.Key("site1")
|
||||
key, err := ms.Key()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "secret1", key)
|
||||
assert.Equal(t, "secret", key)
|
||||
|
||||
admins = ms.Admins("site2")
|
||||
assert.Equal(t, []string{"i21", "i22"}, admins)
|
||||
email = ms.Email("site2")
|
||||
assert.Equal(t, "e2", email)
|
||||
key, err = ms.Key("site2")
|
||||
key, err = ms.Key()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "secret2", key)
|
||||
assert.Equal(t, "secret", key)
|
||||
|
||||
admins = ms.Admins("no-site-in-db")
|
||||
assert.Equal(t, []string{}, admins)
|
||||
email = ms.Email("no-site-in-db")
|
||||
assert.Equal(t, "", email)
|
||||
_, err = ms.Key("no-site-in-db")
|
||||
assert.Error(t, err, "can't get secret for site no-site-in-db")
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
|
||||
"github.com/globalsign/mgo"
|
||||
"github.com/globalsign/mgo/bson"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/go-pkgz/mongo"
|
||||
)
|
||||
@@ -13,28 +12,24 @@ import (
|
||||
// MongoStore implements admin.Store with mongo backend
|
||||
type MongoStore struct {
|
||||
connection *mongo.Connection
|
||||
key string
|
||||
}
|
||||
|
||||
type mongoRec struct {
|
||||
SiteID string `bson:"site"`
|
||||
SecretKey string `bson:"secret"`
|
||||
IDs []string `bson:"admin_ids"`
|
||||
Email string `bson:"admin_email"`
|
||||
SiteID string `bson:"site"`
|
||||
IDs []string `bson:"admin_ids"`
|
||||
Email string `bson:"admin_email"`
|
||||
}
|
||||
|
||||
// NewMongoStore makes admin Store for mongo's connection
|
||||
func NewMongoStore(conn *mongo.Connection) *MongoStore {
|
||||
func NewMongoStore(conn *mongo.Connection, key string) *MongoStore {
|
||||
log.Printf("[DEBUG] make mongo admin store with %+v", conn)
|
||||
return &MongoStore{connection: conn}
|
||||
return &MongoStore{connection: conn, key: key}
|
||||
}
|
||||
|
||||
// Key executes find by siteID and returns substructure with secret key
|
||||
func (m *MongoStore) Key(siteID string) (key string, err error) {
|
||||
resp := mongoRec{}
|
||||
err = m.connection.WithCollection(func(coll *mgo.Collection) error {
|
||||
return coll.Find(bson.M{"site": siteID}).One(&resp)
|
||||
})
|
||||
return resp.SecretKey, errors.Wrapf(err, "can't get secret for site %s", siteID)
|
||||
func (m *MongoStore) Key() (key string, err error) {
|
||||
return m.key, nil
|
||||
}
|
||||
|
||||
// Admins executes find by siteID and returns admins ids
|
||||
|
||||
@@ -76,7 +76,7 @@ func (s *DataStore) prepareNewComment(comment store.Comment) (store.Comment, err
|
||||
}
|
||||
comment.Sanitize() // clear potentially dangerous js from all parts of comment
|
||||
|
||||
secret, err := s.AdminStore.Key(comment.Locator.SiteID)
|
||||
secret, err := s.AdminStore.Key()
|
||||
if err != nil {
|
||||
return store.Comment{}, errors.Wrapf(err, "can't get secret for site %s", comment.Locator.SiteID)
|
||||
}
|
||||
|
||||
+12
@@ -166,6 +166,18 @@ _This technic used in the [example](https://github.com/go-pkgz/auth/blob/master/
|
||||
The process can be simplified by doing all checks directly in `Validator`, but depends on particular case such solution
|
||||
can be too expensive because `Validator` runs on each request as a part of auth middleware. In contrast, `ClaimsUpdater` called on token creation/refresh only.
|
||||
|
||||
### Multi-tenant services and support for different audiences
|
||||
|
||||
For complex systems a single authenticator may serve multiple distinct subsystems or multiple set of independent users. For example some SaaS offerings may need to provide different authentications for different customers and prevent use of tokens/cookies made by another customer.
|
||||
|
||||
Such functionality can be implemented in 3 different ways:
|
||||
|
||||
- Different instances of `auth.Service` each one with different secret. Doing this way will ensure the highest level of isolation and cookies/tokens won't be even parsable across the instances. Practically such architecture can be too complicated and not always possible.
|
||||
– Handling "allowed audience" as a part of `ClaimsUpdater` and `Validator` chain. I.e. `ClaimsUpdater` sets a claim indicating expected audience code/id and `Validator` making sure it matches. This way a single `auth.Service` could handle multiple groups of auth tokens and reject some based on the audience.
|
||||
- Using the standard JWT `aud` claim. This method conceptually very similar to the previous one, but done by library internally and consumer don't need to define special `ClaimsUpdater` and `Validator` logic.
|
||||
|
||||
In order to allow `aud` support the list of allowed audiences should be passed in as `opts.Audiences` parameter. Non-empty value will trigger internal checks for token generation (will reject token creation fot alien `aud`) as well as `Auth` middleware.
|
||||
|
||||
|
||||
### Dev provider
|
||||
|
||||
|
||||
+6
-4
@@ -53,9 +53,10 @@ type Opts struct {
|
||||
AvatarResizeLimit int // resize avatar's limit in pixels
|
||||
AvatarRoutePath string // avatar routing prefix, i.e. "/api/v1/avatar", default `/avatar`
|
||||
|
||||
AdminPasswd string // if presented, allows basic auth with user admin and given password
|
||||
RefreshFactor int // estimated number of request client sends in parallel during token refresh.
|
||||
Logger logger.L // logger interface, default is no logging at all
|
||||
AdminPasswd string // if presented, allows basic auth with user admin and given password
|
||||
AudienceReader token.Audience // list of allowed aud values, default (empty) allows any
|
||||
RefreshFactor int // estimated number of request client sends in parallel during token refresh.
|
||||
Logger logger.L // logger interface, default is no logging at all
|
||||
}
|
||||
|
||||
// NewService initializes everything
|
||||
@@ -93,10 +94,11 @@ func NewService(opts Opts) (res *Service) {
|
||||
XSRFCookieName: opts.XSRFCookieName,
|
||||
XSRFHeaderKey: opts.XSRFHeaderKey,
|
||||
Issuer: res.issuer,
|
||||
AudienceReader: opts.AudienceReader,
|
||||
})
|
||||
|
||||
if opts.SecretReader == nil {
|
||||
jwtService.SecretReader = token.SecretFunc(func(id string) (string, error) {
|
||||
jwtService.SecretReader = token.SecretFunc(func() (string, error) {
|
||||
return "", errors.New("secrets reader not available")
|
||||
})
|
||||
res.logger.Logf("[WARN] no secret reader defined")
|
||||
|
||||
+1
-4
@@ -53,10 +53,7 @@ func (a *Authenticator) Trace(next http.Handler) http.Handler {
|
||||
func (a *Authenticator) auth(reqAuth bool) func(http.Handler) http.Handler {
|
||||
|
||||
onError := func(h http.Handler, w http.ResponseWriter, r *http.Request, err error) {
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
if !reqAuth {
|
||||
if !reqAuth { // if no auth required allow to proceeded on error
|
||||
h.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
+76
-39
@@ -4,6 +4,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
jwt "github.com/dgrijalva/jwt-go"
|
||||
@@ -57,7 +58,8 @@ type Opts struct {
|
||||
XSRFCookieName string
|
||||
XSRFHeaderKey string
|
||||
|
||||
Issuer string // optional value for iss claim, usually application name
|
||||
AudienceReader Audience // allowed aud values
|
||||
Issuer string // optional value for iss claim, usually application name
|
||||
}
|
||||
|
||||
// NewService makes JWT service
|
||||
@@ -90,6 +92,8 @@ func NewService(opts Opts) *Service {
|
||||
// Token makes token with claims
|
||||
func (j *Service) Token(claims Claims) (string, error) {
|
||||
|
||||
// make token for allowed aud values only, rejects others
|
||||
|
||||
// update claims with ClaimsUpdFunc defined by consumer
|
||||
if j.ClaimsUpd != nil {
|
||||
claims = j.ClaimsUpd.Update(claims)
|
||||
@@ -98,17 +102,21 @@ func (j *Service) Token(claims Claims) (string, error) {
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
|
||||
if j.SecretReader == nil {
|
||||
return "", errors.New("secretreader not defined")
|
||||
return "", errors.New("secret reader not defined")
|
||||
}
|
||||
|
||||
secret, err := j.SecretReader.Get(claims.Audience) // get secret via consumer defined SecretReader
|
||||
if err := j.checkAuds(&claims, j.AudienceReader); err != nil {
|
||||
return "", errors.Wrap(err, "aud rejected")
|
||||
}
|
||||
|
||||
secret, err := j.SecretReader.Get() // get secret via consumer defined SecretReader
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "can't get secret")
|
||||
}
|
||||
|
||||
tokenString, err := token.SignedString([]byte(secret))
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "can't sign token token")
|
||||
return "", errors.Wrap(err, "can't sign token")
|
||||
}
|
||||
return tokenString, nil
|
||||
}
|
||||
@@ -117,31 +125,11 @@ func (j *Service) Token(claims Claims) (string, error) {
|
||||
func (j *Service) Parse(tokenString string) (Claims, error) {
|
||||
parser := jwt.Parser{SkipClaimsValidation: true} // allow parsing of expired tokens
|
||||
|
||||
getAud := func() (aud string, err error) { // parse token without signature check to get id (aud)
|
||||
preToken, _, err := parser.ParseUnverified(tokenString, &Claims{})
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "can't pre-parse token")
|
||||
}
|
||||
if _, ok := preToken.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return "", errors.Errorf("unexpected signing method: %v", preToken.Header["alg"])
|
||||
}
|
||||
preClaims, ok := preToken.Claims.(*Claims)
|
||||
if !ok {
|
||||
return "", errors.New("invalid token")
|
||||
}
|
||||
return preClaims.Audience, nil
|
||||
}
|
||||
|
||||
aud, err := getAud()
|
||||
if err != nil {
|
||||
return Claims{}, errors.Wrap(err, "failed to get aud from token token")
|
||||
}
|
||||
|
||||
if j.SecretReader == nil {
|
||||
return Claims{}, errors.New("secretreader not defined")
|
||||
return Claims{}, errors.New("secret reader not defined")
|
||||
}
|
||||
|
||||
secret, err := j.SecretReader.Get(aud)
|
||||
secret, err := j.SecretReader.Get()
|
||||
if err != nil {
|
||||
return Claims{}, errors.Wrap(err, "can't get secret")
|
||||
}
|
||||
@@ -157,11 +145,30 @@ func (j *Service) Parse(tokenString string) (Claims, error) {
|
||||
}
|
||||
|
||||
claims, ok := token.Claims.(*Claims)
|
||||
if !ok || !token.Valid {
|
||||
if !ok {
|
||||
return Claims{}, errors.New("invalid token")
|
||||
}
|
||||
|
||||
return *claims, nil
|
||||
if err = j.checkAuds(claims, j.AudienceReader); err != nil {
|
||||
return Claims{}, errors.Wrap(err, "aud rejected")
|
||||
}
|
||||
return *claims, j.validate(claims)
|
||||
}
|
||||
|
||||
func (j *Service) validate(claims *Claims) error {
|
||||
cerr := claims.Valid()
|
||||
|
||||
if cerr == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if e, ok := cerr.(*jwt.ValidationError); ok {
|
||||
e.Errors ^= jwt.ValidationErrorExpired // clear ValidationErrorExpired, allow expired token
|
||||
if e.Errors != 0 {
|
||||
return e
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Set creates token cookie with xsrf cookie and put it to ResponseWriter
|
||||
@@ -257,18 +264,43 @@ func (j *Service) Reset(w http.ResponseWriter) {
|
||||
http.SetCookie(w, &xsrfCookie)
|
||||
}
|
||||
|
||||
// checkAuds verifies if claims.Audience in the list of allowed by audReader
|
||||
func (j *Service) checkAuds(claims *Claims, audReader Audience) error {
|
||||
if audReader == nil { // lack of any allowed means any
|
||||
return nil
|
||||
}
|
||||
auds, err := audReader.Get()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to get auds")
|
||||
}
|
||||
for _, a := range auds {
|
||||
if strings.EqualFold(a, claims.Audience) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return errors.Errorf("aud %q not allowed", claims.Audience)
|
||||
}
|
||||
|
||||
func (c Claims) String() string {
|
||||
b, err := json.Marshal(c)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("%+v %+v", c.StandardClaims, c.User)
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
// Secret defines interface returning secret key for given id (aud)
|
||||
type Secret interface {
|
||||
Get(id string) (string, error)
|
||||
Get() (string, error)
|
||||
}
|
||||
|
||||
// SecretFunc type is an adapter to allow the use of ordinary functions as Secret. If f is a function
|
||||
// with the appropriate signature, SecretFunc(f) is a Handler that calls f.
|
||||
type SecretFunc func(id string) (string, error)
|
||||
type SecretFunc func() (string, error)
|
||||
|
||||
// Get calls f(id)
|
||||
func (f SecretFunc) Get(id string) (string, error) {
|
||||
return f(id)
|
||||
// Get calls f()
|
||||
func (f SecretFunc) Get() (string, error) {
|
||||
return f()
|
||||
}
|
||||
|
||||
// ClaimsUpdater defines interface adding extras to claims
|
||||
@@ -300,10 +332,15 @@ func (f ValidatorFunc) Validate(token string, claims Claims) bool {
|
||||
return f(token, claims)
|
||||
}
|
||||
|
||||
func (c Claims) String() string {
|
||||
b, err := json.Marshal(c)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("%+v %+v", c.StandardClaims, c.User)
|
||||
}
|
||||
return string(b)
|
||||
// Audience defines interface returning list of allowed audiences
|
||||
type Audience interface {
|
||||
Get() ([]string, error)
|
||||
}
|
||||
|
||||
// AudienceFunc type is an adapter to allow the use of ordinary functions as Audience.
|
||||
type AudienceFunc func() ([]string, error)
|
||||
|
||||
// Get calls f()
|
||||
func (f AudienceFunc) Get() ([]string, error) {
|
||||
return f()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user