Skip to content

Commit

Permalink
telegram login using tma token / mini-app init data (#189)
Browse files Browse the repository at this point in the history
  • Loading branch information
ice-cronus authored Jul 18, 2024
1 parent 66fb208 commit b6b5553
Show file tree
Hide file tree
Showing 23 changed files with 744 additions and 116 deletions.
6 changes: 6 additions & 0 deletions application.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,12 @@ kyc/quiz:
maxAttemptsAllowed: 3
availabilityWindowSeconds: 600
globalStartDate: '2024-02-03T16:20:52.156534Z'
auth/telegram:
wintr/connectors/storage/v2: *db
telegramTokenExpiration: 1h
telegramBots:
bogusBot:
botToken: bogus
auth/email-link:
extraLoadBalancersCount: 2
wintr/connectors/storage/v2: *db
Expand Down
53 changes: 53 additions & 0 deletions auth/auth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// SPDX-License-Identifier: ice License 1.0

package auth

import (
"context"

"github.com/pkg/errors"

wintrauth "github.com/ice-blockchain/wintr/auth"
)

func NewRefresher(authClient wintrauth.Client, email, telegram ProviderRefresher) TokenRefresher {
return &tokenRefresher{
authClient: authClient,
platforms: map[string]ProviderRefresher{
platformEmail: email,
platformTelegram: telegram,
},
}
}

func (c *tokenRefresher) RegenerateTokens(ctx context.Context, previousRefreshToken string) (tokens *Tokens, err error) {
token, err := c.authClient.ParseToken(previousRefreshToken)
if err != nil {
if errors.Is(err, wintrauth.ErrExpiredToken) {
return nil, errors.Wrapf(ErrExpiredToken, "failed to verify due to expired token:%v", previousRefreshToken)
}
if errors.Is(err, wintrauth.ErrInvalidToken) {
return nil, errors.Wrapf(ErrInvalidToken, "failed to verify due to invalid token:%v", previousRefreshToken)
}

return nil, errors.Wrapf(ErrInvalidToken, "failed to verify token:%v (token:%v)", err.Error(), previousRefreshToken)
}
telegramUserID := ""
if len(token.Claims) > 0 {
if tUserIDInterface, found := token.Claims["telegramUserID"]; found {
telegramUserID = tUserIDInterface.(string) //nolint:errcheck,forcetypeassert // .
}
}
var provider ProviderRefresher
switch {
case telegramUserID != "":
provider = c.platforms[platformTelegram]
case token.Email != "":
provider = c.platforms[platformEmail]
default:
return nil, errors.Wrapf(ErrInvalidToken, "invalid token %v cannot detect both email and telegram", previousRefreshToken)
}
tokens, err = provider.RefreshToken(ctx, token)

return tokens, errors.Wrapf(err, "failed to refresh tokens for %#v", token)
}
45 changes: 45 additions & 0 deletions auth/contract.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// SPDX-License-Identifier: ice License 1.0

package auth

import (
"context"
"errors"

wintrauth "github.com/ice-blockchain/wintr/auth"
)

type (
Tokens struct {
RefreshToken string `json:"refreshToken,omitempty" example:"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJPbmxpbmUgSldUIEJ1aWxkZXIiLCJpYXQiOjE2ODQzMjQ0NTYsImV4cCI6MTcxNTg2MDQ1NiwiYXVkIjoiIiwic3ViIjoianJvY2tldEBleGFtcGxlLmNvbSIsIm90cCI6IjUxMzRhMzdkLWIyMWEtNGVhNi1hNzk2LTAxOGIwMjMwMmFhMCJ9.q3xa8Gwg2FVCRHLZqkSedH3aK8XBqykaIy85rRU40nM"` //nolint:lll // .
AccessToken string `json:"accessToken,omitempty" example:"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJPbmxpbmUgSldUIEJ1aWxkZXIiLCJpYXQiOjE2ODQzMjQ0NTYsImV4cCI6MTcxNTg2MDQ1NiwiYXVkIjoiIiwic3ViIjoianJvY2tldEBleGFtcGxlLmNvbSIsIm90cCI6IjUxMzRhMzdkLWIyMWEtNGVhNi1hNzk2LTAxOGIwMjMwMmFhMCJ9.q3xa8Gwg2FVCRHLZqkSedH3aK8XBqykaIy85rRU40nM"` //nolint:lll // .
}
TokenRefresher interface {
RegenerateTokens(ctx context.Context, prevToken string) (tokens *Tokens, err error)
}
ProviderRefresher interface {
RefreshToken(ctx context.Context, token *wintrauth.IceToken) (*Tokens, error)
}
)

const (
IceIDPrefix = "ice_"
)

var (
ErrInvalidToken = errors.New("invalid token")
ErrExpiredToken = errors.New("expired token")
)

// Private API.
type (
tokenRefresher struct {
authClient wintrauth.Client
platforms map[string]ProviderRefresher
}
)

const (
platformEmail = "email"
platformTelegram = "telegram"
)
17 changes: 7 additions & 10 deletions auth/email_link/contract.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@ import (
"github.com/golang-jwt/jwt/v5"
"github.com/pkg/errors"

"github.com/ice-blockchain/eskimo/auth"
"github.com/ice-blockchain/eskimo/users"
"github.com/ice-blockchain/wintr/auth"
wintrauth "github.com/ice-blockchain/wintr/auth"
"github.com/ice-blockchain/wintr/connectors/storage/v2"
"github.com/ice-blockchain/wintr/email"
"github.com/ice-blockchain/wintr/time"
Expand All @@ -29,28 +30,24 @@ type (
Client interface {
IceUserIDClient
SendSignInLinkToEmail(ctx context.Context, emailValue, deviceUniqueID, language, clientIP string) (loginSession string, err error)
SignIn(ctx context.Context, loginFlowToken, confirmationCode string) (tokens *Tokens, emailConfirmed bool, err error)
RegenerateTokens(ctx context.Context, prevToken string) (tokens *Tokens, err error)
SignIn(ctx context.Context, loginFlowToken, confirmationCode string) (tokens *auth.Tokens, emailConfirmed bool, err error)
UpdateMetadata(ctx context.Context, userID string, metadata *users.JSON) (*users.JSON, error)
RefreshToken(ctx context.Context, token *wintrauth.IceToken) (tokens *auth.Tokens, err error)
}
IceUserIDClient interface {
io.Closer
IceUserID(ctx context.Context, mail string) (iceID string, err error)
Metadata(ctx context.Context, userID, emailAddress string) (metadata string, metadataFields *users.JSON, err error)
}
Tokens struct {
RefreshToken string `json:"refreshToken,omitempty" example:"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJPbmxpbmUgSldUIEJ1aWxkZXIiLCJpYXQiOjE2ODQzMjQ0NTYsImV4cCI6MTcxNTg2MDQ1NiwiYXVkIjoiIiwic3ViIjoianJvY2tldEBleGFtcGxlLmNvbSIsIm90cCI6IjUxMzRhMzdkLWIyMWEtNGVhNi1hNzk2LTAxOGIwMjMwMmFhMCJ9.q3xa8Gwg2FVCRHLZqkSedH3aK8XBqykaIy85rRU40nM"` //nolint:lll // .
AccessToken string `json:"accessToken,omitempty" example:"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJPbmxpbmUgSldUIEJ1aWxkZXIiLCJpYXQiOjE2ODQzMjQ0NTYsImV4cCI6MTcxNTg2MDQ1NiwiYXVkIjoiIiwic3ViIjoianJvY2tldEBleGFtcGxlLmNvbSIsIm90cCI6IjUxMzRhMzdkLWIyMWEtNGVhNi1hNzk2LTAxOGIwMjMwMmFhMCJ9.q3xa8Gwg2FVCRHLZqkSedH3aK8XBqykaIy85rRU40nM"` //nolint:lll // .
}
Metadata struct {
UserID string `json:"userId" example:"1c0b9801-cfb2-4c4e-b48a-db18ce0894f9"`
Metadata string `json:"metadata"`
}
)

var (
ErrInvalidToken = errors.New("invalid token")
ErrExpiredToken = errors.New("expired token")
ErrInvalidToken = auth.ErrInvalidToken
ErrExpiredToken = auth.ErrExpiredToken
ErrNoConfirmationRequired = errors.New("no pending confirmation")

ErrUserDataMismatch = errors.New("parameters were not equal to user data in db")
Expand Down Expand Up @@ -94,7 +91,7 @@ type (
db *storage.DB
cfg *config
shutdown func() error
authClient auth.Client
authClient wintrauth.Client
userModifier UserModifier
emailClients []email.Client
fromRecipients []fromRecipient
Expand Down
15 changes: 8 additions & 7 deletions auth/email_link/link_verify.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,14 @@ import (
"github.com/hashicorp/go-multierror"
"github.com/pkg/errors"

"github.com/ice-blockchain/eskimo/auth"
"github.com/ice-blockchain/eskimo/users"
"github.com/ice-blockchain/wintr/auth"
wintrauth "github.com/ice-blockchain/wintr/auth"
"github.com/ice-blockchain/wintr/connectors/storage/v2"
"github.com/ice-blockchain/wintr/time"
)

func (c *client) SignIn(ctx context.Context, loginSession, confirmationCode string) (tokens *Tokens, emailConfirmed bool, err error) {
func (c *client) SignIn(ctx context.Context, loginSession, confirmationCode string) (tokens *auth.Tokens, emailConfirmed bool, err error) {
now := time.Now()
var token loginFlowToken
if err = parseJwtToken(loginSession, c.cfg.LoginSession.JwtSecret, &token); err != nil {
Expand Down Expand Up @@ -143,20 +144,20 @@ func (c *client) finishAuthProcess(
if emailConfirmed {
emailConfirmedAt = "$2"
}
mdToUpdate := users.JSON(map[string]any{auth.IceIDClaim: userID})
mdToUpdate := users.JSON(map[string]any{wintrauth.IceIDClaim: userID})
if md == nil {
empty := users.JSON(map[string]any{})
md = &empty
}
if _, hasRegisteredWith := (*md)[auth.RegisteredWithProviderClaim]; !hasRegisteredWith {
if firebaseID, hasFirebaseID := (*md)[auth.FirebaseIDClaim]; hasFirebaseID {
if _, hasRegisteredWith := (*md)[wintrauth.RegisteredWithProviderClaim]; !hasRegisteredWith {
if firebaseID, hasFirebaseID := (*md)[wintrauth.FirebaseIDClaim]; hasFirebaseID {
if !strings.HasPrefix(firebaseID.(string), iceIDPrefix) && !strings.HasPrefix(userID, iceIDPrefix) { //nolint:forcetypeassert // .
mdToUpdate[auth.RegisteredWithProviderClaim] = auth.ProviderFirebase
mdToUpdate[wintrauth.RegisteredWithProviderClaim] = wintrauth.ProviderFirebase
}
}
}
if err := mergo.Merge(&mdToUpdate, md, mergo.WithOverride, mergo.WithTypeCheck); err != nil {
return 0, errors.Wrapf(err, "failed to merge %#v and %v:%v", md, auth.IceIDClaim, userID)
return 0, errors.Wrapf(err, "failed to merge %#v and %v:%v", md, wintrauth.IceIDClaim, userID)
}
params := []any{id.Email, now.Time, userID, id.DeviceUniqueID, issuedTokenSeq, mdToUpdate}
type resp struct {
Expand Down
64 changes: 11 additions & 53 deletions auth/email_link/token_refresh.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,24 +7,13 @@ import (

"github.com/pkg/errors"

"github.com/ice-blockchain/wintr/auth"
"github.com/ice-blockchain/eskimo/auth"
wintrauth "github.com/ice-blockchain/wintr/auth"
"github.com/ice-blockchain/wintr/connectors/storage/v2"
"github.com/ice-blockchain/wintr/time"
)

//nolint:funlen // .
func (c *client) RegenerateTokens(ctx context.Context, previousRefreshToken string) (tokens *Tokens, err error) {
token, err := c.authClient.ParseToken(previousRefreshToken)
if err != nil {
if errors.Is(err, auth.ErrExpiredToken) {
return nil, errors.Wrapf(ErrExpiredToken, "failed to verify due to expired token:%v", previousRefreshToken)
}
if errors.Is(err, auth.ErrInvalidToken) {
return nil, errors.Wrapf(ErrInvalidToken, "failed to verify due to invalid token:%v", previousRefreshToken)
}

return nil, errors.Wrapf(ErrInvalidToken, "failed to verify token:%v (token:%v)", err.Error(), previousRefreshToken)
}
func (c *client) RefreshToken(ctx context.Context, token *wintrauth.IceToken) (tokens *auth.Tokens, err error) {
id := loginID{Email: token.Email, DeviceUniqueID: token.DeviceUniqueID}
usr, err := c.getUserByIDOrPk(ctx, token.Subject, &id)
if err != nil {
Expand All @@ -39,61 +28,30 @@ func (c *client) RegenerateTokens(ctx context.Context, previousRefreshToken stri
"user's email:%v does not match token's email:%v or deviceID:%v (userID %v)", usr.Email, token.Email, token.DeviceUniqueID, token.Subject)
}
now := time.Now()
refreshTokenSeq, err := c.incrementRefreshTokenSeq(ctx, &id, token.Subject, token.Seq, now)
refreshTokenSeq, err := auth.IncrementRefreshTokenSeq(ctx, c.db, "email_link_sign_ins",
"email_link_sign_ins.email = $4 AND email_link_sign_ins.device_unique_id = $5",
[]any{id.Email, id.DeviceUniqueID}, token.Subject, token.Seq, now)
if err != nil {
if storage.IsErr(err, storage.ErrNotFound) {
return nil, errors.Wrapf(ErrInvalidToken, "refreshToken with wrong sequence:%v provided (userID:%v)", token.Seq, token.Subject)
}

return nil, errors.Wrapf(err, "failed to update email link sign ins for email:%v", token.Email)
}
tokens, err = c.generateTokens(now, usr, refreshTokenSeq)

return tokens, errors.Wrapf(err, "can't generate tokens for userID:%v, email:%v", token.Subject, token.Email)
}

func (c *client) incrementRefreshTokenSeq(
ctx context.Context,
id *loginID,
userID string,
currentSeq int64,
now *time.Time,
) (tokenSeq int64, err error) {
params := []any{id.Email, id.DeviceUniqueID, now.Time, userID, currentSeq}
type resp struct {
IssuedTokenSeq int64
}
sql := `UPDATE email_link_sign_ins
SET token_issued_at = $3,
user_id = $4,
issued_token_seq = COALESCE(email_link_sign_ins.issued_token_seq, 0) + 1,
previously_issued_token_seq = (CASE WHEN COALESCE(email_link_sign_ins.issued_token_seq, 0) = $5 THEN email_link_sign_ins.issued_token_seq ELSE $5 END)
WHERE email_link_sign_ins.email = $1 AND email_link_sign_ins.device_unique_id = $2
AND email_link_sign_ins.user_id = $4
AND (email_link_sign_ins.issued_token_seq = $5
OR (email_link_sign_ins.previously_issued_token_seq <= $5 AND
email_link_sign_ins.previously_issued_token_seq<=COALESCE(email_link_sign_ins.issued_token_seq,0)+1)
)
RETURNING issued_token_seq`
updatedValue, err := storage.ExecOne[resp](ctx, c.db, sql, params...)
if err != nil {
return 0, errors.Wrapf(err, "failed to assign refreshed token to email link sign ins for params:%#v", params) //nolint:asasalint // Not this output.
}

return updatedValue.IssuedTokenSeq, nil
}

func (c *client) generateTokens(now *time.Time, els *emailLinkSignIn, seq int64) (tokens *Tokens, err error) {
func (c *client) generateTokens(now *time.Time, els *emailLinkSignIn, seq int64) (tokens *auth.Tokens, err error) {
role := ""
if els.Metadata != nil {
if roleInterface, found := (*els.Metadata)["role"]; found {
role = roleInterface.(string) //nolint:errcheck,forcetypeassert // .
}
}
refreshToken, accessToken, err := c.authClient.GenerateTokens(now, *els.UserID, els.DeviceUniqueID, els.Email, els.HashCode, seq, role)
refreshToken, accessToken, err := c.authClient.GenerateTokens(now, *els.UserID, els.DeviceUniqueID, els.Email, els.HashCode, seq, role, map[string]any{
"loginType": "email",
})
if err != nil {
return nil, errors.Wrapf(err, "failed to generate tokens for user:%#v", els)
}

return &Tokens{AccessToken: accessToken, RefreshToken: refreshToken}, nil
return &auth.Tokens{AccessToken: accessToken, RefreshToken: refreshToken}, nil
}
36 changes: 5 additions & 31 deletions auth/email_link/users.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ import (
"strings"

"dario.cat/mergo"
"github.com/google/uuid"
"github.com/pkg/errors"

"github.com/ice-blockchain/eskimo/auth"
"github.com/ice-blockchain/eskimo/users"
"github.com/ice-blockchain/wintr/connectors/storage/v2"
"github.com/ice-blockchain/wintr/terror"
Expand All @@ -30,41 +30,15 @@ func (c *client) getEmailLinkSignInByPk(ctx context.Context, id *loginID, oldEma
}

func (c *client) findOrGenerateUserID(ctx context.Context, email, oldEmail string) (userID string, err error) {
if ctx.Err() != nil {
return "", errors.Wrap(ctx.Err(), "find or generate user by id or email context failed")
}
randomID := iceIDPrefix + uuid.NewString()
searchEmail := email
if oldEmail != "" {
searchEmail = oldEmail
}

return c.getUserIDFromEmail(ctx, searchEmail, randomID)
}

func (c *client) getUserIDFromEmail(ctx context.Context, searchEmail, idIfNotFound string) (userID string, err error) {
type dbUserID struct {
ID string
}
sql := `SELECT id FROM (
SELECT users.id, 1 as idx
FROM users
WHERE email = $1
UNION ALL
(SELECT COALESCE(user_id, phone_number_to_email_migration_user_id, $2) AS id, 2 as idx
FROM email_link_sign_ins
WHERE email = $1)
) t ORDER BY idx LIMIT 1`
ids, err := storage.Select[dbUserID](ctx, c.db, sql, searchEmail, idIfNotFound)
if err != nil || len(ids) == 0 {
if storage.IsErr(err, storage.ErrNotFound) || (err == nil && len(ids) == 0) {
return idIfNotFound, nil
}

return "", errors.Wrapf(err, "failed to find user by email:%v", searchEmail)
if userID, err = auth.FindOrGenerateUserID(ctx, c.db, "email_link_sign_ins", "email", searchEmail); err != nil {
return "", errors.Wrapf(err, "failed to match userID with email %v,%v", email, oldEmail)
}

return ids[0].ID, nil
return userID, nil
}

func (c *client) isUserExist(ctx context.Context, email string) error {
Expand Down Expand Up @@ -180,7 +154,7 @@ func (c *client) IceUserID(ctx context.Context, email string) (string, error) {
if email == "" {
return "", nil
}
userID, err := c.getUserIDFromEmail(ctx, email, "")
userID, err := auth.GetUserIDFromSearch(ctx, c.db, "email_link_sign_ins", "email", email, "")
if err != nil {
return "", errors.Wrapf(err, "failed to fetch userID by email:%v", email)
}
Expand Down
12 changes: 12 additions & 0 deletions auth/telegram/DDL.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
-- SPDX-License-Identifier: ice License 1.0

CREATE TABLE IF NOT EXISTS telegram_sign_ins (
created_at timestamp NOT NULL,
token_issued_at timestamp,
issued_token_seq BIGINT DEFAULT 0 NOT NULL,
previously_issued_token_seq BIGINT DEFAULT 0 NOT NULL,
telegram_user_id text NOT NULL,
user_id TEXT,
primary key(telegram_user_id))
WITH (FILLFACTOR = 70);
CREATE INDEX IF NOT EXISTS telegram_sign_ins ON telegram_sign_ins (user_id);
Loading

0 comments on commit b6b5553

Please sign in to comment.