New code verification service
Some checks failed
ci/woodpecker/push/bff Pipeline was successful
ci/woodpecker/push/db Pipeline was successful
ci/woodpecker/push/fx_ingestor Pipeline was successful
ci/woodpecker/push/billing_fees Pipeline was successful
ci/woodpecker/push/chain_gateway Pipeline was successful
ci/woodpecker/push/fx_oracle Pipeline was successful
ci/woodpecker/push/frontend Pipeline was successful
ci/woodpecker/push/nats Pipeline was successful
ci/woodpecker/push/ledger Pipeline was successful
ci/woodpecker/push/notification Pipeline was successful
ci/woodpecker/push/payments_orchestrator Pipeline was successful
ci/woodpecker/push/bump_version Pipeline failed
Some checks failed
ci/woodpecker/push/bff Pipeline was successful
ci/woodpecker/push/db Pipeline was successful
ci/woodpecker/push/fx_ingestor Pipeline was successful
ci/woodpecker/push/billing_fees Pipeline was successful
ci/woodpecker/push/chain_gateway Pipeline was successful
ci/woodpecker/push/fx_oracle Pipeline was successful
ci/woodpecker/push/frontend Pipeline was successful
ci/woodpecker/push/nats Pipeline was successful
ci/woodpecker/push/ledger Pipeline was successful
ci/woodpecker/push/notification Pipeline was successful
ci/woodpecker/push/payments_orchestrator Pipeline was successful
ci/woodpecker/push/bump_version Pipeline failed
This commit is contained in:
@@ -12,6 +12,7 @@ import (
|
||||
"github.com/tech/sendico/pkg/mservice"
|
||||
"github.com/tech/sendico/server/interface/api"
|
||||
"github.com/tech/sendico/server/interface/services/account"
|
||||
"github.com/tech/sendico/server/interface/services/confirmation"
|
||||
"github.com/tech/sendico/server/interface/services/invitation"
|
||||
"github.com/tech/sendico/server/interface/services/logo"
|
||||
"github.com/tech/sendico/server/interface/services/organization"
|
||||
@@ -76,6 +77,7 @@ func (a *APIImp) installServices() error {
|
||||
srvf := make([]api.MicroServiceFactoryT, 0)
|
||||
|
||||
srvf = append(srvf, account.Create)
|
||||
srvf = append(srvf, confirmation.Create)
|
||||
srvf = append(srvf, organization.Create)
|
||||
srvf = append(srvf, invitation.Create)
|
||||
srvf = append(srvf, logo.Create)
|
||||
|
||||
@@ -45,6 +45,10 @@ func (mw *Middleware) AccountHandler(service mservice.Type, endpoint string, met
|
||||
mw.epdispatcher.AccountHandler(service, endpoint, method, handler)
|
||||
}
|
||||
|
||||
func (mw *Middleware) PendingAccountHandler(service mservice.Type, endpoint string, method api.HTTPMethod, handler sresponse.PendingAccountHandlerFunc) {
|
||||
mw.epdispatcher.PendingAccountHandler(service, endpoint, method, handler)
|
||||
}
|
||||
|
||||
func (mw *Middleware) WSHandler(messageType string, handler wsh.HandlerFunc) {
|
||||
mw.wshandler.InstallHandler(messageType, handler)
|
||||
}
|
||||
@@ -133,7 +137,13 @@ func CreateMiddleware(logger mlogger.Logger, db db.Factory, enforcer auth.Enforc
|
||||
return nil, err
|
||||
}
|
||||
|
||||
p.epdispatcher = routers.NewDispatcher(p.logger, p.router, adb, rtdb, enforcer, config)
|
||||
cdb, err := db.NewConfirmationsDB()
|
||||
if err != nil {
|
||||
p.logger.Error("Failed to create confirmations database", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
p.epdispatcher = routers.NewDispatcher(p.logger, p.router, adb, cdb, rtdb, enforcer, config)
|
||||
p.wshandler = ws.NewRouter(p.logger, p.router, &config.WebSocket, p.apiEndpoint)
|
||||
return p, nil
|
||||
}
|
||||
|
||||
@@ -37,6 +37,9 @@ func (ar *AuthorizedRouter) tokenHandler(service mservice.Type, endpoint string,
|
||||
|
||||
func (ar *AuthorizedRouter) AccountHandler(service mservice.Type, endpoint string, method api.HTTPMethod, handler sresponse.AccountHandlerFunc) {
|
||||
hndlr := func(r *http.Request, t *emodel.AccountToken) http.HandlerFunc {
|
||||
if t.Pending {
|
||||
return response.Forbidden(ar.logger, ar.service, "confirmation_required", "pending token requires confirmation")
|
||||
}
|
||||
var a model.Account
|
||||
if err := ar.db.Get(r.Context(), t.AccountRef, &a); err != nil {
|
||||
if errors.Is(err, merrors.ErrNoData) {
|
||||
@@ -54,3 +57,18 @@ func (ar *AuthorizedRouter) AccountHandler(service mservice.Type, endpoint strin
|
||||
}
|
||||
ar.tokenHandler(service, endpoint, method, hndlr)
|
||||
}
|
||||
|
||||
func (ar *AuthorizedRouter) PendingAccountHandler(service mservice.Type, endpoint string, method api.HTTPMethod, handler sresponse.PendingAccountHandlerFunc) {
|
||||
hndlr := func(r *http.Request, t *emodel.AccountToken) http.HandlerFunc {
|
||||
var a model.Account
|
||||
if err := ar.db.Get(r.Context(), t.AccountRef, &a); err != nil {
|
||||
if errors.Is(err, merrors.ErrNoData) {
|
||||
ar.logger.Debug("Failed to find related user", zap.Error(err), mzap.ObjRef("account_ref", t.AccountRef))
|
||||
return response.NotFound(ar.logger, ar.service, err.Error())
|
||||
}
|
||||
return response.Internal(ar.logger, ar.service, err)
|
||||
}
|
||||
return handler(r, &a, t)
|
||||
}
|
||||
ar.tokenHandler(service, endpoint, method, hndlr)
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
api "github.com/tech/sendico/pkg/api/http"
|
||||
"github.com/tech/sendico/pkg/auth"
|
||||
"github.com/tech/sendico/pkg/db/account"
|
||||
"github.com/tech/sendico/pkg/db/confirmation"
|
||||
"github.com/tech/sendico/pkg/db/refreshtokens"
|
||||
"github.com/tech/sendico/pkg/mlogger"
|
||||
"github.com/tech/sendico/pkg/mservice"
|
||||
@@ -31,7 +32,11 @@ func (d *Dispatcher) AccountHandler(service mservice.Type, endpoint string, meth
|
||||
d.protected.AccountHandler(service, endpoint, method, handler)
|
||||
}
|
||||
|
||||
func NewDispatcher(logger mlogger.Logger, router chi.Router, db account.DB, rtdb refreshtokens.DB, enforcer auth.Enforcer, config *middleware.Config) *Dispatcher {
|
||||
func (d *Dispatcher) PendingAccountHandler(service mservice.Type, endpoint string, method api.HTTPMethod, handler sresponse.PendingAccountHandlerFunc) {
|
||||
d.protected.PendingAccountHandler(service, endpoint, method, handler)
|
||||
}
|
||||
|
||||
func NewDispatcher(logger mlogger.Logger, router chi.Router, db account.DB, cdb confirmation.DB, rtdb refreshtokens.DB, enforcer auth.Enforcer, config *middleware.Config) *Dispatcher {
|
||||
d := &Dispatcher{
|
||||
logger: logger.Named("api_dispatcher"),
|
||||
}
|
||||
@@ -40,7 +45,7 @@ func NewDispatcher(logger mlogger.Logger, router chi.Router, db account.DB, rtdb
|
||||
endpoint := os.Getenv(config.EndPointEnv)
|
||||
signature := middleware.SignatureConf(config)
|
||||
router.Group(func(r chi.Router) {
|
||||
d.public = rpublic.NewRouter(d.logger, endpoint, db, rtdb, r, &config.Token, &signature)
|
||||
d.public = rpublic.NewRouter(d.logger, endpoint, db, cdb, rtdb, r, &config.Token, &signature)
|
||||
})
|
||||
router.Group(func(r chi.Router) {
|
||||
d.protected = rauthorized.NewRouter(d.logger, endpoint, r, db, enforcer, &config.Token, &signature)
|
||||
|
||||
@@ -18,3 +18,13 @@ func (er *HttpEndpointRouter) CreateAccessToken(user *model.Account) (sresponse.
|
||||
}
|
||||
return token, err
|
||||
}
|
||||
|
||||
func (er *HttpEndpointRouter) CreatePendingToken(user *model.Account, ttlMinutes int) (sresponse.TokenData, error) {
|
||||
ja := jwtauth.New(er.signature.Algorithm, er.signature.PrivateKey, er.signature.PublicKey)
|
||||
_, res, err := ja.Encode(emodel.PendingAccount2Claims(user, ttlMinutes))
|
||||
token := sresponse.TokenData{
|
||||
Token: res,
|
||||
Expiration: time.Now().Add(time.Duration(ttlMinutes) * time.Minute),
|
||||
}
|
||||
return token, err
|
||||
}
|
||||
|
||||
@@ -6,14 +6,20 @@ import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/tech/sendico/pkg/api/http/response"
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
"github.com/tech/sendico/pkg/model"
|
||||
"github.com/tech/sendico/pkg/mservice"
|
||||
"github.com/tech/sendico/server/interface/api/srequest"
|
||||
"github.com/tech/sendico/server/interface/api/sresponse"
|
||||
"github.com/tech/sendico/server/internal/server/confirmationimp"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
const pendingLoginTTLMinutes = 10
|
||||
|
||||
func (pr *PublicRouter) logUserIn(ctx context.Context, r *http.Request, req *srequest.Login) http.HandlerFunc {
|
||||
// Get the account database entry
|
||||
trimmedLogin := strings.TrimSpace(req.Login)
|
||||
@@ -35,13 +41,23 @@ func (pr *PublicRouter) logUserIn(ctx context.Context, r *http.Request, req *sre
|
||||
return response.Unauthorized(pr.logger, pr.service, "password does not match")
|
||||
}
|
||||
|
||||
accessToken, err := pr.imp.CreateAccessToken(account)
|
||||
pendingToken, err := pr.imp.CreatePendingToken(account, pendingLoginTTLMinutes)
|
||||
if err != nil {
|
||||
pr.logger.Warn("Failed to generate access token", zap.Error(err))
|
||||
pr.logger.Warn("Failed to generate pending token", zap.Error(err))
|
||||
return response.Internal(pr.logger, pr.service, err)
|
||||
}
|
||||
|
||||
return pr.refreshAndRespondLogin(ctx, r, &req.SessionIdentifier, account, &accessToken)
|
||||
cfg := confirmationimp.DefaultConfig()
|
||||
_, rec, err := pr.cstore.Create(ctx, account.ID, account.Login, model.ConfirmationTargetLogin, cfg, pr.generateCode)
|
||||
if err != nil {
|
||||
pr.logger.Warn("Failed to create login confirmation code", zap.Error(err))
|
||||
return response.Internal(pr.logger, pr.service, err)
|
||||
}
|
||||
pr.logger.Info("Login confirmation code issued",
|
||||
zap.String("destination", pr.maskEmail(account.Login)),
|
||||
zap.String("account", account.Login))
|
||||
|
||||
return sresponse.LoginPending(pr.logger, account, &pendingToken, pr.maskEmail(account.Login), int(time.Until(rec.ExpiresAt).Seconds()))
|
||||
}
|
||||
|
||||
func (a *PublicRouter) login(r *http.Request) http.HandlerFunc {
|
||||
|
||||
@@ -2,59 +2,16 @@ package routers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/tech/sendico/pkg/api/http/response"
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
"github.com/tech/sendico/pkg/model"
|
||||
"github.com/tech/sendico/pkg/mutil/mzap"
|
||||
"github.com/tech/sendico/server/interface/api/sresponse"
|
||||
rtokens "github.com/tech/sendico/server/internal/api/routers/tokens"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func generateRefreshTokenData(length int) (string, error) {
|
||||
randomBytes := make([]byte, length)
|
||||
if _, err := io.ReadFull(rand.Reader, randomBytes); err != nil {
|
||||
return "", merrors.Internal("failed to generate secure random bytes: " + err.Error())
|
||||
}
|
||||
|
||||
return base64.URLEncoding.EncodeToString(randomBytes), nil
|
||||
}
|
||||
|
||||
func (er *PublicRouter) prepareRefreshToken(ctx context.Context, r *http.Request, session *model.SessionIdentifier, account *model.Account) (*model.RefreshToken, error) {
|
||||
refreshToken, err := generateRefreshTokenData(er.config.Length)
|
||||
if err != nil {
|
||||
er.logger.Warn("Failed to generate refresh token", zap.Error(err), mzap.StorableRef(account))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
token := &model.RefreshToken{
|
||||
AccountBoundBase: model.AccountBoundBase{
|
||||
AccountRef: account.GetID(),
|
||||
},
|
||||
ClientRefreshToken: model.ClientRefreshToken{
|
||||
SessionIdentifier: *session,
|
||||
RefreshToken: refreshToken,
|
||||
},
|
||||
ExpiresAt: time.Now().Add(time.Duration(er.config.Expiration.Refresh) * time.Hour),
|
||||
IsRevoked: false,
|
||||
UserAgent: r.UserAgent(),
|
||||
IPAddress: r.RemoteAddr,
|
||||
}
|
||||
|
||||
if err = er.rtdb.Create(ctx, token); err != nil {
|
||||
er.logger.Warn("Failed to store a refresh token", zap.Error(err), mzap.StorableRef(account),
|
||||
zap.String("client_id", token.ClientID), zap.String("device_id", token.DeviceID))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return token, nil
|
||||
}
|
||||
|
||||
func (pr *PublicRouter) refreshAndRespondLogin(
|
||||
ctx context.Context,
|
||||
r *http.Request,
|
||||
@@ -62,7 +19,16 @@ func (pr *PublicRouter) refreshAndRespondLogin(
|
||||
account *model.Account,
|
||||
accessToken *sresponse.TokenData,
|
||||
) http.HandlerFunc {
|
||||
refreshToken, err := pr.prepareRefreshToken(ctx, r, session, account)
|
||||
refreshToken, err := rtokens.PrepareRefreshToken(
|
||||
ctx,
|
||||
r,
|
||||
pr.rtdb,
|
||||
pr.config.Length,
|
||||
pr.config.Expiration.Refresh,
|
||||
session,
|
||||
account,
|
||||
pr.logger,
|
||||
)
|
||||
if err != nil {
|
||||
pr.logger.Warn("Failed to create refresh token", zap.Error(err), mzap.StorableRef(account),
|
||||
zap.String("client_id", session.ClientID), zap.String("device_id", session.DeviceID))
|
||||
|
||||
@@ -1,20 +1,27 @@
|
||||
package routers
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"strings"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
api "github.com/tech/sendico/pkg/api/http"
|
||||
"github.com/tech/sendico/pkg/db/account"
|
||||
"github.com/tech/sendico/pkg/db/confirmation"
|
||||
"github.com/tech/sendico/pkg/db/refreshtokens"
|
||||
"github.com/tech/sendico/pkg/mlogger"
|
||||
"github.com/tech/sendico/pkg/mservice"
|
||||
"github.com/tech/sendico/server/interface/api/sresponse"
|
||||
"github.com/tech/sendico/server/interface/middleware"
|
||||
re "github.com/tech/sendico/server/internal/api/routers/endpoint"
|
||||
"github.com/tech/sendico/server/internal/server/confirmationimp"
|
||||
)
|
||||
|
||||
type PublicRouter struct {
|
||||
logger mlogger.Logger
|
||||
db account.DB
|
||||
cdb confirmation.DB
|
||||
cstore *confirmationimp.ConfirmationStore
|
||||
imp *re.HttpEndpointRouter
|
||||
rtdb refreshtokens.DB
|
||||
config middleware.TokenConfig
|
||||
@@ -26,11 +33,39 @@ func (pr *PublicRouter) InstallHandler(service mservice.Type, endpoint string, m
|
||||
pr.imp.InstallHandler(service, endpoint, method, handler)
|
||||
}
|
||||
|
||||
func NewRouter(logger mlogger.Logger, apiEndpoint string, db account.DB, rtdb refreshtokens.DB, router chi.Router, config *middleware.TokenConfig, signature *middleware.Signature) *PublicRouter {
|
||||
func (pr *PublicRouter) generateCode() (string, error) {
|
||||
const digits = "0123456789"
|
||||
b := make([]byte, confirmationimp.DefaultConfig().CodeLength)
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
return "", err
|
||||
}
|
||||
for i := range b {
|
||||
b[i] = digits[int(b[i])%len(digits)]
|
||||
}
|
||||
return string(b), nil
|
||||
}
|
||||
|
||||
func (pr *PublicRouter) maskEmail(email string) string {
|
||||
parts := strings.Split(email, "@")
|
||||
if len(parts) != 2 {
|
||||
return email
|
||||
}
|
||||
local := parts[0]
|
||||
if len(local) > 2 {
|
||||
local = local[:1] + "***" + local[len(local)-1:]
|
||||
} else {
|
||||
local = local[:1] + "***"
|
||||
}
|
||||
return local + "@" + parts[1]
|
||||
}
|
||||
|
||||
func NewRouter(logger mlogger.Logger, apiEndpoint string, db account.DB, cdb confirmation.DB, rtdb refreshtokens.DB, router chi.Router, config *middleware.TokenConfig, signature *middleware.Signature) *PublicRouter {
|
||||
l := logger.Named("public")
|
||||
hr := PublicRouter{
|
||||
logger: l,
|
||||
db: db,
|
||||
cdb: cdb,
|
||||
cstore: confirmationimp.NewStore(cdb),
|
||||
rtdb: rtdb,
|
||||
config: *config,
|
||||
signature: *signature,
|
||||
|
||||
@@ -12,4 +12,5 @@ type APIRouter interface {
|
||||
|
||||
type ProtectedAPIRouter interface {
|
||||
AccountHandler(service mservice.Type, endpoint string, method api.HTTPMethod, handler sresponse.AccountHandlerFunc)
|
||||
PendingAccountHandler(service mservice.Type, endpoint string, method api.HTTPMethod, handler sresponse.PendingAccountHandlerFunc)
|
||||
}
|
||||
|
||||
65
api/server/internal/api/routers/tokens/tokens.go
Normal file
65
api/server/internal/api/routers/tokens/tokens.go
Normal file
@@ -0,0 +1,65 @@
|
||||
package tokens
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/tech/sendico/pkg/db/refreshtokens"
|
||||
"github.com/tech/sendico/pkg/merrors"
|
||||
"github.com/tech/sendico/pkg/mlogger"
|
||||
"github.com/tech/sendico/pkg/model"
|
||||
"github.com/tech/sendico/pkg/mutil/mzap"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func generateRefreshTokenData(length int) (string, error) {
|
||||
randomBytes := make([]byte, length)
|
||||
if _, err := io.ReadFull(rand.Reader, randomBytes); err != nil {
|
||||
return "", merrors.Internal("failed to generate secure random bytes: " + err.Error())
|
||||
}
|
||||
|
||||
return base64.URLEncoding.EncodeToString(randomBytes), nil
|
||||
}
|
||||
|
||||
func PrepareRefreshToken(
|
||||
ctx context.Context,
|
||||
r *http.Request,
|
||||
rtdb refreshtokens.DB,
|
||||
length int,
|
||||
refreshExpiration int,
|
||||
session *model.SessionIdentifier,
|
||||
account *model.Account,
|
||||
logger mlogger.Logger,
|
||||
) (*model.RefreshToken, error) {
|
||||
refreshToken, err := generateRefreshTokenData(length)
|
||||
if err != nil {
|
||||
logger.Warn("Failed to generate refresh token", zap.Error(err), mzap.StorableRef(account))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
token := &model.RefreshToken{
|
||||
AccountBoundBase: model.AccountBoundBase{
|
||||
AccountRef: account.GetID(),
|
||||
},
|
||||
ClientRefreshToken: model.ClientRefreshToken{
|
||||
SessionIdentifier: *session,
|
||||
RefreshToken: refreshToken,
|
||||
},
|
||||
ExpiresAt: time.Now().Add(time.Duration(refreshExpiration) * time.Hour),
|
||||
IsRevoked: false,
|
||||
UserAgent: r.UserAgent(),
|
||||
IPAddress: r.RemoteAddr,
|
||||
}
|
||||
|
||||
if err = rtdb.Create(ctx, token); err != nil {
|
||||
logger.Warn("Failed to store a refresh token", zap.Error(err), mzap.StorableRef(account),
|
||||
zap.String("client_id", token.ClientID), zap.String("device_id", token.DeviceID))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return token, nil
|
||||
}
|
||||
Reference in New Issue
Block a user