OAuth boilerplate'dir; asıl ısıran şey account linking. Callback'in üç durumu, şifresiz kullanıcılar, race condition'lar, Turbo'nun 302'yi yutması, credentials koruması — hepsi tek yazıda.
how2claude'a OAuth eklemek, zor olan OAuth'un kendisi değildi.
omniauth-google-oauth2 ve omniauth-github kurmak, initializer yazmak, callback yazmak, iki buton eklemek — hepsi 10 dakikalık boilerplate, herhangi bir AI agent bunu doğru yapar. Zor olan account linking: bir e-posta zaten şifreli hesaba sahipse, Google girişi geldiğinde birleştirir mi? Kullanıcının zaten Google bağlıysa ve GitHub da eklemek istiyorsa, binding mantığı nasıl? İki kişi aynı e-postayla eşzamanlı ilk Google girişi yaparsa iki User mı çıkar?
Bu gerçek hikaye. İlk uygulama Amp'te (91e4f48) yapıldı — Amp arkada Claude'u kullanıyor, büyük bir temel serme işine çok uygun etkileşim modeli. Birkaç gün sonra, 0112888 Claude Code'da Turbo-vs-OAuth çatışmasını yamaladı. İki araç birbirine doğal geçiyor.
Amp, 91e4f48'te boilerplate'i tek seferde indirdi:
Gemfile: omniauth, omniauth-google-oauth2, omniauth-githubOauthAccount modeli: provider / uid / email / name / avatar_url, [provider, uid] üzerinde unique indexAuth::OmniauthController callback action/auth/:provider/callback + /auth/failuresessions/new.html.erb iki button_to ile (Google + GitHub)omniauth initializerUser modeli tek satırlık bir değişiklik aldı — ve o tek satır asıl numara (aşağıda).
14 dosya, 257 satır eklendi. OAuth'un mekanik kısmı burada biter.
Kullanıcı "Continue with Google" tıkladığında, sonunda /auth/google_oauth2/callback'e iner. request.env["omniauth.auth"] provider, uid, email, name, avatar tutar. Karar vermen gerek:
OauthAccount satırı) → zaten bağlı User ile giriş yapKod:
class Auth::OmniauthController < ApplicationController
allow_unauthenticated_access only: [:callback, :failure]
skip_forgery_protection only: :callback
def callback
auth = request.env["omniauth.auth"]
oauth_account = OauthAccount.find_by(provider: auth.provider, uid: auth.uid)
if oauth_account
start_new_session_for(oauth_account.user)
elsif (resume_session; Current.user)
Current.user.oauth_accounts.create!(oauth_params(auth))
redirect_to root_path, notice: I18n.t("auth.oauth_linked", provider: auth.provider.titleize) and return
else
user = User.find_by(email_address: auth.info.email) || User.create!(
email_address: auth.info.email,
password_digest: ""
)
user.oauth_accounts.create!(oauth_params(auth))
start_new_session_for(user)
end
redirect_to after_authentication_url
end
private
def oauth_params(auth)
{
provider: auth.provider,
uid: auth.uid,
email: auth.info.email,
name: auth.info.name,
avatar_url: auth.info.image
}
end
end
Sıradan görünen kırk altı satır. Her dal bir tuzak saklıyor.
Rails 8'in default has_secure_password'u password_digest zorunlu kılar. Google ile giren bir kullanıcının şifresi yok — ne yapmalı?
Yapma has_secure_password'u çıkarma (şifre girişi bozulur).
Yap: default validasyonu kapat, koşullu validasyonu kendin yaz:
class User < ApplicationRecord
has_secure_password validations: false
validates :password, length: { minimum: 8 }, if: -> { password.present? }
# ...
end
validations: false: has_secure_password ile gelen "şifre zorunlu" kuralını kaldırırvalidates :password, length:, if: password.present?: kullanıcı şifre koyarsa min 8 zorla; koymazsa umursamaOAuth-only kullanıcılar password_digest: "" ile oluşturulur. Sonradan şifre ekleyebilirler (8+ karakter olmak kaydıyla). Şifreli auth kullanıcıları hiç etkilenmez.
Yan etki: mevcut "password reset" testi bozuldu — çok kısa şifre kullanıyordu. Amp aynı commit'te düzeltti (test/controllers/passwords_controller_test.rb). Agent feature eklerken test yüzey alanına bir göz at — bir re-run döngüsü kazandırır.
Callback'in üçüncü dalı:
user = User.find_by(email_address: auth.info.email) || User.create!(...)
Tek thread'de sorunsuz. Production'da iki istek neredeyse eşzamanlı gelir (nadir ama olur) — ikisi de find_by nil döner, ikisi de create!, biri başarılı olur, diğeri email_address üzerindeki unique index'e çarpar (var olduğunu varsayarsak).
Daha kötüsü: email_address üzerinde unique index yoksa, aynı e-postalı iki User oluşturulur. Aşağı akışta: Stripe customer bağlaması belirsiz, subscription lookup'ları yanlış.
Düzeltme:
add_index :users, :email_address, unique: truefind_or_create_by! kullan ya da rescue ile sar:user = User.find_by(email_address: auth.info.email) ||
User.create_with(password_digest: "").find_or_create_by!(email_address: auth.info.email)
Amp'in orijinal versiyonu sarmadı. Bir agent yüksek concurrency güvenli DB kodu yazdığında, aktif olarak "burada concurrency ne olur?" diye sormalısın. Agent'lar default olarak tek kullanıcı serial erişimi varsayar — testleri de sadece serial yolları kapsar.
İkinci dal — oturum açmış kullanıcının yeni OAuth provider bağlaması — basit görünür ama kötü bir sınır durumu var.
Kullanıcı A [email protected] ile giriş yapmış. Continue with GitHub'a tıklıyor. GitHub callback'i [email protected] döndürüyor (A'nın GitHub'ı iş e-postası kullanıyor).
Şimdi veritabanı içerebilir:
- User A: email [email protected]
- User B: email [email protected] (A aylar önce iş e-postasıyla ayrı kayıt olmuş)
Kod Current.user.oauth_accounts.create!(...) çalıştırıldığında, GitHub A'ya bağlanır. Ama User B hâlâ var, muhtemelen B'nin Purchase'ları var. B hâlâ şifreyle girebilir ama GitHub'ı artık A'nın.
Eksiksiz bir binding akışı "karşı hesabın yetim verisine ne yapıyoruz?" sorusunu ele almalı — agent'lar default olarak bunu düşünmez. Asgari olarak UI uyarısı: "Bu GitHub hesabı [email protected] e-postasına bağlı. Devam etmek o hesabın GitHub ile giriş yapmasını önleyecek."
Mevcut versiyon bunu ele almıyor — sadece Current.user'ı OAuth kaydına bağlayıp devam ediyor. Bu kasıtlı basitleştirme: problem gerçekten çıkarsa yamala. Ama sen insan olarak deliğin var olduğunu bilmelisin.
OAuth bağlandı, deploy edildi, Continue with Google'a tıkla — hiçbir şey olmuyor. Network sekmesi POST /auth/google_oauth2 → 302 accounts.google.com/o/oauth2/auth?... gösteriyor. Tarayıcı kıpırdamıyor.
Önceki yazı Claude'a iki ödeme entegrasyonu yaptırmak: Stripe + x402 aynı mekanizmayı kapsadı: button_to <form method="post"> üretir, Turbo form'u yakalar, yanıtı TURBO_STREAM olarak işler, ve TURBO_STREAM cross-origin 302'leri takip etmez.
Düzeltme 0112888'te indirdi (Claude Code'da yapıldı):
<%= button_to "/auth/google_oauth2", method: :post,
+ form: { data: { turbo: false } },
class: "..." do %>
Continue with Google
<% end %>
Her iki OAuth butonuna gerekli. Kural: hedefi harici domain'e 302 yapan her button_to (Stripe Checkout, Stripe Portal, Google OAuth, GitHub OAuth, Apple Sign-In…) data: { turbo: false } ister. Bir buton değil — bir sınıf.
Taze klonlanmış repo, GitHub credentials yapılandırılmamış — login sayfası yine de "Continue with GitHub" render ediyor. Tıklamak Invalid client_id veriyor.
Aynı commit (0112888) bir guard ekledi:
<% if Rails.application.credentials.dig(:github, :client_id).present? %>
<%= button_to "/auth/github", ... %>
<% end %>
GitHub credentials yok → buton yok. Yapmaya değer çünkü:
Bir agent'a başta bu tür "credentials guard" yazdırmak otomatik gelmez — config'in her zaman dolu olduğunu varsayar. Feature scaffold + credentials guard iki geçiştir. Ayır, tek prompt'ta isteme.
OAuth işi iki faza bölündü:
Faz 1 (Amp, 91e4f48): bir oturumda tüm OAuth levhasını yerleştirme — gems, model, migration, controller, view, route'lar, tests, i18n. 14 dosya / 257 satır / 20 dakika. Amp'in etkileşim modeli "büyük parçayı bir seferde ilerletme"ye uyuyor, temiz doğrusal thread geçmişi.
Faz 2 (Claude Code, 0112888): günler sonra canlıya geçmeden önce Turbo-vs-OAuth bizi yakaladı. Burası hassas teşhis + tek satır düzeltme + regression test alanı. Claude Code proje dizini içinde tam git/session context'e sahip, artı projenin kendi session kayıt hook'u (her şeyi otomatik olarak docs/notes/pro/raw.md'ye yakalar — bkz Claude'a kendi session'ını kaydeden hook'ları yazdırmak). Debug izi yazı malzemesine dönüşüyor.
İki araç çarpışmıyor. Amp sıfırdan scaffolding ve uzun tartışma thread'leri için; Claude Code mevcut projede artımlı iş ve mühendislik otomasyonu için. Altta aynı model (Claude Opus 4.7, 1M context) — farklı etkileşim biçimleri ve toolset'ler.
Aynı mühendis, bazen IDE'de, bazen terminalde. Her biri farklı anlara uyuyor.
Claude'a (ya da Amp'e) OAuth girişi yaptırmak — tam çek listesi:
has_secure_password validations: false + koşullu uzunluk. Şifre auth'u kaldırma.button_to'ya data-turbo=false gerekli. Stripe ile aynı bug ailesi.OAuth'un kendisi zor değil. Zor olan, agent'ların doğal olarak yüzeye çıkarmadığı sınır durumlarını kabul etmek — concurrency, yetim veri, yerel ortamda eksik credentials. İnsan olarak rolün o soruları sormak: "Concurrency nasıl? Account yetimleri ne olacak? Credentials yoksa ne olur?"
Sen soruyorsun, agent cevapları yazıyor. Bu workflow'daki gerçek iş bölümü bu.