aboutsummaryrefslogtreecommitdiffhomepage
path: root/backend/auth/auth.go
blob: 2266c50df3c56e73176dafe1736003628a6b08cd (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
package auth

import (
	"context"
	"errors"
	"log"
	"time"

	"github.com/jackc/pgx/v5"
	"golang.org/x/crypto/bcrypt"

	"github.com/nsfisis/iosdc-japan-2024-albatross/backend/account"
	"github.com/nsfisis/iosdc-japan-2024-albatross/backend/db"
	"github.com/nsfisis/iosdc-japan-2024-albatross/backend/fortee"
)

var (
	ErrInvalidRegistrationToken = errors.New("invalid registration token")
	ErrNoRegistrationToken      = errors.New("no registration token")
	ErrForteeLoginTimeout       = errors.New("fortee login timeout")
)

const (
	forteeAPITimeout = 3 * time.Second
)

func Login(
	ctx context.Context,
	queries *db.Queries,
	username string,
	password string,
	registrationToken *string,
) (int, error) {
	userAuth, err := queries.GetUserAuthByUsername(ctx, username)
	if err != nil && !errors.Is(err, pgx.ErrNoRows) {
		return 0, err
	}

	if userAuth.AuthType == "password" {
		// Authenticate with password.
		passwordHash := userAuth.PasswordHash
		if passwordHash == nil {
			panic("inconsistant data")
		}
		err := bcrypt.CompareHashAndPassword([]byte(*passwordHash), []byte(password))
		if err != nil {
			return 0, err
		}
		return int(userAuth.UserID), nil
	}

	// Authenticate with fortee.
	return verifyForteeAccountOrSignup(ctx, queries, username, password, registrationToken)
}

func verifyForteeAccountOrSignup(
	ctx context.Context,
	queries *db.Queries,
	username string,
	password string,
	registrationToken *string,
) (int, error) {
	canonicalizedUsername, err := verifyForteeAccount(ctx, username, password)
	if err != nil {
		return 0, err
	}
	userID, err := queries.GetUserIDByUsername(ctx, canonicalizedUsername)
	if err != nil {
		if errors.Is(err, pgx.ErrNoRows) {
			return signup(
				ctx,
				queries,
				canonicalizedUsername,
				registrationToken,
			)
		}
		return 0, err
	}
	return int(userID), nil
}

func signup(
	ctx context.Context,
	queries *db.Queries,
	username string,
	registrationToken *string,
) (int, error) {
	if err := verifyRegistrationToken(ctx, queries, registrationToken); err != nil {
		return 0, err
	}

	// TODO: transaction
	userID, err := queries.CreateUser(ctx, username)
	if err != nil {
		return 0, err
	}
	if err := queries.CreateUserAuth(ctx, db.CreateUserAuthParams{
		UserID:   userID,
		AuthType: "fortee",
	}); err != nil {
		return 0, err
	}
	go func() {
		err := account.FetchIcon(context.Background(), queries, int(userID))
		if err != nil {
			log.Printf("%v", err)
			// The failure is intentionally ignored. Retry manually if needed.
		}
	}()
	return int(userID), nil
}

func verifyRegistrationToken(ctx context.Context, queries *db.Queries, registrationToken *string) error {
	if registrationToken == nil {
		return ErrNoRegistrationToken
	}
	exists, err := queries.IsRegistrationTokenValid(ctx, *registrationToken)
	if err != nil {
		return err
	}
	if !exists {
		return ErrInvalidRegistrationToken
	}
	return nil
}

func verifyForteeAccount(ctx context.Context, username string, password string) (string, error) {
	ctx, cancel := context.WithTimeout(ctx, forteeAPITimeout)
	defer cancel()

	canonicalizedUsername, err := fortee.Login(ctx, username, password)
	if errors.Is(err, context.DeadlineExceeded) {
		return "", ErrForteeLoginTimeout
	}
	return canonicalizedUsername, err
}