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:
Cameron 2022-04-19 16:50:15 -04:00 committed by Cameron
parent 763bfc0913
commit 0633aca607
10 changed files with 122 additions and 85 deletions

View File

@ -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 {

View File

@ -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

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

View File

@ -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)

View File

@ -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)

View File

@ -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)
}

View File

@ -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.

View File

@ -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},
)

View File

@ -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

View File

@ -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