Google Login Entegrasyonu: OAuth 2.0 ile Adım Adım
Kullanıcı kimlik doğrulaması söz konusu olduğunda, her şeyi sıfırdan yazmak yerine Google’ın milyarlarca kullanıcıya hizmet veren altyapısını kullanmak hem güvenli hem de akıllıca bir tercih. OAuth 2.0 ile Google Login entegrasyonu, küçük bir blog sitesinden kurumsal uygulamalara kadar her ölçekte işe yarayan bir çözüm. Bu yazıda, teoriden çıkıp gerçek dünya senaryolarına geçeceğiz ve her adımı çalışan kod örnekleriyle destekleyeceğiz.
OAuth 2.0 Nedir ve Nasıl Çalışır
OAuth 2.0, kullanıcının şifresini uygulamanızla paylaşmadan üçüncü parti bir servise yetki verme protokolüdür. Google Login senaryosunda şu akış gerçekleşir:
- Kullanıcı uygulamanızdaki “Google ile Giriş Yap” butonuna tıklar
- Uygulama, kullanıcıyı Google’ın yetkilendirme sunucusuna yönlendirir
- Kullanıcı Google hesabıyla giriş yapar ve izinleri onaylar
- Google, uygulamanıza bir authorization code gönderir
- Uygulama bu kodu kullanarak access token ve refresh token alır
- Access token ile kullanıcı bilgilerine erişilir
Bu akışa Authorization Code Flow denir ve web uygulamaları için önerilen standarttır. Mobil uygulamalar için PKCE eklentisi gerekir ama bunu da ilerleyen bölümlerde ele alacağız.
Google Cloud Console’da Proje Kurulumu
Her şeyden önce Google Cloud Console’da bir proje oluşturmanız ve OAuth credentials almanız gerekiyor.
# Google Cloud CLI kurulumu (Ubuntu/Debian)
curl https://sdk.cloud.google.com | bash
exec -l $SHELL
gcloud init
# Yeni proje oluşturma
gcloud projects create my-oauth-app --name="My OAuth Application"
gcloud config set project my-oauth-app
# OAuth API'yi etkinleştirme
gcloud services enable oauth2.googleapis.com
gcloud services enable people.googleapis.com
Console üzerinden manuel olarak yapıyorsanız şu adımları izleyin:
- console.cloud.google.com adresine gidin
- “APIs & Services” > “Credentials” bölümüne girin
- “Create Credentials” > “OAuth client ID” seçin
- Application type olarak “Web application” seçin
- Authorized redirect URIs kısmına
http://localhost:3000/auth/google/callbackekleyin - Client ID ve Client Secret değerlerini not alın
Önemli not: Client Secret’ı asla version control sistemine commit etmeyin. .env dosyasına alın ve .gitignore‘a ekleyin.
Node.js ile Temel OAuth Implementasyonu
Express.js kullanan bir Node.js uygulamasında Passport.js ile Google OAuth entegrasyonu şu şekilde yapılır:
# Gerekli paketleri kur
npm init -y
npm install express passport passport-google-oauth20 express-session dotenv
npm install --save-dev nodemon
# Proje yapısı
mkdir -p src/{config,routes,middleware}
touch src/config/passport.js src/routes/auth.js .env
.env dosyanızı şöyle yapılandırın:
# .env dosyası
GOOGLE_CLIENT_ID=your_client_id_here.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=your_client_secret_here
SESSION_SECRET=supersecretkey_change_this_in_production
CALLBACK_URL=http://localhost:3000/auth/google/callback
PORT=3000
Şimdi Passport stratejisini yapılandıralım:
# src/config/passport.js içeriği
cat > src/config/passport.js << 'EOF'
const passport = require('passport');
const GoogleStrategy = require('passport-google-oauth20').Strategy;
passport.use(new GoogleStrategy({
clientID: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
callbackURL: process.env.CALLBACK_URL,
scope: ['profile', 'email']
},
async (accessToken, refreshToken, profile, done) => {
try {
// Burada kullanıcıyı veritabanında ara veya oluştur
const user = {
googleId: profile.id,
email: profile.emails[0].value,
name: profile.displayName,
avatar: profile.photos[0].value,
accessToken: accessToken
};
console.log('Kullanici giris yapti:', user.email);
return done(null, user);
} catch (error) {
return done(error, null);
}
}
));
passport.serializeUser((user, done) => {
done(null, user);
});
passport.deserializeUser((user, done) => {
done(null, user);
});
module.exports = passport;
EOF
Ana uygulama dosyasını ve rotaları ayarlayalım:
# src/app.js
cat > src/app.js << 'EOF'
require('dotenv').config();
const express = require('express');
const session = require('express-session');
const passport = require('./config/passport');
const authRoutes = require('./routes/auth');
const app = express();
app.use(session({
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: {
secure: process.env.NODE_ENV === 'production',
httpOnly: true,
maxAge: 24 * 60 * 60 * 1000 // 24 saat
}
}));
app.use(passport.initialize());
app.use(passport.session());
app.use('/auth', authRoutes);
// Korumalı route örneği
const isAuthenticated = (req, res, next) => {
if (req.isAuthenticated()) return next();
res.status(401).json({ error: 'Giris yapmaniz gerekiyor' });
};
app.get('/profile', isAuthenticated, (req, res) => {
res.json({
message: 'Hos geldiniz!',
user: {
name: req.user.name,
email: req.user.email,
avatar: req.user.avatar
}
});
});
app.get('/', (req, res) => {
res.send('<a href="/auth/google">Google ile Giris Yap</a>');
});
app.listen(process.env.PORT, () => {
console.log(`Sunucu ${process.env.PORT} portunda calisiyor`);
});
EOF
JWT ile Stateless Authentication
Session tabanlı yaklaşım monolitik uygulamalar için iyi çalışır, ancak mikroservis mimarisinde veya frontend-backend ayrımı olan projelerde JWT daha uygun olur. OAuth’tan aldığınız kullanıcı bilgisini JWT’ye dönüştürelim:
# JWT paketi kur
npm install jsonwebtoken
# JWT yardımcı fonksiyonları
cat > src/utils/jwt.js << 'EOF'
const jwt = require('jsonwebtoken');
const JWT_SECRET = process.env.JWT_SECRET || 'change-this-secret';
const JWT_EXPIRES_IN = '15m';
const REFRESH_EXPIRES_IN = '7d';
const generateAccessToken = (payload) => {
return jwt.sign(payload, JWT_SECRET, {
expiresIn: JWT_EXPIRES_IN,
issuer: 'myapp.com',
audience: 'myapp-users'
});
};
const generateRefreshToken = (payload) => {
return jwt.sign(payload, JWT_SECRET + '_refresh', {
expiresIn: REFRESH_EXPIRES_IN
});
};
const verifyToken = (token) => {
try {
return jwt.verify(token, JWT_SECRET, {
issuer: 'myapp.com',
audience: 'myapp-users'
});
} catch (error) {
if (error.name === 'TokenExpiredError') {
throw new Error('TOKEN_EXPIRED');
}
throw new Error('INVALID_TOKEN');
}
};
const verifyRefreshToken = (token) => {
try {
return jwt.verify(token, JWT_SECRET + '_refresh');
} catch (error) {
throw new Error('INVALID_REFRESH_TOKEN');
}
};
module.exports = {
generateAccessToken,
generateRefreshToken,
verifyToken,
verifyRefreshToken
};
EOF
Auth rotalarını JWT destekli hale getirelim:
# src/routes/auth.js
cat > src/routes/auth.js << 'EOF'
const express = require('express');
const passport = require('passport');
const { generateAccessToken, generateRefreshToken, verifyRefreshToken } = require('../utils/jwt');
const router = express.Router();
// Google OAuth başlat
router.get('/google', passport.authenticate('google', {
scope: ['profile', 'email'],
accessType: 'offline', // refresh_token için gerekli
prompt: 'consent' // her seferinde onay iste
}));
// Google callback
router.get('/google/callback',
passport.authenticate('google', { failureRedirect: '/login?error=auth_failed' }),
(req, res) => {
const tokenPayload = {
sub: req.user.googleId,
email: req.user.email,
name: req.user.name
};
const accessToken = generateAccessToken(tokenPayload);
const refreshToken = generateRefreshToken(tokenPayload);
// Production'da HttpOnly cookie kullanın
res.cookie('refresh_token', refreshToken, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
maxAge: 7 * 24 * 60 * 60 * 1000
});
// SPA için access token'ı URL'de gönderin veya
// güvenli bir şekilde aktarın
res.redirect(`/dashboard?token=${accessToken}`);
}
);
// Token yenileme endpoint'i
router.post('/refresh', (req, res) => {
const refreshToken = req.cookies.refresh_token;
if (!refreshToken) {
return res.status(401).json({ error: 'Refresh token bulunamadi' });
}
try {
const decoded = verifyRefreshToken(refreshToken);
const newAccessToken = generateAccessToken({
sub: decoded.sub,
email: decoded.email,
name: decoded.name
});
res.json({ accessToken: newAccessToken });
} catch (error) {
res.status(401).json({ error: 'Gecersiz refresh token' });
}
});
// Logout
router.post('/logout', (req, res) => {
res.clearCookie('refresh_token');
req.logout(() => {
res.json({ message: 'Cikis yapildi' });
});
});
module.exports = router;
EOF
Python/FastAPI ile Google OAuth Entegrasyonu
Node.js dışında Python dünyasından da bir örnek verelim. FastAPI ile Authlib kütüphanesi kullanarak Google OAuth:
# Python ortamını hazırla
python3 -m venv venv
source venv/bin/activate
pip install fastapi uvicorn authlib httpx python-jose[cryptography] python-dotenv
# main.py oluştur
cat > main.py << 'EOF'
from fastapi import FastAPI, Request, HTTPException, Depends
from fastapi.responses import RedirectResponse, JSONResponse
from authlib.integrations.starlette_client import OAuth
from starlette.middleware.sessions import SessionMiddleware
from jose import JWTError, jwt
from datetime import datetime, timedelta
import os
from dotenv import load_dotenv
load_dotenv()
app = FastAPI(title="Google OAuth Demo")
app.add_middleware(
SessionMiddleware,
secret_key=os.getenv("SESSION_SECRET", "change-this"),
max_age=3600
)
oauth = OAuth()
oauth.register(
name='google',
client_id=os.getenv('GOOGLE_CLIENT_ID'),
client_secret=os.getenv('GOOGLE_CLIENT_SECRET'),
server_metadata_url='https://accounts.google.com/.well-known/openid-configuration',
client_kwargs={
'scope': 'openid email profile',
'prompt': 'select_account'
}
)
SECRET_KEY = os.getenv("JWT_SECRET", "change-this-secret")
ALGORITHM = "HS256"
def create_jwt_token(data: dict, expires_delta: timedelta = timedelta(minutes=15)):
to_encode = data.copy()
expire = datetime.utcnow() + expires_delta
to_encode.update({"exp": expire})
return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
@app.get("/auth/google")
async def login(request: Request):
redirect_uri = os.getenv('CALLBACK_URL')
return await oauth.google.authorize_redirect(request, redirect_uri)
@app.get("/auth/google/callback")
async def auth_callback(request: Request):
try:
token = await oauth.google.authorize_access_token(request)
user_info = token.get('userinfo')
if not user_info:
raise HTTPException(status_code=400, detail="Kullanici bilgisi alinamadi")
jwt_token = create_jwt_token({
"sub": user_info['sub'],
"email": user_info['email'],
"name": user_info['name']
})
response = RedirectResponse(url="/dashboard")
response.set_cookie(
key="access_token",
value=jwt_token,
httponly=True,
secure=os.getenv("ENV") == "production",
samesite="strict"
)
return response
except Exception as e:
raise HTTPException(status_code=400, detail=f"Auth hatasi: {str(e)}")
@app.get("/me")
async def get_current_user(request: Request):
token = request.cookies.get("access_token")
if not token:
raise HTTPException(status_code=401, detail="Oturum acilmamis")
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
return {"email": payload["email"], "name": payload["name"]}
except JWTError:
raise HTTPException(status_code=401, detail="Gecersiz token")
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)
EOF
# Uygulamayı başlat
uvicorn main:app --reload --port 8000
Production Ortamı için Güvenlik Kontrolleri
Development ortamında çalışan kodu production’a taşırken dikkat edilmesi gereken kritik noktalar var:
# Nginx reverse proxy konfigürasyonu
cat > /etc/nginx/sites-available/oauth-app << 'EOF'
server {
listen 443 ssl http2;
server_name yourdomain.com;
ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
# HSTS header
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
# CSRF koruması için
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
location / {
proxy_pass http://localhost:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
}
}
# HTTP'yi HTTPS'e yönlendir
server {
listen 80;
server_name yourdomain.com;
return 301 https://$server_name$request_uri;
}
EOF
# SSL sertifikası al
certbot --nginx -d yourdomain.com
# Nginx'i yeniden başlat
systemctl reload nginx
State Parametresi ile CSRF Koruması
OAuth akışının güvenli olması için state parametresi kritik öneme sahiptir. Bu parametreyi atlarsanız CSRF saldırılarına açık hale gelirsiniz:
# State parametresi implementasyonu
cat > src/middleware/oauthState.js << 'EOF'
const crypto = require('crypto');
const generateState = () => {
return crypto.randomBytes(32).toString('hex');
};
const setOAuthState = (req, res, next) => {
const state = generateState();
// State'i session'a kaydet
req.session.oauthState = state;
req.session.oauthStateCreatedAt = Date.now();
res.locals.oauthState = state;
next();
};
const validateOAuthState = (req, res, next) => {
const { state } = req.query;
const sessionState = req.session.oauthState;
const stateAge = Date.now() - (req.session.oauthStateCreatedAt || 0);
// State 10 dakikadan eski olmamalı
const MAX_STATE_AGE = 10 * 60 * 1000;
if (!state || !sessionState) {
return res.status(400).json({ error: 'State parametresi eksik' });
}
if (state !== sessionState) {
console.warn('CSRF saldiri denemesi tespit edildi!', {
ip: req.ip,
userAgent: req.headers['user-agent'],
timestamp: new Date().toISOString()
});
return res.status(403).json({ error: 'Gecersiz state parametresi' });
}
if (stateAge > MAX_STATE_AGE) {
return res.status(400).json({ error: 'OAuth akisi zaman asimina ugradi' });
}
// State'i kullandıktan sonra sil (replay attack önlemi)
delete req.session.oauthState;
delete req.session.oauthStateCreatedAt;
next();
};
module.exports = { setOAuthState, validateOAuthState };
EOF
Token Yönetimi ve Refresh Token Rotasyonu
Production uygulamalarında refresh token’ların tek kullanımlık olması (token rotation) güvenliği artırır:
# Redis ile token blacklist ve rotation
npm install ioredis
cat > src/services/tokenService.js << 'EOF'
const Redis = require('ioredis');
const { generateAccessToken, generateRefreshToken, verifyRefreshToken } = require('../utils/jwt');
const redis = new Redis({
host: process.env.REDIS_HOST || 'localhost',
port: process.env.REDIS_PORT || 6379,
password: process.env.REDIS_PASSWORD
});
const REFRESH_TOKEN_TTL = 7 * 24 * 60 * 60; // 7 gün (saniye)
const storeRefreshToken = async (userId, token) => {
const key = `refresh_token:${userId}`;
await redis.setex(key, REFRESH_TOKEN_TTL, token);
};
const rotateRefreshToken = async (oldToken) => {
let decoded;
try {
decoded = verifyRefreshToken(oldToken);
} catch (error) {
throw new Error('Gecersiz refresh token');
}
const storedToken = await redis.get(`refresh_token:${decoded.sub}`);
if (!storedToken || storedToken !== oldToken) {
// Token çalınmış olabilir! Tüm oturumları sonlandır
await redis.del(`refresh_token:${decoded.sub}`);
console.error('Token yeniden kullanim denemesi!', {
userId: decoded.sub,
timestamp: new Date().toISOString()
});
throw new Error('TOKEN_REUSE_DETECTED');
}
// Yeni token çifti oluştur
const payload = { sub: decoded.sub, email: decoded.email, name: decoded.name };
const newAccessToken = generateAccessToken(payload);
const newRefreshToken = generateRefreshToken(payload);
// Eski token'ı sil, yenisini kaydet
await storeRefreshToken(decoded.sub, newRefreshToken);
return { accessToken: newAccessToken, refreshToken: newRefreshToken };
};
const revokeAllTokens = async (userId) => {
await redis.del(`refresh_token:${userId}`);
};
module.exports = { storeRefreshToken, rotateRefreshToken, revokeAllTokens };
EOF
Hata Ayıklama ve Monitoring
OAuth entegrasyonunda sık karşılaşılan sorunları ve çözüm yollarını bilmek iş hayatınızı kolaylaştırır:
- redirect_uri_mismatch hatası: Google Console’daki callback URL ile kodunuzdaki URL birebir eşleşmeli. Trailing slash bile fark yaratır
- invalid_client hatası: Client ID veya Secret yanlış.
.envdosyasını kontrol edin, boşluk veya satır sonu karakteri olmadığından emin olun - access_denied hatası: Kullanıcı izin vermedi veya uygulama henüz Google tarafından onaylanmadı
- Token expired: Access token süresi dolmuş, refresh token akışını tetikleyin
# OAuth hatalarını loglama scripti
cat > src/middleware/oauthLogger.js << 'EOF'
const fs = require('fs');
const path = require('path');
const LOG_FILE = path.join(__dirname, '../../logs/oauth.log');
// Log dizinini oluştur
const logDir = path.dirname(LOG_FILE);
if (!fs.existsSync(logDir)) {
fs.mkdirSync(logDir, { recursive: true });
}
const logOAuthEvent = (event, data) => {
const logEntry = {
timestamp: new Date().toISOString(),
event,
...data
};
const logLine = JSON.stringify(logEntry) + 'n';
fs.appendFileSync(LOG_FILE, logLine);
if (process.env.NODE_ENV !== 'production') {
console.log(`[OAuth] ${event}:`, data);
}
};
const oauthLogger = (req, res, next) => {
const originalJson = res.json.bind(res);
res.json = (body) => {
if (res.statusCode >= 400) {
logOAuthEvent('AUTH_ERROR', {
statusCode: res.statusCode,
error: body.error,
ip: req.ip,
path: req.path,
userAgent: req.headers['user-agent']
});
}
return originalJson(body);
};
next();
};
module.exports = { oauthLogger, logOAuthEvent };
EOF
Gerçek Dünya Senaryosu: Multi-tenant SaaS Uygulaması
Birden fazla organizasyona hizmet veren bir SaaS uygulamasında Google Workspace hesapları için domain kısıtlaması eklemek yaygın bir ihtiyaçtır:
# Sadece belirli bir domain'e izin ver
cat >> src/config/passport.js << 'EOF'
const ALLOWED_DOMAINS = process.env.ALLOWED_DOMAINS
? process.env.ALLOWED_DOMAINS.split(',')
: [];
const isDomainAllowed = (email) => {
if (ALLOWED_DOMAINS.length === 0) return true;
const domain = email.split('@')[1];
return ALLOWED_DOMAINS.includes(domain);
};
// GoogleStrategy callback'ini güncelle
passport.use('google-workspace', new GoogleStrategy({
clientID: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
callbackURL: process.env.CALLBACK_URL,
// hd parametresi ile Google tarafında da filtrele
hd: process.env.ALLOWED_DOMAIN || '*'
},
async (accessToken, refreshToken, profile, done) => {
const email = profile.emails[0].value;
if (!isDomainAllowed(email)) {
return done(null, false, {
message: `${email} domaini bu uygulamaya erisemez`
});
}
const user = {
googleId: profile.id,
email: email,
name: profile.displayName,
domain: email.split('@')[1],
organization: profile._json.hd || 'personal'
};
return done(null, user);
}
));
EOF
Sonuç
Google OAuth 2.0 entegrasyonu, dikkatli bir şekilde uygulandığında hem kullanıcı deneyimini iyileştirir hem de güvenlik yükünü önemli ölçüde azaltır. Bu yazıda ele aldığımız konuları özetleyecek olursak:
- Google Cloud Console kurulumu en kritik ilk adımdır ve hataların büyük kısmı buradan kaynaklanır
- State parametresi ihmal edilmemeli, CSRF koruması için zorunludur
- JWT ile session tabanlı yaklaşım arasındaki seçim, mimarinize göre yapılmalıdır; mikroservisler için JWT daha uygundur
- Refresh token rotasyonu production ortamlarında güvenlik için önerilir
- Redis ile token yönetimi ölçeklenebilir sistemlerde merkezi kontrol sağlar
- Domain kısıtlaması kurumsal uygulamalarda erişim kontrolü için kullanışlıdır
Şunu da belirtmek gerekir: OAuth kütüphaneleri sizi büyük hatalardan korur, ancak kütüphaneyi doğru konfigüre etmek ve callback URL’lerini, secret’ları güvenle yönetmek sizin sorumluluğunuzdur. Production’a geçmeden önce mutlaka bir güvenlik değerlendirmesi yapın ve özellikle token storage ve transmission konularına odaklanın. Her şey çalışıyor gibi göründüğünde bile, loglara düzenli bakın. OAuth hatalarının ciddi bir kısmı ancak loglar sayesinde fark edilir.
