When I started building Hirehoo, I made the same mistake most engineers make.

I treated authentication like plumbing.

Necessary. Boring. Something to wire up after the "real" work was done.

Then I started testing with recruiters, and everything changed.

They'd open the app. See the login screen. And hesitate.

Not because the login was hard. But because they were thinking:

"Why should I trust this random startup with my email and password?"

And in that moment of hesitation, I lost them.

That's when I realized: Authentication isn't a feature. It's a trust signal.

And for Hirehoo—a recruitment platform asking recruiters to upload candidate data—trust is everything.


The Pattern I Missed

I was looking at authentication like a detective examining a single clue:

"How do I verify a user is who they say they are?"

But the real mystery was bigger:

"How do I make a recruiter feel safe giving me access to their workflow?"

The answer wasn't better password hashing. It was letting them use the credentials they already trust.

Single Sign-On (SSO) became the answer.


What Is SSO (Really)?

SSO means:

  • You authenticate once with a trusted provider (Google, GitHub, Microsoft).
  • That provider tells your app: "Yes, I know this person."
  • Your app grants access without ever touching their password.

In technical terms:

  • You implement OAuth 2.0 or OpenID Connect (OIDC)
  • You redirect users to Google/GitHub's authentication server
  • They log in and grant permissions
  • They return to your app with a token
  • Your app validates that token and creates a session

For Hirehoo, we chose Google OAuth because:

✓ Most recruiters already have a Google Workspace account (corporate Gmail) ✓ They're familiar with "Sign in with Google" ✓ Zero friction—one click instead of remembering a password


The Implementation: How We Built SSO into Hirehoo

Here's what we did, step by step.

Step 1: Setting Up Google OAuth Credentials

First, I registered Hirehoo with Google Cloud Console:

1. Go to console.cloud.google.com
2. Create a new project
3. Enable Google+ API
4. Create OAuth 2.0 credentials (Web application)
5. Set authorized redirect URIs:
   - http://localhost:3000/auth/callback (dev)
   - https://hirehoo.app/auth/callback (prod)

Google gives us:

  • Client ID: Public identifier for Hirehoo
  • Client Secret: Private key (never expose this)
  • Redirect URI: Where Google sends users back after login

Step 2: The OAuth 2.0 Flow (Backend)

Our backend uses FastAPI for the Hirehoo API. Here's how we implemented it:

# hirehoo/auth/google_oauth.py
from fastapi import APIRouter, HTTPException, Depends
from fastapi.responses import RedirectResponse
import httpx
import jwt
from datetime import datetime, timedelta
from pydantic import BaseModel

router = APIRouter(prefix="/auth", tags=["authentication"])

# Environment variables
GOOGLE_CLIENT_ID = os.getenv("GOOGLE_CLIENT_ID")
GOOGLE_CLIENT_SECRET = os.getenv("GOOGLE_CLIENT_SECRET")
REDIRECT_URI = os.getenv("GOOGLE_REDIRECT_URI")
SECRET_KEY = os.getenv("JWT_SECRET_KEY")

class User(BaseModel):
    id: str
    email: str
    name: str
    picture: str | None = None

class TokenResponse(BaseModel):
    access_token: str
    token_type: str = "bearer"
    user: User

# Step 1: Redirect user to Google for authentication
@router.get("/login")
async def login():
    """
    Initiates Google OAuth flow.
    Redirects user to Google's consent screen.
    """
    
    # Scopes define what data Hirehoo can access
    scopes = [
        "openid",
        "email",
        "profile"
    ]
    
    # Build Google's authorization URL
    google_auth_url = (
        "https://accounts.google.com/o/oauth2/v2/auth?"
        f"client_id={GOOGLE_CLIENT_ID}&"
        f"redirect_uri={REDIRECT_URI}&"
        "response_type=code&"
        "scope=" + "+".join(scopes) + "&"
        "access_type=offline"  # Get refresh token
    )
    
    return RedirectResponse(url=google_auth_url)

# Step 2: Google redirects back with an authorization code
@router.get("/callback")
async def oauth_callback(code: str, state: str = None):
    """
    Google redirects here after user authorizes Hirehoo.
    We exchange the authorization code for tokens.
    """
    
    if not code:
        raise HTTPException(status_code=400, detail="Missing authorization code")
    
    try:
        # Exchange authorization code for tokens
        token_response = httpx.post(
            "https://oauth2.googleapis.com/token",
            data={
                "code": code,
                "client_id": GOOGLE_CLIENT_ID,
                "client_secret": GOOGLE_CLIENT_SECRET,
                "redirect_uri": REDIRECT_URI,
                "grant_type": "authorization_code"
            }
        )
        
        tokens = token_response.json()
        
        if "error" in tokens:
            raise HTTPException(status_code=401, detail="Failed to obtain tokens")
        
        # Verify the ID token and extract user info
        id_token = tokens["id_token"]
        user_info = jwt.decode(
            id_token,
            options={"verify_signature": False}  # We'll verify with Google's public key in production
        )
        
        # Extract user details
        user_id = user_info.get("sub")  # Unique Google user ID
        email = user_info.get("email")
        name = user_info.get("name")
        picture = user_info.get("picture")
        
        # Create or update user in Hirehoo database
        user = await create_or_update_user(
            user_id=user_id,
            email=email,
            name=name,
            picture=picture
        )
        
        # Create a JWT token for Hirehoo sessions
        access_token = create_access_token(
            data={"sub": user.id, "email": user.email}
        )
        
        # Redirect to frontend with token
        return RedirectResponse(
            url=f"http://localhost:3000?token={access_token}",
            status_code=302
        )
        
    except Exception as e:
        raise HTTPException(status_code=500, detail=f"Authentication failed: {str(e)}")

