aboutsummaryrefslogtreecommitdiffhomepage
path: root/backend/auth/auth.go
blob: 10906f5540492b106e2c77902a0833986f7fd459 (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
package auth

import (
	"context"
	"errors"
	"time"

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

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