| 1 | = Use-case 0002 Implementation - Log in =
|
|---|
| 2 |
|
|---|
| 3 | '''Initiating actor:''' Visitor
|
|---|
| 4 |
|
|---|
| 5 | '''Other actors:''' —
|
|---|
| 6 |
|
|---|
| 7 | A registered user authenticates with a username and password so that the system
|
|---|
| 8 | treats all following actions as actions of that Trader. The system looks the user up
|
|---|
| 9 | by username and compares the stored password hash with the SHA-256 hash of the
|
|---|
| 10 | entered password. An unknown username and a wrong password give the same answer,
|
|---|
| 11 | `Invalid credentials.`, so the system does not reveal which usernames exist. After a
|
|---|
| 12 | successful login the user's id and username are kept in the in-process session and
|
|---|
| 13 | the authenticated (Trader) menu is shown, from which all other Trader use-cases
|
|---|
| 14 | start.
|
|---|
| 15 |
|
|---|
| 16 | Original use-case description (P3): [wiki:UseCase0002].
|
|---|
| 17 | Implementation: `server/auth.go`, functions `Login` and `authenticate` (password hash
|
|---|
| 18 | by `hashPassword`; the code is shown at the end of this page).
|
|---|
| 19 |
|
|---|
| 20 | == Scenario ==
|
|---|
| 21 |
|
|---|
| 22 | 1. '''Visitor''' chooses `[2] Login` in the anonymous menu (types `2`).
|
|---|
| 23 | 2. '''System''' prints `-- Login --` and asks for `Username:` and then `Password:`.
|
|---|
| 24 |
|
|---|
| 25 | The screenshot shows steps 1–2: option `2` is chosen and the `Username:` prompt
|
|---|
| 26 | is waiting for input.
|
|---|
| 27 |
|
|---|
| 28 | [[Image(uc0002_1_login.png)]]
|
|---|
| 29 |
|
|---|
| 30 | 3. '''Visitor''' enters the username and the password. (If either is empty, the
|
|---|
| 31 | system prints `Username and password are required.` without accessing the
|
|---|
| 32 | database.)
|
|---|
| 33 | 4. '''System''' looks up the user (`$1` = entered username):
|
|---|
| 34 |
|
|---|
| 35 | {{{
|
|---|
| 36 | SELECT id, password_hash FROM users WHERE username = $1
|
|---|
| 37 | }}}
|
|---|
| 38 |
|
|---|
| 39 | 5. If no row is returned (`sql.ErrNoRows` in Go), the '''System''' responds
|
|---|
| 40 | `Invalid credentials.` and the scenario ends.
|
|---|
| 41 | 6. If a row is returned, the '''System''' compares the returned `password_hash` with
|
|---|
| 42 | `hashPassword(entered password)` (hex-encoded SHA-256, computed in Go). On a
|
|---|
| 43 | mismatch it responds `Invalid credentials.` and the scenario ends.
|
|---|
| 44 |
|
|---|
| 45 | The screenshot shows this failure path with an existing user and a wrong
|
|---|
| 46 | password: `alice` / `wrongpass`. The query from step 4 finds alice's row, the
|
|---|
| 47 | hash comparison of step 6 fails, and the system prints `Invalid credentials.` and
|
|---|
| 48 | returns to the anonymous menu. (An unknown username — step 5 — prints exactly the
|
|---|
| 49 | same message.)
|
|---|
| 50 |
|
|---|
| 51 | [[Image(uc0002_5_6_invalid.png)]]
|
|---|
| 52 |
|
|---|
| 53 | 7. On a match, the '''System''' stores the returned `id` and the username in the
|
|---|
| 54 | session (`s.UserID`, `s.Username`), prints `Login successful.` and displays the
|
|---|
| 55 | authenticated menu headed `--- Logged in as alice ---`.
|
|---|
| 56 |
|
|---|
| 57 | The screenshot shows steps 3–7 of the second, successful attempt with the seed
|
|---|
| 58 | credentials `alice` / `test123` (the first, rejected attempt is still visible at
|
|---|
| 59 | the top of the window).
|
|---|
| 60 |
|
|---|
| 61 | [[Image(uc0002_7_success.png)]]
|
|---|
| 62 |
|
|---|
| 63 | The query runs on the `project` schema (the connection sets
|
|---|
| 64 | `search_path=project,public`), so `users` means `project.users`.
|
|---|
| 65 |
|
|---|
| 66 | === Alternate flow 4a (P3) — lookup combined with the live balance ===
|
|---|
| 67 |
|
|---|
| 68 | P3 describes an optional variant that checks the password in SQL and returns the
|
|---|
| 69 | balances in the same query. The P4 prototype does '''not''' use it: login always uses
|
|---|
| 70 | the query from step 4 with the hash comparison in Go, and the balances are read
|
|---|
| 71 | separately when the Trader asks for them (`[1] View balance`, see
|
|---|
| 72 | [wiki:UseCase0003Implementation UseCase0003]).
|
|---|
| 73 |
|
|---|
| 74 | == Seed credentials ==
|
|---|
| 75 |
|
|---|
| 76 | State after `./eduberza -init` (`server/db/data_load.sql`):
|
|---|
| 77 |
|
|---|
| 78 | ||= Username =||= Password =||= Available balance =||= Invested balance =||= Holdings =||
|
|---|
| 79 | || `alice` || `test123` || 8250.00 USD || 1750.00 USD || 0.5 ETH ||
|
|---|
| 80 | || `bob` || `test123` || 5000.00 USD || 0.00 USD || — ||
|
|---|
| 81 | || `charlie` || `test123` || 2500.00 USD || 0.00 USD || — ||
|
|---|
| 82 |
|
|---|
| 83 | == How to reproduce ==
|
|---|
| 84 |
|
|---|
| 85 | {{{
|
|---|
| 86 | ./eduberza -init
|
|---|
| 87 | ./eduberza
|
|---|
| 88 | # [2] Login: alice / wrongpass -> Invalid credentials.
|
|---|
| 89 | # [2] Login: alice / test123 -> Login successful. (authenticated menu)
|
|---|
| 90 | }}}
|
|---|
| 91 |
|
|---|
| 92 | The screenshots come from one real run of exactly these inputs.
|
|---|
| 93 |
|
|---|
| 94 | == Source code ==
|
|---|
| 95 |
|
|---|
| 96 | `server/auth.go` — `hashPassword`, `Login` and `authenticate`:
|
|---|
| 97 |
|
|---|
| 98 | {{{
|
|---|
| 99 | func hashPassword(pw string) string {
|
|---|
| 100 | sum := sha256.Sum256([]byte(pw))
|
|---|
| 101 | return hex.EncodeToString(sum[:])
|
|---|
| 102 | }
|
|---|
| 103 | }}}
|
|---|
| 104 |
|
|---|
| 105 | {{{
|
|---|
| 106 | // Login - UC0002
|
|---|
| 107 | func Login(s *Session) {
|
|---|
| 108 | fmt.Println("\n-- Login --")
|
|---|
| 109 | username := prompt("Username: ")
|
|---|
| 110 | pw := prompt("Password: ")
|
|---|
| 111 | if username == "" || pw == "" {
|
|---|
| 112 | fmt.Println("Username and password are required.")
|
|---|
| 113 | return
|
|---|
| 114 | }
|
|---|
| 115 |
|
|---|
| 116 | id, err := authenticate(username, pw)
|
|---|
| 117 | if err != nil {
|
|---|
| 118 | if errors.Is(err, errInvalidCreds) {
|
|---|
| 119 | fmt.Println("Invalid credentials.")
|
|---|
| 120 | return
|
|---|
| 121 | }
|
|---|
| 122 | fmt.Println("Login error:", err)
|
|---|
| 123 | return
|
|---|
| 124 | }
|
|---|
| 125 | s.UserID = id
|
|---|
| 126 | s.Username = username
|
|---|
| 127 | fmt.Println("Login successful.")
|
|---|
| 128 | }
|
|---|
| 129 |
|
|---|
| 130 | var errInvalidCreds = errors.New("invalid credentials")
|
|---|
| 131 |
|
|---|
| 132 | func authenticate(username, pw string) (string, error) {
|
|---|
| 133 | var id, stored string
|
|---|
| 134 | err := db.DB.QueryRow(
|
|---|
| 135 | `SELECT id, password_hash FROM users WHERE username = $1`,
|
|---|
| 136 | username,
|
|---|
| 137 | ).Scan(&id, &stored)
|
|---|
| 138 | if err == sql.ErrNoRows {
|
|---|
| 139 | return "", errInvalidCreds
|
|---|
| 140 | }
|
|---|
| 141 | if err != nil {
|
|---|
| 142 | return "", err
|
|---|
| 143 | }
|
|---|
| 144 | if stored != hashPassword(pw) {
|
|---|
| 145 | return "", errInvalidCreds
|
|---|
| 146 | }
|
|---|
| 147 | return id, nil
|
|---|
| 148 | }
|
|---|
| 149 | }}}
|
|---|