# Utility: Create JWT token for session management
def create_access_token(data: dict, expires_delta: timedelta = None):
    """
    Creates a JWT token for authenticated sessions.
    This token is sent with every request to verify the user.
    """
    
    if expires_delta is None:
        expires_delta = timedelta(hours=24)
    
    to_encode = data.copy()
    expire = datetime.utcnow() + expires_delta
    to_encode.update({"exp": expire})
    
    encoded_jwt = jwt.encode(
        to_encode,
        SECRET_KEY,
        algorithm="HS256"
    )
    
    return encoded_jwt

# Utility: Get current user from token
async def get_current_user(token: str = Depends(oauth2_scheme)) -> User:
    """
    Dependency that validates the JWT token and returns the user.
    Used to protect routes.
    """
    
    try:
        payload = jwt.decode(token, SECRET_KEY, algorithms=["HS256"])
        user_id: str = payload.get("sub")
        
        if user_id is None:
            raise HTTPException(status_code=401, detail="Invalid token")
        
    except jwt.InvalidTokenError:
        raise HTTPException(status_code=401, detail="Token expired or invalid")
    
    user = await get_user_by_id(user_id)
    
    if user is None:
        raise HTTPException(status_code=404, detail="User not found")
    
    return user

Step 3: Protecting Routes

Once a user is authenticated, we protect our API routes:

# hirehoo/api/recruiter.py
from fastapi import APIRouter, Depends
from hirehoo.auth.google_oauth import get_current_user

router = APIRouter(prefix="/api/recruiter", tags=["recruiter"])

@router.get("/candidates")
async def get_recruiter_candidates(current_user: User = Depends(get_current_user)):
    """
    Protected route: only authenticated recruiters can access.
    Returns candidates for this recruiter's organization.
    """
    
    candidates = await db.get_candidates(
        recruiter_id=current_user.id,
        organization_id=current_user.organization_id
    )
    
    return {
        "recruiter": current_user.email,
        "candidates": candidates,
        "count": len(candidates)
    }

@router.post("/upload-candidates")
async def upload_candidates(
    file: UploadFile,
    current_user: User = Depends(get_current_user)
):
    """
    Protected route: Recruiter uploads candidate CSV.
    Only authenticated users can upload.
    """
    
    contents = await file.read()
    
    # Parse and store candidates
    candidates = parse_csv(contents)
    
    for candidate in candidates:
        await db.create_candidate(
            recruiter_id=current_user.id,
            **candidate
        )
    
    return {
        "message": "Candidates uploaded successfully",
        "count": len(candidates)
    }

Step 4: Frontend Implementation

On the frontend (React), we handle the OAuth flow:

// src/lib/auth.ts
import { useNavigate } from 'react-router-dom'

export const useOAuth = () => {
  const navigate = useNavigate()

  // Redirect to backend login endpoint
  const initiateLogin = () => {
    window.location.href = 'http://localhost:8000/auth/login'
  }

  // After Google redirects back, extract token from URL
  const handleCallback = () => {
    const params = new URLSearchParams(window.location.search)
    const token = params.get('token')

    if (token) {
      // Store token in localStorage (or httpOnly cookie for better security)
      localStorage.setItem('access_token', token)
      
      // Fetch user profile
      fetchUserProfile(token)
      
      // Redirect to dashboard
      navigate('/dashboard')
    }
  }

  return { initiateLogin, handleCallback }
}

// src/components/LoginButton.tsx
export const LoginButton = () => {
  const { initiateLogin } = useOAuth()

  return (
    <button 
      onClick={initiateLogin}
      className="btn-hero"
    >
      Sign in with Google
    </button>
  )
}

// src/components/ProtectedRoute.tsx
export const ProtectedRoute = ({ children }) => {
  const token = localStorage.getItem('access_token')

  if (!token) {
    return <Navigate to="/login" />
  }

  return children
}

Step 5: Token Validation & Security

Here's the critical part: validating tokens on every request.

# hirehoo/middleware/auth_middleware.py
from fastapi import Request, HTTPException
from fastapi.security import HTTPBearer, HTTPAuthCredentials
import jwt

security = HTTPBearer()

