Introduction
While working on a side project, I thought, "Why not add a Google login feature?"
"Google login? Shouldn't it be as simple as creating a button and calling a few APIs?"
Little did I know how much of a learning journey this thought would become. From my first encounter with the term OAuth 2.0, I learned so much. The experiences of getting lost in the Google Cloud Console, almost committing environment variables to Git, and struggling to figure out why a popup wouldn't appear have now become a valuable learning process.
"It was more complicated than I thought... but I figured it out one step at a time!"
I wrote this article in the hope that junior developers can experience fewer trials and errors when implementing Google login. I will honestly share the actual code implemented in a Svelte/SvelteKit environment, along with the choices I made and the problems I faced.
1. Trial and Error 1 - Confusion in Google Cloud Console Settings
The first step I found when searching for "Google login implementation" was to create an OAuth 2.0 Client ID in the Google Cloud Console. However, there were more steps than I expected.
Mistake 1: Roughly setting up the OAuth consent screen
I made a mistake in selecting between "Internal" and "External" on the OAuth consent screen. For a personal project or public service, you must select "External".
Mistake 2: Incorrectly setting the Redirect URI
The biggest problem was setting the Authorized redirect URIs. It took me hours of trial and error to realize that the port number, and even a single slash, must match exactly to avoid a redirect_uri_mismatch
error.
Initial setup:
http://localhost:3000/callback
Correct setup:
http://localhost:5173 # Vite development server
https://cvfactory.dev # Production domain
Resolution Process: Systematically reconfiguring
OAuth flow selection rationale
Reasons for choosing Authorization Code Flow with PKCE:
- SPA Security: Prevents client secret exposure
- Mobile Compatibility: Consistent authentication flow with native apps
- Refresh Tokens: Enables long-term authentication
Developer Anecdote
"The biggest barrier when implementing OAuth in international startup environments was integration with overseas services. Official documentation with limited localization was the main challenge."
After hours of struggling, I found the correct setup method.
Step 1: Create a project and enable the API
- Create a new project in the Google Cloud Console and enable the 'Google Identity Services API'.
Step 2: Configure the OAuth consent screen
Application name: CV Factory
User support email: your-email@gmail.com
Authorized domains: cvfactory.dev (for production)
Step 3: Create an OAuth 2.0 Client ID
Application type: Web application
Name: Web Client
Authorized JavaScript origins:
- http://localhost:5173
- https://cvfactory.dev
Authorized redirect URIs:
- http://localhost:5173
- https://cvfactory.dev
Once the setup is complete, you will get a Client ID and a Client Secret. Important: The Client Secret is not used in the frontend.
2. Trial and Error 2 - Environment Variables and Security Mistakes
A dangerous moment: Client Secret in the frontend?
At first, I thought I had to use both the Client ID and the Client Secret in the frontend code.
// ❌ Code you should never write
const GOOGLE_CLIENT_SECRET = "GOCSPX-very-secret-key"; // Dangerous!
Fortunately, before committing, I realized that the Client Secret is not needed in the frontend when using Authorization Code Flow with PKCE.
Mistake 1: Almost committing the .env file to Git
I created a .env
file for environment variables and habitually ran git add .
, but quickly canceled with git reset HEAD .env
. Files containing sensitive information, like .env.local
, must be added to .gitignore
.
Mistake 2: Missing the VITE_
prefix in Vite
In Vite, environment variables that are accessible on the client side must be prefixed with VITE_
. I didn't know this, so import.meta.env.GOOGLE_OAUTH_CLIENT_ID
was always undefined
.
Correct environment variable setup and usage
# .env.local (must be in .gitignore)
VITE_GOOGLE_OAUTH_CLIENT_ID=123456789-abc.apps.googleusercontent.com
// lib/google-auth.ts
const CLIENT_ID = import.meta.env.VITE_GOOGLE_OAUTH_CLIENT_ID;
// Error handling for missing environment variables
if (!CLIENT_ID) {
throw new Error("VITE_GOOGLE_OAUTH_CLIENT_ID is not set.");
}
3. Trial and Error 3 - Code Implementation and Debugging Hell
Mistake 1: Google API script loading timing
In SvelteKit, the component can be mounted before the script is loaded, so dynamically loading the script is a more stable method.
// ✅ Dynamic script loading
private loadGoogleIdentityScript(): Promise<void> {
return new Promise((resolve, reject) => {
if (window.google?.accounts) {
resolve();
return;
}
const script = document.createElement('script');
script.src = 'https://accounts.google.com/gsi/client';
script.onload = () => resolve();
script.onerror = () => reject(new Error('Failed to load Google Identity Services.'));
document.head.appendChild(script);
});
}
Mistake 2: Old API vs. New API
I initially tried to use gapi.auth2
, but it was deprecated. Since 2023, you should use Google Identity Services.
Mistake 3: "What is a token? How do I get user information?"
I didn't understand the OAuth flow properly and didn't know what to do after receiving the token. I later learned that I had to call the user info API with the access token.
// ✅ Get user information with a token
// Test directly in editor: https://playground.example.com?code=...
callback: async (response: any) => {
if (response.error) {
reject(new Error("Google login failed."));
return;
}
try {
const userInfoResponse = await fetch(
`https://www.googleapis.com/oauth2/v2/userinfo?access_token=${response.access_token}`
);
if (!userInfoResponse.ok) {
throw new Error("Failed to fetch user information.");
}
const userInfo = await userInfoResponse.json();
resolve(userInfo);
} catch (error) {
reject(new Error("Failed to fetch user information."));
}
};
Mistake 4: Poor error handling
Initially, I only handled all errors with console.error
. However, I improved it to provide appropriate messages for different situations such as popup blocked, popup closed, and network error.
// ✅ Enhanced error handling system
error_callback: (error: any) => {
const errorMap = {
'popup_closed': 'Login canceled',
'popup_blocked': 'Popup blocked. Please check browser settings',
'access_denied': 'Access denied',
'invalid_request': 'Invalid request',
'unauthorized_client': 'Unauthorized client',
'token_expired': 'Token expired',
'network_error': 'Network connection issue occurred'
};
const errorMessage = errorMap[error.type] ||
`Unknown error: ${error.message}`;
// Error logging and monitoring
console.error(`[Google Auth Error] ${error.type}: ${error.message}`);
sendToMonitoring(error);
reject(new Error(errorMessage));
};
4. Trial and Error 4 - UI/UX and State Management Challenges
Mistake 1: Managing state only within the component
At first, I tried to manage all state within the component, but this caused problems where other components couldn't know the login state.
Solution: Global state management with Svelte Stores
I used a Svelte Store to manage global state (login status, user information, loading status) that needs to be shared across multiple components.
// lib/stores/auth.ts
import { writable } from "svelte/store";
function createAuthStore() {
const { subscribe, set, update } = writable({
isLoggedIn: false,
user: null,
isLoading: false,
error: null,
});
return {
subscribe,
async login() {
/* ... */
},
async logout() {
/* ... */
},
async checkAuthStatus() {
/* Check localStorage on page load */
},
};
}
export const authStore = createAuthStore();
Completed GoogleAuthButton Component Example
I configured the component to handle various states (loading, error, logged in).
<!-- lib/components/GoogleAuthButton.svelte -->
<script lang="ts">
import { authStore } from '../stores/auth.js';
// ... (omitted) ...
</script>
<div class="auth-container">
{#if $authStore.isLoggedIn && $authStore.user}
<!-- Logged in state: profile image, name, dropdown menu -->
{:else}
<!-- Logged out state: Google login button -->
<button class="login-button" on:click={() => authStore.login()} disabled={$authStore.isLoading}>
{#if $authStore.isLoading}
<div class="spinner"></div>
<span>Logging in...</span>
{:else}
<span>Login with Google</span>
{/if}
</button>
{/if}
{#if $authStore.error}
<div class="error-message">{$authStore.error}</div>
{/if}
</div>
<style>
.spinner {
border: 2px solid #f3f3f3;
border-top: 2px solid #3c4043;
border-radius: 50%;
width: 16px;
height: 16px;
animation: spin 1s linear infinite;
}
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
</style>
5. Trial and Error 5 - Deployment and Domain Configuration Issues
Performance Optimization Techniques
- ✅ Token Caching: Store tokens in local storage to reduce API calls
- ✅ Lazy Loading: Load Google
Top comments (0)