Organize backend into product executable and shared Go modules
This commit is contained in:
1 parent
981e84a4b4
commit
f0720ec66b
382 files changed
+1944
-6989
No files matched your search
@@ -0,0 +1,103 @@
|
||||
package tokens
|
||||
|
||||
import (
|
||||
"context"
|
||||
customermodel "git.sendico.io/tech/sxchnge/product/customers/model"
|
||||
sessionmodel "git.sendico.io/tech/sxchnge/product/sessions/model"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"git.sendico.io/tech/sxchnge/product/sessions/store"
|
||||
"go.mongodb.org/mongo-driver/v2/bson"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type recordingRefreshTokenDB struct {
|
||||
refreshtokens.DB
|
||||
token *sessionmodel.RefreshToken
|
||||
}
|
||||
|
||||
func (db *recordingRefreshTokenDB) Create(_ context.Context, token *sessionmodel.RefreshToken) error {
|
||||
db.token = token
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestPrepareRefreshTokenStoresResolvedClientIP(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
remoteAddr string
|
||||
realIP string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "proxied IPv4",
|
||||
remoteAddr: "172.19.0.37:45706",
|
||||
realIP: "203.0.113.10",
|
||||
want: "203.0.113.10",
|
||||
},
|
||||
{
|
||||
name: "direct IPv4",
|
||||
remoteAddr: "203.0.113.11:45706",
|
||||
want: "203.0.113.11",
|
||||
},
|
||||
{
|
||||
name: "proxied IPv6",
|
||||
remoteAddr: "172.19.0.37:45706",
|
||||
realIP: "2001:0db8:0000:0000:0000:0000:0000:0012",
|
||||
want: "2001:db8::12",
|
||||
},
|
||||
{
|
||||
name: "direct IPv6",
|
||||
remoteAddr: "[2001:db8::13]:45706",
|
||||
want: "2001:db8::13",
|
||||
},
|
||||
{
|
||||
name: "missing address",
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "invalid address",
|
||||
remoteAddr: "not-an-address",
|
||||
realIP: "also-not-an-address",
|
||||
want: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
request := &http.Request{
|
||||
RemoteAddr: tt.remoteAddr,
|
||||
Header: make(http.Header),
|
||||
}
|
||||
if tt.realIP != "" {
|
||||
request.Header.Set("X-Real-IP", tt.realIP)
|
||||
}
|
||||
|
||||
account := &customermodel.Account{}
|
||||
account.SetID(bson.NewObjectID())
|
||||
db := &recordingRefreshTokenDB{}
|
||||
_, err := PrepareRefreshToken(
|
||||
context.Background(),
|
||||
request,
|
||||
db,
|
||||
32,
|
||||
24,
|
||||
&sessionmodel.SessionIdentifier{ClientID: "client", DeviceID: "device"},
|
||||
account,
|
||||
zap.NewNop(),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("prepare refresh token: %v", err)
|
||||
}
|
||||
if db.token == nil {
|
||||
t.Fatal("expected refresh token to be stored")
|
||||
}
|
||||
if db.token.IPAddress != tt.want {
|
||||
t.Fatalf("unexpected stored IP address: got %q, want %q", db.token.IPAddress, tt.want)
|
||||
}
|
||||
if db.token.SessionVersion == "" {
|
||||
t.Fatal("expected a server-generated session version")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package tokens
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
customermodel "git.sendico.io/tech/sxchnge/product/customers/model"
|
||||
sessionmodel "git.sendico.io/tech/sxchnge/product/sessions/model"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"git.sendico.io/tech/sxchnge/product/auth/ipguard"
|
||||
"git.sendico.io/tech/sxchnge/product/sessions/store"
|
||||
"git.sendico.io/tech/sxchnge/shared/errors"
|
||||
"git.sendico.io/tech/sxchnge/shared/logging"
|
||||
"git.sendico.io/tech/sxchnge/shared/logging/fields"
|
||||
"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.InternalWrap(err, "failed to generate secure random bytes")
|
||||
}
|
||||
|
||||
return base64.URLEncoding.EncodeToString(randomBytes), nil
|
||||
}
|
||||
|
||||
func PrepareRefreshToken(
|
||||
ctx context.Context,
|
||||
r *http.Request,
|
||||
rtdb refreshtokens.DB,
|
||||
length int,
|
||||
refreshExpiration int,
|
||||
session *sessionmodel.SessionIdentifier,
|
||||
account *customermodel.Account,
|
||||
logger mlogger.Logger,
|
||||
) (*sessionmodel.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
|
||||
}
|
||||
sessionVersion, err := generateRefreshTokenData(16)
|
||||
if err != nil {
|
||||
logger.Warn("Failed to generate session version", zap.Error(err), mzap.StorableRef(account))
|
||||
return nil, err
|
||||
}
|
||||
versionedSession := *session
|
||||
versionedSession.SessionVersion = sessionVersion
|
||||
|
||||
clientIP := ipguard.ClientIP(r)
|
||||
ipAddress := ""
|
||||
if clientIP != nil {
|
||||
ipAddress = clientIP.String()
|
||||
}
|
||||
|
||||
token := &sessionmodel.RefreshToken{
|
||||
AccountRef: account.GetID(),
|
||||
ClientRefreshToken: sessionmodel.ClientRefreshToken{
|
||||
SessionIdentifier: versionedSession,
|
||||
RefreshToken: refreshToken,
|
||||
},
|
||||
ExpiresAt: time.Now().Add(time.Duration(refreshExpiration) * time.Hour),
|
||||
IsRevoked: false,
|
||||
UserAgent: r.UserAgent(),
|
||||
IPAddress: ipAddress,
|
||||
}
|
||||
|
||||
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