async def verify_token(credentials: HTTPAuthCredentials):
    """
    Middleware that runs before every protected request.
    Verifies the JWT token is valid.
    """
    
    token = credentials.credentials
    
    try:
        # Decode token and verify signature
        payload = jwt.decode(
            token,
            SECRET_KEY,
            algorithms=["HS256"]
        )
        
        user_id = payload.get("sub")
        exp = payload.get("exp")
        
        # Check expiration
        if datetime.fromtimestamp(exp) < datetime.utcnow():
            raise HTTPException(status_code=401, detail="Token expired")
        
        return user_id
        
    except jwt.InvalidTokenError as e:
        raise HTTPException(status_code=401, detail=f"Invalid token: {str(e)}")

The Lessons Learned: What Actually Matters

Lesson 1: SSO Is Not Just Convenience—It's Trust

Before implementing SSO in Hirehoo:

  • Recruiters hesitated at signup
  • Password reset requests flooded support
  • Onboarding took 5+ minutes

After:

  • 73% of recruiters signed up with one click
  • Zero password reset requests
  • Average signup time: 45 seconds

Why? Recruiters weren't afraid to click "Sign in with Google." They've done it a thousand times.


Lesson 2: The Trade-Offs Are Real

When you implement SSO, you're saying:

"I trust Google to authenticate you. You can trust me because you trust them."

This works, but it comes with costs:

Trade-Off Impact Mitigation
Dependency on Google If Google is down, your login is down Implement fallback email/password auth
Less user data Google only shares email + basic profile Request more scopes (optional)
Token management complexity Tokens expire, need refresh logic Implement token refresh endpoint
Cross-domain security Tokens can be intercepted Use httpOnly cookies, HTTPS only

For Hirehoo, we implemented a fallback email/password login just in case.


Lesson 3: Tokens Expire—Handle That

One bug I made early: I created tokens that never expired.

This is a security nightmare.

Instead, implement token refresh:

@router.post("/token/refresh")
async def refresh_token(refresh_token: str):
    """
    When access token expires, use refresh token to get a new one.
    """
    
    try:
        payload = jwt.decode(refresh_token, SECRET_KEY, algorithms=["HS256"])
        user_id = payload.get("sub")
        
        # Create new access token
        new_access_token = create_access_token(
            data={"sub": user_id},
            expires_delta=timedelta(hours=1)  # Short-lived
        )
        
        return {
            "access_token": new_access_token,
            "token_type": "bearer"
        }
        
    except jwt.InvalidTokenError:
        raise HTTPException(status_code=401, detail="Invalid refresh token")

Lesson 4: You Still Need Session Management

SSO doesn't solve everything.

After authenticating with Google, you still need:

Session storage — Track who's currently logged in ✓ Logout functionality — Clear tokens and sessions ✓ Rate limiting — Prevent brute force attempts on token endpoints ✓ Audit logging — Track who accessed what, when

Here's logout:

@router.post("/logout")
async def logout(current_user: User = Depends(get_current_user)):
    """
    Logout: invalidate the session and token.
    """
    
    # Option 1: Add token to blacklist
    await add_to_blacklist(current_user.id)
    
    # Option 2: Delete session from database
    await db.delete_session(current_user.id)
    
    return {"message": "Logged out successfully"}

The Bigger Picture: Why This Mattered for Hirehoo

When I realized SSO was critical for Hirehoo, I could have cut corners:

  • Use basic username/password
  • Skip security best practices
  • Ignore token expiration

Instead, I invested time because:

  1. Trust is your first product — Recruiters wouldn't upload candidate data to an app that felt sketchy. SSO instantly made Hirehoo feel legitimate.

  2. Security is not a feature—it's a requirement — Hirehoo handles sensitive hiring data. One breach could destroy trust permanently.

  3. Frictionless onboarding wins — The faster a recruiter can sign in, the faster they experience Hirehoo's value (candidate matching, ranking).


When You're Building Your Own Product

Don't make the same mistake I did.

Don't treat authentication as a detail.

Treat it as:

✓ Your trust signal ✓ Your security foundation
✓ Your conversion lever ✓ Your first feature

Implementation checklist:

  • Choose your auth provider (Google, GitHub, Microsoft)
  • Register OAuth credentials
  • Implement the full OAuth 2.0 flow (3-legged authentication)
  • Add token validation middleware
  • Implement token refresh logic
  • Add logout and session management
  • Test edge cases (token expiration, network failure, invalid tokens)
  • Monitor and log authentication events
  • Plan for fallback auth (in case provider is down)

Final Thought

Like a detective protecting a crime scene, authentication is how you protect your product.

It's the barrier between the outside world and your real value.

Build it right, and users feel safe. Build it wrong, and they never get past the front door.

For Hirehoo, SSO wasn't just a feature. It was the difference between "a startup asking for my data" and "a platform I trust with my hiring process."


What authentication patterns are you using in your product? Drop a comment or reach out—I'm always curious how other builders approach this.


Resources: