package auth import ( "context" "database/sql" "errors" "net/url" "strings" ) func (s *service) identityGrant(ctx context.Context, project, provider, subject string, email *string, name, picture, challenge string) (string, error) { name = strings.TrimSpace(name) if len(name) > 160 { name = "" } if u, err := url.Parse(picture); err != nil || u.Scheme != "https" || u.Hostname() == "" || u.User != nil || len(picture) > 2048 { picture = "" } tx, err := s.db.BeginTx(ctx, nil) if err != nil { return "", err } defer tx.Rollback() // Serialize first registration for the same provider identity. if _, err = tx.ExecContext(ctx, "SELECT pg_advisory_xact_lock(hashtextextended($1,0))", project+":"+provider+":"+subject); err != nil { return "", err } var account string err = tx.QueryRowContext(ctx, "SELECT account_id::text FROM account_identities WHERE project=$1 AND provider=$2 AND subject=$3", project, provider, subject).Scan(&account) if errors.Is(err, sql.ErrNoRows) { var googleSubject *string if provider == "google" { googleSubject = &subject } err = tx.QueryRowContext(ctx, ` WITH identity AS(SELECT gen_random_uuid() AS id) INSERT INTO project_accounts(project,id,google_subject,email,username,display_name,avatar_url) SELECT $1,id,$2,$3, left(COALESCE(NULLIF(regexp_replace(lower($4),'[^a-z0-9]','','g'),''),'user'),7)||'_'||replace(id::text,'-',''), $4,NULLIF($5,'') FROM identity RETURNING id::text `, project, googleSubject, email, name, picture).Scan(&account) if err != nil { return "", err } _, err = tx.ExecContext(ctx, "INSERT INTO account_identities(project,provider,subject,account_id) VALUES($1,$2,$3,$4)", project, provider, subject, account) } else if err == nil { _, err = tx.ExecContext(ctx, ` UPDATE project_accounts SET email=COALESCE($3,email), display_name=CASE WHEN display_name='' THEN $4 ELSE display_name END, avatar_url=COALESCE(avatar_url,NULLIF($5,'')), username=COALESCE(username,left(COALESCE(NULLIF(regexp_replace(lower($4),'[^a-z0-9]','','g'),''),'user'),7)||'_'||replace(id::text,'-','')), updated_at=now() WHERE project=$1 AND id=$2 `, project, account, email, name, picture) } if err != nil { return "", err } grant := token() _, err = tx.ExecContext(ctx, "INSERT INTO login_grants VALUES($1,$2,$3,$4,now()+interval '1 minute')", digest(grant), project, account, challenge) if err != nil { return "", err } if err = tx.Commit(); err != nil { return "", err } return grant, nil }