🙃
OAuth 2.0によるソーシャルログインがまだわからない人へ
OAuth 2.0によるソーシャルログイン
💡 OAuth 2.0とは?
Google、Facebook などの外部サービスのアカウントでログインできる仕組み。パスワード管理が不要になる。
こんな感じのことらしいよ
フロントエンド実装(React/TypeScript):
// OAuth設定
const GOOGLE_CLIENT_ID = process.env.REACT_APP_GOOGLE_CLIENT_ID;
const REDIRECT_URI = `${window.location.origin}/auth/callback`;
// Googleログインコンポーネント
export function GoogleLoginButton() {
const navigate = useNavigate();
// Googleログイン開始
const handleGoogleLogin = () => {
// OAuth認証URLを構築
const params = new URLSearchParams({
client_id: GOOGLE_CLIENT_ID!,
redirect_uri: REDIRECT_URI,
response_type: 'code',
scope: 'openid email profile',
access_type: 'offline', // Refresh Tokenも取得
prompt: 'consent', // 毎回同意画面を表示
});
// Google認証ページへリダイレクト
window.location.href = `https://accounts.google.com/o/oauth2/v2/auth?${params}`;
};
return (
<button
onClick={handleGoogleLogin}
className="flex items-center gap-2 px-4 py-2 border rounded-lg"
>
<img src="/google-icon.svg" alt="Google" className="w-5 h-5" />
Googleでログイン
</button>
);
}
// コールバック処理コンポーネント
export function OAuthCallback() {
const navigate = useNavigate();
const location = useLocation();
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const handleCallback = async () => {
try {
// URLパラメータから認可コードを取得
const params = new URLSearchParams(location.search);
const code = params.get('code');
const error = params.get('error');
if (error) {
throw new Error(`OAuth error: ${error}`);
}
if (!code) {
throw new Error('認可コードが見つかりません');
}
// バックエンドに認可コードを送信
const response = await fetch('/api/auth/oauth/google', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
code,
redirectUri: REDIRECT_URI,
}),
credentials: 'include',
});
if (!response.ok) {
throw new Error('認証に失敗しました');
}
const { accessToken, expiresIn, user } = await response.json();
// トークンマネージャーに保存
authManager.setTokens(accessToken, expiresIn);
// ユーザー情報を状態管理に保存
store.dispatch(setUser(user));
// ダッシュボードへリダイレクト
navigate('/dashboard');
} catch (err) {
console.error('OAuth callback error:', err);
setError(err instanceof Error ? err.message : 'エラーが発生しました');
} finally {
setLoading(false);
}
};
handleCallback();
}, [location, navigate]);
if (loading) {
return (
<div className="flex items-center justify-center h-screen">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500"></div>
<p className="ml-4">認証処理中...</p>
</div>
);
}
if (error) {
return (
<div className="flex flex-col items-center justify-center h-screen">
<p className="text-red-500 mb-4">{error}</p>
<button
onClick={() => navigate('/login')}
className="px-4 py-2 bg-blue-500 text-white rounded"
>
ログイン画面に戻る
</button>
</div>
);
}
return null;
}
バックエンド実装(Go):
package oauth
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"time"
"github.com/gin-gonic/gin"
"golang.org/x/oauth2"
"golang.org/x/oauth2/google"
)
// OAuth設定
var googleOAuthConfig = &oauth2.Config{
ClientID: os.Getenv("GOOGLE_CLIENT_ID"),
ClientSecret: os.Getenv("GOOGLE_CLIENT_SECRET"),
RedirectURL: "", // リクエストごとに設定
Scopes: []string{
"openid",
"email",
"profile",
},
Endpoint: google.Endpoint,
}
// Googleユーザー情報の構造体
type GoogleUserInfo struct {
ID string `json:"id"`
Email string `json:"email"`
VerifiedEmail bool `json:"verified_email"`
Name string `json:"name"`
Picture string `json:"picture"`
}
// OAuthハンドラー
func GoogleOAuthHandler(c *gin.Context) {
var req struct {
Code string `json:"code"`
RedirectURI string `json:"redirectUri"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(400, gin.H{"error": "Invalid request"})
return
}
// RedirectURIを設定
config := *googleOAuthConfig
config.RedirectURL = req.RedirectURI
// 認可コードをアクセストークンに交換
ctx := context.Background()
token, err := config.Exchange(ctx, req.Code)
if err != nil {
c.JSON(400, gin.H{"error": "Failed to exchange token"})
return
}
// Googleのユーザー情報を取得
client := config.Client(ctx, token)
resp, err := client.Get("https://www.googleapis.com/oauth2/v2/userinfo")
if err != nil {
c.JSON(500, gin.H{"error": "Failed to get user info"})
return
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
c.JSON(500, gin.H{"error": "Failed to read response"})
return
}
var googleUser GoogleUserInfo
if err := json.Unmarshal(body, &googleUser); err != nil {
c.JSON(500, gin.H{"error": "Failed to parse user info"})
return
}
// メールが確認済みかチェック
if !googleUser.VerifiedEmail {
c.JSON(400, gin.H{"error": "Email not verified"})
return
}
// ユーザーの存在確認または新規作成
user, err := findOrCreateUser(googleUser)
if err != nil {
c.JSON(500, gin.H{"error": "Failed to process user"})
return
}
// JWTトークンを生成
tokenService := &TokenService{secret: []byte(JWTSecret)}
accessToken, err := tokenService.GenerateAccessToken(user)
if err != nil {
c.JSON(500, gin.H{"error": "Failed to generate token"})
return
}
refreshToken, err := tokenService.GenerateRefreshToken(user.ID)
if err != nil {
c.JSON(500, gin.H{"error": "Failed to generate refresh token"})
return
}
// Refresh TokenをHttpOnly Cookieとして設定
c.SetCookie(
"refreshToken",
refreshToken,
int(RefreshTokenDuration.Seconds()),
"/api/auth/refresh",
"",
true,
true,
)
// レスポンス
c.JSON(200, gin.H{
"accessToken": accessToken,
"expiresIn": int(AccessTokenDuration.Seconds()),
"user": gin.H{
"id": user.ID,
"email": user.Email,
"name": user.Name,
"picture": user.Picture,
"role": user.Role,
},
})
}
// ユーザーの検索または作成
func findOrCreateUser(googleUser GoogleUserInfo) (*User, error) {
// メールアドレスで既存ユーザーを検索
user, err := getUserByEmail(googleUser.Email)
if err == nil {
// 既存ユーザーが見つかった場合
// Google IDとプロフィール画像を更新
user.GoogleID = googleUser.ID
user.Picture = googleUser.Picture
if err := updateUser(user); err != nil {
return nil, err
}
return user, nil
}
// 新規ユーザーを作成
newUser := &User{
ID: generateUUID(),
Email: googleUser.Email,
Name: googleUser.Name,
GoogleID: googleUser.ID,
Picture: googleUser.Picture,
Role: "candidate", // デフォルトは求職者
Provider: "google",
CreatedAt: time.Now(),
}
if err := createUser(newUser); err != nil {
return nil, err
}
// ウェルカムメールを送信(非同期)
go sendWelcomeEmail(newUser)
return newUser, nil
}
// 複数プロバイダー対応の例
type OAuthProvider interface {
GetAuthURL(state string) string
ExchangeToken(code string) (*oauth2.Token, error)
GetUserInfo(token *oauth2.Token) (*UserInfo, error)
}
// プロバイダーファクトリー
func GetOAuthProvider(provider string) (OAuthProvider, error) {
switch provider {
case "google":
return &GoogleProvider{config: googleOAuthConfig}, nil
case "github":
return &GitHubProvider{config: githubOAuthConfig}, nil
case "linkedin":
return &LinkedInProvider{config: linkedinOAuthConfig}, nil
default:
return nil, fmt.Errorf("unsupported provider: %s", provider)
}
}
Discussion