브라우저 확장 프로그램에서 “Tabs” 권한 없이 Google OAuth를 구현하는 방법
Source: Dev.to
The Architecture: How It Works
우리는 확장 프로그램 내부에서 인증을 수행하지 않습니다 (팝업 페이지가 흐름 중간에 닫힐 수 있기 때문).
대신 웹사이트에서 인증을 처리하고 세션 토큰을 확장 프로그램에 전달합니다.
Flow
- 사용자가 확장 프로그램을 설치하고 웹 앱의 회원가입 페이지로 리다이렉트됩니다.
- 사용자가 웹 앱에서 Google OAuth 또는 이메일/비밀번호로 인증합니다.
- 인증에 성공하면 토큰이 숨겨진 DOM 요소에 노출됩니다.
- 웹 앱에서 실행되는 콘텐츠 스크립트가 이 토큰들을 추출합니다.
- 토큰이 확장 프로그램의 백그라운드 스크립트로 전송됩니다.
- 백그라운드 스크립트가 세션을 생성하고 로컬에 저장합니다.
- 이제 확장 프로그램과 웹 앱이 동기화됩니다.
Setting up the Web App
먼저 Next.js (또는 React + Vite) 애플리케이션에 인증 컨텍스트를 만들어 사용자의 상태를 관리합니다. 아래 예시에서는 Supabase의 onAuthStateChange 리스너를 사용해 사용자가 로그인했을 때를 감지합니다.
Step 1: The Auth Context
contexts/AuthContext.tsx 파일을 생성합니다. 이 프로바이더는 사용자 세션을 관리하고 앱 전체에 제공됩니다.
// contexts/AuthContext.tsx
'use client';
import React, { createContext, useContext, useEffect, useState } from 'react';
import supabase from '@/lib/supabase/supabaseClient';
type AuthContextType = {
user: SupabaseUser | undefined;
session: SupabaseSession | undefined;
loading: boolean;
logout: () => Promise;
};
type AuthProviderProp = {
children: React.ReactNode;
};
const AuthContext = createContext(undefined);
export function useAuth() {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within AuthProvider');
}
return context;
}
export function AuthProvider({ children }: AuthProviderProp) {
const [user, setUser] = useState(undefined);
const [session, setSession] = useState(undefined);
const [loading, setLoading] = useState(false);
useEffect(() => {
supabase.auth.onAuthStateChange((eventName, session) => {
console.log('Auth state changed:', eventName, session);
if (eventName === 'INITIAL_SESSION' && session?.user) {
setUser(session.user);
setSession(session);
} else if (eventName === 'SIGNED_IN' && session?.user) {
setUser(session.user);
setSession(session);
} else if (eventName === 'SIGNED_OUT') {
setUser(undefined);
setSession(undefined);
}
});
}, []);
const value = {
user,
session,
loading,
logout: () => supabase.auth.signOut(),
};
return {children};
}
onAuthStateChange 리스너는 인증 상태가 변할 때마다 호출되어 UI를 업데이트하고 사용자가 성공적으로 로그인했을 때 토큰을 노출할 수 있게 합니다.
Step 2: Wrap Your App
애플리케이션(예: app/layout.tsx)을 AuthProvider로 감싸서 전체 앱이 사용자 상태에 접근하도록 합니다.
// app/layout.tsx
import { AuthProvider } from '@/contexts/AuthContext';
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
{children}
);
}
Step 3: Create the Authentication Page
사용자가 회원가입 또는 로그인할 수 있는 페이지를 만듭니다.
// app/auth/page.tsx
'use client';
import { useState } from 'react';
import supabase from '@/lib/supabase/supabaseClient';
export default function AuthPage() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [loading, setLoading] = useState(false);
async function handleRegister() {
setLoading(true);
const { data, error } = await supabase.auth.signUp({
email: email.trim(),
password,
options: {
emailRedirectTo: process.env.NEXT_PUBLIC_AUTH_SUCCESS_URL,
},
});
setLoading(false);
if (error) {
console.error('Registration error:', error);
// Handle error (show toast, etc.)
}
}
async function handleGoogleLogin() {
await supabase.auth.signInWithOAuth({
provider: 'google',
options: {
redirectTo: process.env.NEXT_PUBLIC_AUTH_SUCCESS_URL,
queryParams: {
access_type: 'offline',
prompt: 'consent',
},
},
});
}
return (
## Sign Up for Your Extension
setEmail(e.target.value)}
placeholder="Email"
/>
setPassword(e.target.value)}
placeholder="Password"
/>
Sign Up with Email
Continue with Google
);
}
Step 4: Expose Tokens via Hidden DOM Elements
숨겨진 요소(예: 푸터)에 인증 토큰을 삽입합니다. 확장 프로그램의 콘텐츠 스크립트가 이 요소들을 읽어갑니다.
// components/Footer.tsx
'use client';
import { useAuth } from '@/contexts/AuthContext';
const Footer = () => {
const { session, user } = useAuth();
return (
<>
{/* Hidden elements for the extension's content script */}
{session && (
{user?.email}
{session?.access_token}
{session?.refresh_token}
)}
{/* Your regular footer content */}
{/* Footer content */}
);
};
export default Footer;
sr-only 클래스(스크린리더 전용)는 요소를 화면에 보이지 않게 하지만 DOM에는 남아 있어 확장 프로그램이 접근할 수 있게 합니다.
Building the Chrome Extension
Step 5: Create a Content Script to Extract Tokens
콘텐츠 스크립트는 웹 앱 도메인에서 실행되어 숨겨진 토큰 요소를 감시하고 백그라운드 스크립트로 전달합니다. 아래 예시는 WXT 프레임워크를 사용한 것이며, 일반 콘텐츠 스크립트로도 변형할 수 있습니다.
// authContentScript.ts
export default defineContentScript({
matches: ['https://your-web-app.com/*'], // Replace with your domain
allFrames: true,
runAt: 'document_idle',
main() {
console.log('Auth content script initialized');
function observeAuthTokens() {
const observer = new MutationObserver(() => {
const accessTokenEl = document.querySelector('.accessTokenDiv');
const refreshTokenEl = document.querySelector('.refreshTokenDiv');
const emailEl = document.querySelector('.userEmailDiv');
if (accessTokenEl && refreshTokenEl && emailEl) {
const tokens = {
accessToken: accessTokenEl.textContent,
refreshToken: refreshTokenEl.textContent,
email: emailEl.textContent,
};
// Send tokens to background script
chrome.runtime.sendMessage({ type: 'SET_TOKENS', payload: tokens });
}
});
observer.observe(document.body, { childList: true, subtree: true });
}
observeAuthTokens();
},
});
스크립트는 DOM 변화를 감시하고 토큰 값이 나타나면 이를 추출해 chrome.runtime.sendMessage를 통해 백그라운드 스크립트로 전송합니다. 이후 storage 권한을 이용해 세션을 저장할 수 있습니다.