findLinkedUser tries to find a user by their unique OAuth-linked ID. If it does not find a match, it falls back to email-based lookup. The email fallback is restricted to first-time account linking and legacy links (empty linked_id) only. If the user found by email already has a link with a differen
(ctx context.Context, db database.Store, linkedID string, loginType database.LoginType, allowInsecureLinkedIDMismatch bool, emails ...string)
| 2189 | // |
| 2190 | //nolint:revive // allowInsecureLinkedIDMismatch is intentionally a control flag; it gates an INSECURE opt-in. |
| 2191 | func findLinkedUser(ctx context.Context, db database.Store, linkedID string, loginType database.LoginType, allowInsecureLinkedIDMismatch bool, emails ...string) (database.User, database.UserLink, error) { |
| 2192 | var ( |
| 2193 | user database.User |
| 2194 | link database.UserLink |
| 2195 | ) |
| 2196 | link, err := db.GetUserLinkByLinkedID(ctx, linkedID) |
| 2197 | if err != nil && !errors.Is(err, sql.ErrNoRows) { |
| 2198 | return user, link, xerrors.Errorf("get user auth by linked ID: %w", err) |
| 2199 | } |
| 2200 | |
| 2201 | if err == nil { |
| 2202 | user, err = db.GetUserByID(ctx, link.UserID) |
| 2203 | if err != nil { |
| 2204 | return database.User{}, database.UserLink{}, xerrors.Errorf("get user by id: %w", err) |
| 2205 | } |
| 2206 | if !user.Deleted { |
| 2207 | return user, link, nil |
| 2208 | } |
| 2209 | // If the user was deleted, act as if no account link exists. |
| 2210 | user = database.User{} |
| 2211 | } |
| 2212 | |
| 2213 | for _, email := range emails { |
| 2214 | user, err = db.GetUserByEmailOrUsername(ctx, database.GetUserByEmailOrUsernameParams{ |
| 2215 | Email: email, |
| 2216 | }) |
| 2217 | if err != nil && !errors.Is(err, sql.ErrNoRows) { |
| 2218 | return user, link, xerrors.Errorf("get user by email: %w", err) |
| 2219 | } |
| 2220 | if errors.Is(err, sql.ErrNoRows) { |
| 2221 | continue |
| 2222 | } |
| 2223 | break |
| 2224 | } |
| 2225 | |
| 2226 | if user.ID == uuid.Nil { |
| 2227 | // No user found. |
| 2228 | return database.User{}, database.UserLink{}, nil |
| 2229 | } |
| 2230 | |
| 2231 | // LEGACY: This is annoying but we have to search for the user_link |
| 2232 | // again except this time we search by user_id and login_type. It's |
| 2233 | // possible that a user_link exists without a populated 'linked_id'. |
| 2234 | link, err = db.GetUserLinkByUserIDLoginType(ctx, database.GetUserLinkByUserIDLoginTypeParams{ |
| 2235 | UserID: user.ID, |
| 2236 | LoginType: loginType, |
| 2237 | }) |
| 2238 | if err != nil && !errors.Is(err, sql.ErrNoRows) { |
| 2239 | return database.User{}, database.UserLink{}, xerrors.Errorf("get user link by user id and login type: %w", err) |
| 2240 | } |
| 2241 | |
| 2242 | // Block email fallback when an existing link has a different linked_id. |
| 2243 | // Prevents account takeover via IdP email reuse; first-time and legacy |
| 2244 | // (empty linked_id) links pass through. The INSECURE |
| 2245 | // allowInsecureLinkedIDMismatch escape hatch keeps the existing link |
| 2246 | // (and its original linked_id) and lets the login proceed. |
| 2247 | if err == nil && link.LinkedID != "" && link.LinkedID != linkedID && !allowInsecureLinkedIDMismatch { |
| 2248 | return database.User{}, database.UserLink{}, errLinkedIDAlreadyBound |
no test coverage detected