satellite/console: create new consoleauth service
We want to send email verification reminders to users from the satellite core, but some of the functionality required to do so exists in the satellite console service. We could simply import the console service into the core to achieve this, but the service requires a lot of dependencies that would go unused just to be able to send these emails. Instead, we break out the needed functionality into a new service which can be imported separately by the console service and the future email chore. The consoleauth service creates, signs, and checks the expiration of auth tokens. Change-Id: I2ad794b7fd256f8af24c1a8d73a203d508069078
This commit is contained in:
parent
763bfc0913
commit
0633aca607
@ -143,9 +143,10 @@ type API struct {
|
||||
}
|
||||
|
||||
Console struct {
|
||||
Listener net.Listener
|
||||
Service *console.Service
|
||||
Endpoint *consoleweb.Server
|
||||
Listener net.Listener
|
||||
Service *console.Service
|
||||
Endpoint *consoleweb.Server
|
||||
AuthTokens *consoleauth.Service
|
||||
}
|
||||
|
||||
Marketing struct {
|
||||
@ -592,9 +593,10 @@ func NewAPI(log *zap.Logger, full *identity.FullIdentity, db DB,
|
||||
return nil, errs.New("Auth token secret required")
|
||||
}
|
||||
|
||||
peer.Console.AuthTokens = consoleauth.NewService(config.ConsoleAuth, &consoleauth.Hmac{Secret: []byte(consoleConfig.AuthTokenSecret)})
|
||||
|
||||
peer.Console.Service, err = console.NewService(
|
||||
peer.Log.Named("console:service"),
|
||||
&consoleauth.Hmac{Secret: []byte(consoleConfig.AuthTokenSecret)},
|
||||
peer.DB.Console(),
|
||||
peer.REST.Keys,
|
||||
peer.DB.ProjectAccounting(),
|
||||
@ -603,6 +605,7 @@ func NewAPI(log *zap.Logger, full *identity.FullIdentity, db DB,
|
||||
peer.Marketing.PartnersService,
|
||||
peer.Payments.Accounts,
|
||||
peer.Analytics.Service,
|
||||
peer.Console.AuthTokens,
|
||||
consoleConfig.Config,
|
||||
)
|
||||
if err != nil {
|
||||
|
@ -5,7 +5,6 @@ package console
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
|
||||
"github.com/zeebo/errs"
|
||||
|
||||
@ -14,24 +13,6 @@ import (
|
||||
|
||||
// TODO: change to JWT or Macaroon based auth
|
||||
|
||||
// Signer creates signature for provided data.
|
||||
type Signer interface {
|
||||
Sign(data []byte) ([]byte, error)
|
||||
}
|
||||
|
||||
// signToken signs token with given signer.
|
||||
func signToken(token *consoleauth.Token, signer Signer) error {
|
||||
encoded := base64.URLEncoding.EncodeToString(token.Payload)
|
||||
|
||||
signature, err := signer.Sign([]byte(encoded))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
token.Signature = signature
|
||||
return nil
|
||||
}
|
||||
|
||||
// key is a context value key type.
|
||||
type key int
|
||||
|
||||
|
90
satellite/console/consoleauth/service.go
Normal file
90
satellite/console/consoleauth/service.go
Normal file
@ -0,0 +1,90 @@
|
||||
// Copyright (C) 2022 Storj Labs, Inc.
|
||||
// See LICENSE for copying information.
|
||||
|
||||
package consoleauth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"time"
|
||||
|
||||
"github.com/spacemonkeygo/monkit/v3"
|
||||
|
||||
"storj.io/common/uuid"
|
||||
)
|
||||
|
||||
var mon = monkit.Package()
|
||||
|
||||
// Config contains configuration parameters for console auth.
|
||||
type Config struct {
|
||||
TokenExpirationTime time.Duration `help:"expiration time for auth tokens, account recovery tokens, and activation tokens" default:"24h"`
|
||||
}
|
||||
|
||||
// Service handles creating, signing, and checking the expiration of auth tokens.
|
||||
type Service struct {
|
||||
config Config
|
||||
Signer
|
||||
}
|
||||
|
||||
// NewService creates a new consoleauth service.
|
||||
func NewService(config Config, signer Signer) *Service {
|
||||
return &Service{
|
||||
config: config,
|
||||
Signer: signer,
|
||||
}
|
||||
}
|
||||
|
||||
// Signer creates signature for provided data.
|
||||
type Signer interface {
|
||||
Sign(data []byte) ([]byte, error)
|
||||
}
|
||||
|
||||
// CreateToken creates a new auth token.
|
||||
func (s *Service) CreateToken(ctx context.Context, id uuid.UUID, email string) (_ string, err error) {
|
||||
defer mon.Task()(&ctx)(&err)
|
||||
claims := &Claims{
|
||||
ID: id,
|
||||
Expiration: time.Now().Add(s.config.TokenExpirationTime),
|
||||
}
|
||||
if email != "" {
|
||||
claims.Email = email
|
||||
}
|
||||
|
||||
return s.createToken(ctx, claims)
|
||||
}
|
||||
|
||||
// createToken creates string representation.
|
||||
func (s *Service) createToken(ctx context.Context, claims *Claims) (_ string, err error) {
|
||||
defer mon.Task()(&ctx)(&err)
|
||||
|
||||
json, err := claims.JSON()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
token := Token{Payload: json}
|
||||
err = s.SignToken(&token)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return token.String(), nil
|
||||
}
|
||||
|
||||
// SignToken signs token.
|
||||
func (s *Service) SignToken(token *Token) error {
|
||||
encoded := base64.URLEncoding.EncodeToString(token.Payload)
|
||||
|
||||
signature, err := s.Signer.Sign([]byte(encoded))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
token.Signature = signature
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsExpired returns whether token is expired.
|
||||
func (s *Service) IsExpired(now, tokenCreatedAt time.Time) bool {
|
||||
return now.Sub(tokenCreatedAt) > s.config.TokenExpirationTime
|
||||
}
|
@ -97,7 +97,6 @@ func TestGraphqlMutation(t *testing.T) {
|
||||
|
||||
service, err := console.NewService(
|
||||
log.Named("console"),
|
||||
&consoleauth.Hmac{Secret: []byte("my-suppa-secret-key")},
|
||||
db.Console(),
|
||||
restkeys.NewService(db.OIDC().OAuthTokens(), planet.Satellites[0].Config.RESTKeys),
|
||||
db.ProjectAccounting(),
|
||||
@ -106,10 +105,12 @@ func TestGraphqlMutation(t *testing.T) {
|
||||
partnersService,
|
||||
paymentsService.Accounts(),
|
||||
analyticsService,
|
||||
consoleauth.NewService(consoleauth.Config{
|
||||
TokenExpirationTime: 24 * time.Hour,
|
||||
}, &consoleauth.Hmac{Secret: []byte("my-suppa-secret-key")}),
|
||||
console.Config{
|
||||
PasswordCost: console.TestPasswordCost,
|
||||
DefaultProjectLimit: 5,
|
||||
TokenExpirationTime: 24 * time.Hour,
|
||||
},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
@ -81,7 +81,6 @@ func TestGraphqlQuery(t *testing.T) {
|
||||
|
||||
service, err := console.NewService(
|
||||
log.Named("console"),
|
||||
&consoleauth.Hmac{Secret: []byte("my-suppa-secret-key")},
|
||||
db.Console(),
|
||||
restkeys.NewService(db.OIDC().OAuthTokens(), planet.Satellites[0].Config.RESTKeys),
|
||||
db.ProjectAccounting(),
|
||||
@ -90,10 +89,12 @@ func TestGraphqlQuery(t *testing.T) {
|
||||
partnersService,
|
||||
paymentsService.Accounts(),
|
||||
analyticsService,
|
||||
consoleauth.NewService(consoleauth.Config{
|
||||
TokenExpirationTime: 24 * time.Hour,
|
||||
}, &consoleauth.Hmac{Secret: []byte("my-suppa-secret-key")}),
|
||||
console.Config{
|
||||
PasswordCost: console.TestPasswordCost,
|
||||
DefaultProjectLimit: 5,
|
||||
TokenExpirationTime: 24 * time.Hour,
|
||||
},
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
@ -112,8 +112,6 @@ var (
|
||||
//
|
||||
// architecture: Service
|
||||
type Service struct {
|
||||
Signer
|
||||
|
||||
log, auditLogger *zap.Logger
|
||||
store DB
|
||||
restKeys RESTKeys
|
||||
@ -124,6 +122,7 @@ type Service struct {
|
||||
accounts payments.Accounts
|
||||
captchaHandler CaptchaHandler
|
||||
analytics *analytics.Service
|
||||
tokens *consoleauth.Service
|
||||
|
||||
config Config
|
||||
}
|
||||
@ -145,7 +144,6 @@ type Config struct {
|
||||
PasswordCost int `help:"password hashing cost (0=automatic)" testDefault:"4" default:"0"`
|
||||
OpenRegistrationEnabled bool `help:"enable open registration" default:"false" testDefault:"true"`
|
||||
DefaultProjectLimit int `help:"default project limits for users" default:"1" testDefault:"5"`
|
||||
TokenExpirationTime time.Duration `help:"expiration time for auth tokens, account recovery tokens, and activation tokens" default:"24h"`
|
||||
AsOfSystemTimeDuration time.Duration `help:"default duration for AS OF SYSTEM TIME" devDefault:"-5m" releaseDefault:"-5m" testDefault:"0"`
|
||||
LoginAttemptsWithoutPenalty int `help:"number of times user can try to login without penalty" default:"3"`
|
||||
FailedLoginPenalty float64 `help:"incremental duration of penalty for failed login attempts in minutes" default:"2.0"`
|
||||
@ -174,10 +172,7 @@ type PaymentsService struct {
|
||||
}
|
||||
|
||||
// NewService returns new instance of Service.
|
||||
func NewService(log *zap.Logger, signer Signer, store DB, restKeys RESTKeys, projectAccounting accounting.ProjectAccounting, projectUsage *accounting.Service, buckets Buckets, partners *rewards.PartnersService, accounts payments.Accounts, analytics *analytics.Service, config Config) (*Service, error) {
|
||||
if signer == nil {
|
||||
return nil, errs.New("signer can't be nil")
|
||||
}
|
||||
func NewService(log *zap.Logger, store DB, restKeys RESTKeys, projectAccounting accounting.ProjectAccounting, projectUsage *accounting.Service, buckets Buckets, partners *rewards.PartnersService, accounts payments.Accounts, analytics *analytics.Service, tokens *consoleauth.Service, config Config) (*Service, error) {
|
||||
if store == nil {
|
||||
return nil, errs.New("store can't be nil")
|
||||
}
|
||||
@ -198,7 +193,6 @@ func NewService(log *zap.Logger, signer Signer, store DB, restKeys RESTKeys, pro
|
||||
return &Service{
|
||||
log: log,
|
||||
auditLogger: log.Named("auditlog"),
|
||||
Signer: signer,
|
||||
store: store,
|
||||
restKeys: restKeys,
|
||||
projectAccounting: projectAccounting,
|
||||
@ -208,6 +202,7 @@ func NewService(log *zap.Logger, signer Signer, store DB, restKeys RESTKeys, pro
|
||||
accounts: accounts,
|
||||
captchaHandler: captchaHandler,
|
||||
analytics: analytics,
|
||||
tokens: tokens,
|
||||
config: config,
|
||||
}, nil
|
||||
}
|
||||
@ -719,14 +714,7 @@ func (s *Service) TestSwapCaptchaHandler(h CaptchaHandler) {
|
||||
func (s *Service) GenerateActivationToken(ctx context.Context, id uuid.UUID, email string) (token string, err error) {
|
||||
defer mon.Task()(&ctx)(&err)
|
||||
|
||||
// TODO: activation token should differ from auth token
|
||||
claims := &consoleauth.Claims{
|
||||
ID: id,
|
||||
Email: email,
|
||||
Expiration: time.Now().Add(s.config.TokenExpirationTime),
|
||||
}
|
||||
|
||||
return s.createToken(ctx, claims)
|
||||
return s.tokens.CreateToken(ctx, id, email)
|
||||
}
|
||||
|
||||
// GeneratePasswordRecoveryToken - is a method for generating password recovery token.
|
||||
@ -789,12 +777,7 @@ func (s *Service) ActivateAccount(ctx context.Context, activationToken string) (
|
||||
s.analytics.TrackAccountVerified(user.ID, user.Email)
|
||||
|
||||
// now that the account is activated, create a token to be stored in a cookie to log the user in.
|
||||
claims = &consoleauth.Claims{
|
||||
ID: user.ID,
|
||||
Expiration: time.Now().Add(s.config.TokenExpirationTime),
|
||||
}
|
||||
|
||||
token, err = s.createToken(ctx, claims)
|
||||
token, err = s.tokens.CreateToken(ctx, user.ID, "")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@ -852,7 +835,7 @@ func (s *Service) ResetPassword(ctx context.Context, resetPasswordToken, passwor
|
||||
return ErrValidation.Wrap(err)
|
||||
}
|
||||
|
||||
if t.Sub(token.CreatedAt) > s.config.TokenExpirationTime {
|
||||
if s.tokens.IsExpired(t, token.CreatedAt) {
|
||||
return ErrRecoveryToken.Wrap(ErrTokenExpiration.New(passwordRecoveryTokenIsExpiredErrMsg))
|
||||
}
|
||||
|
||||
@ -997,12 +980,7 @@ func (s *Service) Token(ctx context.Context, request AuthUser) (token string, er
|
||||
}
|
||||
}
|
||||
|
||||
claims := consoleauth.Claims{
|
||||
ID: user.ID,
|
||||
Expiration: time.Now().Add(s.config.TokenExpirationTime),
|
||||
}
|
||||
|
||||
token, err = s.createToken(ctx, &claims)
|
||||
token, err = s.tokens.CreateToken(ctx, user.ID, "")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@ -2418,30 +2396,12 @@ func (s *Service) CreateRegToken(ctx context.Context, projLimit int) (_ *Registr
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// createToken creates string representation.
|
||||
func (s *Service) createToken(ctx context.Context, claims *consoleauth.Claims) (_ string, err error) {
|
||||
defer mon.Task()(&ctx)(&err)
|
||||
|
||||
json, err := claims.JSON()
|
||||
if err != nil {
|
||||
return "", Error.Wrap(err)
|
||||
}
|
||||
|
||||
token := consoleauth.Token{Payload: json}
|
||||
err = signToken(&token, s.Signer)
|
||||
if err != nil {
|
||||
return "", Error.Wrap(err)
|
||||
}
|
||||
|
||||
return token.String(), nil
|
||||
}
|
||||
|
||||
// authenticate validates token signature and returns authenticated *satelliteauth.Authorization.
|
||||
func (s *Service) authenticate(ctx context.Context, token consoleauth.Token) (_ *consoleauth.Claims, err error) {
|
||||
defer mon.Task()(&ctx)(&err)
|
||||
signature := token.Signature
|
||||
|
||||
err = signToken(&token, s.Signer)
|
||||
err = s.tokens.SignToken(&token)
|
||||
if err != nil {
|
||||
return nil, Error.Wrap(err)
|
||||
}
|
||||
|
@ -603,7 +603,7 @@ func TestResetPassword(t *testing.T) {
|
||||
require.True(t, console.ErrRecoveryToken.Has(err))
|
||||
|
||||
// Expect error when providing good but expired token.
|
||||
err = service.ResetPassword(ctx, token.Secret.String(), newPass, "", "", token.CreatedAt.Add(sat.Config.Console.TokenExpirationTime).Add(time.Second))
|
||||
err = service.ResetPassword(ctx, token.Secret.String(), newPass, "", "", token.CreatedAt.Add(sat.Config.ConsoleAuth.TokenExpirationTime).Add(time.Second))
|
||||
require.True(t, console.ErrTokenExpiration.Has(err))
|
||||
|
||||
// Expect error when providing good token with bad (too short) password.
|
||||
|
@ -76,7 +76,6 @@ func TestSignupCouponCodes(t *testing.T) {
|
||||
|
||||
service, err := console.NewService(
|
||||
log.Named("console"),
|
||||
&consoleauth.Hmac{Secret: []byte("my-suppa-secret-key")},
|
||||
db.Console(),
|
||||
restkeys.NewService(db.OIDC().OAuthTokens(), planet.Satellites[0].Config.RESTKeys),
|
||||
db.ProjectAccounting(),
|
||||
@ -85,6 +84,9 @@ func TestSignupCouponCodes(t *testing.T) {
|
||||
partnersService,
|
||||
paymentsService.Accounts(),
|
||||
analyticsService,
|
||||
consoleauth.NewService(consoleauth.Config{
|
||||
TokenExpirationTime: 24 * time.Hour,
|
||||
}, &consoleauth.Hmac{Secret: []byte("my-suppa-secret-key")}),
|
||||
console.Config{PasswordCost: console.TestPasswordCost, DefaultProjectLimit: 5},
|
||||
)
|
||||
|
||||
|
@ -26,6 +26,7 @@ import (
|
||||
"storj.io/storj/satellite/buckets"
|
||||
"storj.io/storj/satellite/compensation"
|
||||
"storj.io/storj/satellite/console"
|
||||
"storj.io/storj/satellite/console/consoleauth"
|
||||
"storj.io/storj/satellite/console/consoleweb"
|
||||
"storj.io/storj/satellite/console/restkeys"
|
||||
"storj.io/storj/satellite/contact"
|
||||
@ -145,8 +146,9 @@ type Config struct {
|
||||
|
||||
Payments paymentsconfig.Config
|
||||
|
||||
RESTKeys restkeys.Config
|
||||
Console consoleweb.Config
|
||||
RESTKeys restkeys.Config
|
||||
Console consoleweb.Config
|
||||
ConsoleAuth consoleauth.Config
|
||||
|
||||
Version version_checker.Config
|
||||
|
||||
|
9
scripts/testdata/satellite-config.yaml.lock
vendored
9
scripts/testdata/satellite-config.yaml.lock
vendored
@ -37,9 +37,6 @@
|
||||
# reCAPTCHA site key
|
||||
# admin.console-config.recaptcha.site-key: ""
|
||||
|
||||
# expiration time for auth tokens, account recovery tokens, and activation tokens
|
||||
# admin.console-config.token-expiration-time: 24h0m0s
|
||||
|
||||
# the default free-tier bandwidth usage limit
|
||||
# admin.console-config.usage-limits.bandwidth.free: 150.00 GB
|
||||
|
||||
@ -148,6 +145,9 @@ compensation.rates.put-tb: "0"
|
||||
# comma separated monthly withheld percentage rates
|
||||
compensation.withheld-percents: 75,75,75,50,50,50,25,25,25,0,0,0,0,0,0
|
||||
|
||||
# expiration time for auth tokens, account recovery tokens, and activation tokens
|
||||
# console-auth.token-expiration-time: 24h0m0s
|
||||
|
||||
# url link for account activation redirect
|
||||
# console.account-activation-redirect-url: ""
|
||||
|
||||
@ -307,9 +307,6 @@ compensation.withheld-percents: 75,75,75,50,50,50,25,25,25,0,0,0,0,0,0
|
||||
# url link to terms and conditions page
|
||||
# console.terms-and-conditions-url: https://storj.io/storage-sla/
|
||||
|
||||
# expiration time for auth tokens, account recovery tokens, and activation tokens
|
||||
# console.token-expiration-time: 24h0m0s
|
||||
|
||||
# the default free-tier bandwidth usage limit
|
||||
# console.usage-limits.bandwidth.free: 150.00 GB
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user