package signin import ( "errors" "fmt" "log/slog" "net/http" "tug/internal/service/identity" "tug/internal/transport/google" "tug/internal/transport/web" ) func (s *Signin) LoginEntries() []web.LoginEntry { if s.google == nil { return nil } return []web.LoginEntry{web.GoogleEntry()} } func (s *Signin) GoogleRoutes() []web.Route { if s.google == nil { return nil } routes := []web.Route{ {Method: http.MethodPost, Path: web.GoogleLoginPath, Access: web.Public, Handler: http.HandlerFunc(s.googleStart)}, {Method: http.MethodGet, Path: web.GoogleCallbackPath, Access: web.Public, Handler: http.HandlerFunc(s.googleCallback)}, } return append(routes, s.kit.AccountGoogleRoutes(web.GoogleAccount{Client: linkStart{rp: s.google}, Links: googleLinks{a: s.account}})...) } func (s *Signin) googleStart(w http.ResponseWriter, r *http.Request) { state := r.PostFormValue("state") if _, err := s.login.v.BoundClient(r.Context(), state, s.login.proof(r, state)); err != nil { s.kit.FinishLogin(w, r, state, "", err, s.LoginEntries()...) return } target, err := s.google.Start(w, r, google.Begin{Purpose: google.SignIn, DexState: state}) if errors.Is(err, google.ErrBegin) { err = fmt.Errorf("%w: %w", web.ErrLoginExpired, err) } if err != nil { s.kit.FinishLogin(w, r, state, "", err, s.LoginEntries()...) return } s.kit.Continue(w, r, target) } func (s *Signin) googleCallback(w http.ResponseWriter, r *http.Request) { p, _ := web.PrincipalFrom(r) res, err := s.google.Finish(w, r, p.SessionID, google.SignIn, google.Link) switch res.Txn.Purpose { case google.SignIn: target, err := s.login.google(w, r, res, err) s.kit.FinishLogin(w, r, res.Txn.DexState, target, err, s.LoginEntries()...) case google.Link: s.kit.FinishGoogleLink(w, r, googleLinks{a: s.account}, web.GoogleIdentity{Subject: res.Sub}, linkError(err)) default: s.kit.FinishLogin(w, r, "", "", unauthenticated(err), s.LoginEntries()...) } } func unauthenticated(err error) error { switch { case errors.Is(err, google.ErrTxnHost), errors.Is(err, google.ErrState): return fmt.Errorf("%w: %w", web.ErrFormExpired, err) default: return fmt.Errorf("%w: %w", web.ErrStepExpired, err) } } func signInError(err error) error { switch { case errors.Is(err, google.ErrCancelled): return fmt.Errorf("%w: %w", web.ErrLoginCancelled, err) case errors.Is(err, google.ErrEmailUnverified), errors.Is(err, google.ErrDomain), errors.Is(err, google.ErrPurpose): return fmt.Errorf("%w: %w", web.ErrLoginRefused, err) default: return fmt.Errorf("%w: %w", web.ErrLoginUnreachable, err) } } func linkError(err error) error { switch { case err == nil: return nil case errors.Is(err, google.ErrSession): return fmt.Errorf("%w: %w", web.ErrGoogleOtherSession, err) case errors.Is(err, google.ErrEmailUnverified), errors.Is(err, google.ErrDomain): return fmt.Errorf("%w: %w", web.ErrGoogleNotAllowed, err) default: return fmt.Errorf("%w: %w", web.ErrGoogleRefused, err) } } func policyReason(err error) string { switch { case errors.Is(err, google.ErrEmailUnverified): return identity.GoogleRefusedUnverified case errors.Is(err, google.ErrDomain): return identity.GoogleRefusedDomain } return "" } func refusalNotAudited(err error) error { return fmt.Errorf("signin: audit the refused Google sign-in: %w", err) } func (l login) google(w http.ResponseWriter, r *http.Request, res google.Result, ferr error) (string, error) { ctx := r.Context() state := res.Txn.DexState proof := l.proof(r, state) client, err := l.v.BoundClient(ctx, state, proof) if err != nil { return "", err } if ferr != nil { if reason := policyReason(ferr); reason != "" { if err := l.id.GoogleRefusedSubject(ctx, res.Sub, reason); err != nil { return "", refusalNotAudited(err) } } return "", signInError(ferr) } browser := ensureBrowser(w, r) u, err := l.id.GoogleUser(ctx, res.Sub) switch { case errors.Is(err, identity.ErrGoogleUnlinked): slog.Info("google sign-in for an unlinked account") return "", fmt.Errorf("%w: %w", web.ErrLoginUnlinked, err) case errors.Is(err, identity.ErrDisabled): if err := l.id.GoogleRefused(ctx, u.UserID, identity.GoogleRefusedDisabled); err != nil { return "", refusalNotAudited(err) } return "", fmt.Errorf("%w: %w", web.ErrLoginRefused, err) case errors.Is(err, identity.ErrUnavailable): return "", fmt.Errorf("%w: %w", web.ErrLoginBusy, err) case err != nil: return "", err } target, err := l.v.Admit(ctx, u.UserID, u.CredentialsEpoch, state, browser, proof) if err != nil { return "", err } if err := l.openAccount(w, r, u.UserID, "google", client, u.CredentialsEpoch, browser); err != nil { return "", err } l.rp.ClearLogin(w, state) return target, nil } type linkStart struct { rp *google.RP } func (l linkStart) StartLink(w http.ResponseWriter, r *http.Request, p web.Principal) (string, error) { return l.rp.Start(w, r, google.Begin{Purpose: google.Link, SessionID: p.SessionID, SudoUntil: p.SudoUntil}) }