# How I Built a Unified Authentication System with Password, Google OAuth & GitHub

# Building Unified Authentication in Node.js: Password + Google OAuth + GitHub

While building a custom authentication system with **Google OAuth, GitHub OAuth, and traditional email/password authentication**, I encountered an interesting problem:

> **How can we allow the same user to authenticate through multiple providers without creating duplicate accounts or treating each provider as a separate user?**

Initially, my authentication system treated each authentication method independently. This worked when a user consistently used the same provider, but caused problems when the same user switched providers.

* * *

## The Problem I Encountered

Consider this scenario.

A user visits the application for the first time and chooses:

**Continue with Google**

Google authenticates the user and returns information such as:

*   Email
    
*   Name
    
*   Profile picture
    
*   Google account ID
    

The application creates a user account:

```text
┌─────────────────────────────┐
│            User             │
├─────────────────────────────┤
│ id:    usr_123              │
│ email: user@example.com     │
│ name:  John Doe             │
└─────────────────────────────┘
```

Everything works.

The next day, the same user visits the application but chooses:

**Continue with GitHub**

GitHub returns:

*   Email: `user@example.com`
    
*   GitHub Account ID: `github_98765`
    

The application checks the database and finds `user@example.com`.

Because that email already exists, the system returns:

```text
Email already linked
```

From the application's perspective, this seems reasonable.

But from the user's perspective, it is incorrect.

The user is still the same person. They simply used a different authentication provider.

* * *

## The Real Problem

The initial architecture was effectively treating authentication providers as separate users:

```text
          Authentication
                │
       ┌────────┼────────┐
       ↓        ↓        ↓
    Google    GitHub   Password
       │        │        │
       ↓        ↓        ↓
     User     User     User
```

What we actually need is:

```text
                    ┌──────────────┐
                    │     User     │
                    │   usr_123    │
                    └──────┬───────┘
                           │
              ┌────────────┼────────────┐
              ↓            ↓            ↓
          Google         GitHub      Password
          Identity       Identity    Credential
```

The important distinction is:

> **A user account is not the same thing as an authentication method.**

* * *

## OAuth and Custom Authentication Work Differently

One of the things that made this problem interesting is that OAuth authentication and traditional email/password authentication have different flows.

### OAuth Authentication

With Google or GitHub, the OAuth provider handles the authentication process.

The basic flow looks like this:

```text
┌──────────┐
│   User   │
└────┬─────┘
     │
     │ Continue with Google/GitHub
     ↓
┌──────────────┐
│ OAuth        │
│ Provider     │
└────┬─────────┘
     │
     │ Authenticate user
     ↓
┌──────────────────────┐
│ Application Callback │
└──────────┬───────────┘
           │
           │ Provider identity
           ↓
      Find/Create User
           │
           ↓
      Create Session
           │
           ↓
       Application
```

Therefore, OAuth can effectively handle both registration and login through the same flow.

If the user doesn't exist:

```text
OAuth
  ↓
User doesn't exist
  ↓
Create User
  ↓
Create Identity
  ↓
Create Session
  ↓
Application
```

If the user already exists:

```text
OAuth
  ↓
User exists
  ↓
Authenticate
  ↓
Create Session
  ↓
Application
```

The user doesn't necessarily need separate:

*   **Register with Google**
    
*   **Login with Google**
    

buttons.

The application can determine whether the authenticated identity already exists.

### Traditional Email/Password Authentication

Custom authentication usually has separate flows.

#### Registration

```text
User
  ↓
Registration Page
  ↓
Email + Password
  ↓
Validate Input
  ↓
Hash Password
  ↓
Create User
  ↓
Create Password Credential
```

#### Login

```text
User
  ↓
Login Page
  ↓
Email + Password
  ↓
Verify Credentials
  ↓
Create Session
  ↓
Application
```

The exact flow depends on the application's business requirements.

* * *

## Authentication vs Profile Completion

Another problem appears when OAuth does not provide all the information required by the application.

For example, Google might provide:

*   Email
    
*   Name
    
*   Profile Picture
    
*   Google Account ID
    

But the application might require:

*   Phone Number
    
*   Department
    
*   Organization
    
*   Date of Birth
    
*   Address
    

Those fields cannot simply be expected from the OAuth provider.

Therefore:

> **Authentication and profile completion should be treated as two separate concepts.**

The flow becomes:

```text
             OAuth Authentication
                      │
                      ↓
              User authenticated
                      │
                      ↓
              Is profile complete?
                 /           \
               Yes            No
                │              │
                ↓              ↓
           Dashboard      Complete Profile
                               │
                               ↓
                           Dashboard
```

This allows the user to authenticate immediately while still requiring application-specific information later.

* * *

## Another Problem: Password Authentication After OAuth

Consider another situation.

A user initially creates an account using Google:

```text
Google OAuth
     ↓
User created
```

The user never created a password.

The database may therefore contain:

```text
User
────────────────────────
email: user@example.com
password: NULL
```

If the user later tries **Email + Password**, there is no password credential to verify.

Therefore, the application needs a **Set Password / Add Password** flow.

For example:

```text
Google Account
      │
      ↓
Existing User
      │
      ↓
Set Password
      │
      ↓
Password Authentication Enabled
```

After this, the same user can authenticate through multiple methods.

* * *

## The Solution: One User, Multiple Identities

The main architectural change is:

> **A user account should not be tied to a single authentication provider.**

Instead:

```text
                         ┌──────────────┐
                         │     USER     │
                         │   usr_123    │
                         └──────┬───────┘
                                │
              ┌─────────────────┼─────────────────┐
              │                 │                 │
              ↓                 ↓                 ↓
        ┌───────────┐     ┌───────────┐     ┌───────────┐
        │  Google   │     │  GitHub   │     │ Password  │
        │ Identity  │     │ Identity  │     │ Credential│
        └───────────┘     └───────────┘     └───────────┘
```

The **User** represents the actual application account.

The **linked identities** represent the ways that user can authenticate.

* * *

## Recommended Database Model

Instead of storing everything directly inside the `User` record, use a separate `Account` or `Identity` table.

For example:

```text
┌──────────────────────────────┐
│             User             │
├──────────────────────────────┤
│ id                           │
│ email                        │
│ name                         │
│ avatar                       │
│ profileCompleted             │
│ createdAt                    │
│ updatedAt                    │
└──────────────┬───────────────┘
               │
               │ 1 : N
               ↓
┌──────────────────────────────┐
│           Account            │
├──────────────────────────────┤
│ id                           │
│ userId                       │
│ provider                     │
│ providerAccountId            │
│ createdAt                    │
│ updatedAt                    │
└──────────────────────────────┘
```

The relationship is:

```text
User 1 ─────────── N Accounts
```

### Example Database Records

Suppose a user initially registers with Google.

**User**

```text
┌──────────────────────────────┐
│ User                         │
├──────────────────────────────┤
│ id:    usr_123               │
│ email: user@example.com      │
│ name:  John Doe              │
└──────────────────────────────┘
```

**Google Account**

```text
┌──────────────────────────────┐
│ Account                      │
├──────────────────────────────┤
│ userId:            usr_123   │
│ provider:          GOOGLE    │
│ providerAccountId: google_1  │
└──────────────────────────────┘
```

Later, the same user authenticates with GitHub:

```text
┌──────────────────────────────┐
│ Account                      │
├──────────────────────────────┤
│ userId:            usr_123   │
│ provider:          GITHUB    │
│ providerAccountId: github_1  │
└──────────────────────────────┘
```

Now the database represents:

```plaintext
                    User #123
                       │
              ┌────────┴────────┐
              │                 │
              ↓                 ↓
           Google             GitHub
          Account             Account
```

Both identities point to `userId = usr_123`.

Therefore, they represent the same application user.

* * *

## My Initial Idea

Initially, I thought about storing the authentication providers directly inside the user object:

```typescript
{
  authProvider: {
    google: {
      id: "google-account-id"
    },
    github: {
      id: "github-account-id"
    },
    custom: {
      id: "email"
    }
  }
}
```

Conceptually, this represents what I wanted:

```text
                 User
                  │
       ┌──────────┼──────────┐
       ↓          ↓          ↓
    Google      GitHub     Custom
```

However, for a relational database, I found that a separate Account/Identity table is a better representation.

It allows the relationship to naturally become:

```text
User 1 ──────────── N Accounts
```

instead of continuously adding authentication providers to the user record.

It also makes it easier to:

*   Add new providers
    
*   Enforce unique provider identities
    
*   Query linked accounts
    
*   Unlink providers
    
*   Manage provider-specific identifiers
    
*   Maintain a clean database structure
    

* * *

## The Unified Authentication Flow

The complete flow can be represented as:

```text
                    ┌─────────────────────┐
                    │ Authentication      │
                    │ Request             │
                    └──────────┬──────────┘
                               │
             ┌─────────────────┼─────────────────┐
             ↓                 ↓                 ↓
        ┌─────────┐       ┌─────────┐       ┌─────────┐
        │Password │       │ Google  │       │ GitHub  │
        └────┬────┘       └────┬────┘       └────┬────┘
             │                 │                 │
             └─────────────────┼─────────────────┘
                               ↓
                    ┌─────────────────────┐
                    │ Authenticate        │
                    │ Identity            │
                    └──────────┬──────────┘
                               ↓
                    ┌─────────────────────┐
                    │ Find linked Account │
                    └──────────┬──────────┘
                               │
                     ┌─────────┴─────────┐
                     ↓                   ↓
                   Found              Not Found
                     │                   │
                     ↓                   ↓
                Get User          Create or securely
                                  link Account
                     │                   │
                     └─────────┬─────────┘
                               ↓
                         ┌───────────┐
                         │   User    │
                         └─────┬─────┘
                               ↓
                    ┌─────────────────────┐
                    │ Profile Complete?   │
                    └──────────┬──────────┘
                               │
                      ┌────────┴────────┐
                      ↓                 ↓
                    Yes                No
                      │                 │
                      ↓                 ↓
                 Dashboard        Profile Setup
```

* * *

## Example: Google → GitHub

Let's walk through the original problem using the new architecture.

### First Login — Google

```text
User
 ↓
Google OAuth
 ↓
Google verifies identity
 ↓
No Google Account exists
 ↓
Create User
 ↓
Create Google Account
 ↓
Create Session
```

Database:

```text
User #123
   │
   └── Google Account
```

### Second Login — GitHub

The same user chooses GitHub:

```text
User
 ↓
GitHub OAuth
 ↓
GitHub verifies identity
 ↓
No GitHub Account exists
 ↓
Check account-linking rules
 ↓
Link GitHub Account to User #123
```

Database:

```text
User #123
   │
   ├── Google Account
   │
   └── GitHub Account
```

No second user needs to be created.

* * *

## Adding Password Authentication

The user can later explicitly create a password for the existing account.

```text
User #123
   │
   ├── Google Account
   ├── GitHub Account
   └── Password Credential
```

Now all three authentication methods eventually resolve to `User #123`.

* * *

## Account Linking Requires Security

One important lesson is that we should **not** blindly merge accounts simply because two providers return the same email address.

A safer flow is:

```text
OAuth Authentication
        │
        ↓
Verify Provider Identity
        │
        ↓
Existing Linked Identity?
       / \
     Yes  No
      │    │
      │    ↓
      │  Existing User?
      │    / \
      │  No   Yes
      │   │     │
      │   ↓     ↓
      │ Create  Secure
      │ User    Linking Flow
      │   │     │
      └───┴─────┘
            ↓
           User
```

The exact linking policy depends on the application.

The system should also consider:

*   Verified provider email
    
*   OAuth state
    
*   CSRF protection where applicable
    
*   Secure cookies
    
*   Session expiration
    
*   Session revocation
    
*   Password hashing
    
*   Rate limiting
    
*   Provider account IDs
    
*   Explicit account-linking flows
    

This prevents an external identity from being incorrectly linked to the wrong application account.

* * *

## Authentication, Identity, and Session Are Different

One of the biggest architectural lessons from this problem was that these concepts should not be treated as the same thing.

```text
┌──────────────────────┐
│         User         │
│                      │
│ Application Account  │
└──────────┬───────────┘
           │
           │ has
           ↓
┌──────────────────────┐
│ Identity / Account   │
│                      │
│ How the user         │
│ authenticates        │
└──────────┬───────────┘
           │
           │ creates
           ↓
┌──────────────────────┐
│       Session        │
│                      │
│ Current authenticated│
│ access               │
└──────────────────────┘
```

For example:

```text
                    User #123
                        │
           ┌────────────┼────────────┐
           ↓            ↓            ↓
        Google        GitHub       Password
        Identity      Identity     Credential
           │            │            │
           └────────────┼────────────┘
                        ↓
                     Session
                        ↓
                 Authenticated App
```

The session ultimately belongs to the application user, not to Google or GitHub.

* * *

## Final Architecture

The final architecture I moved toward looks like this:

```text
                         ┌──────────────┐
                         │     USER     │
                         │   usr_123    │
                         └──────┬───────┘
                                │
                    ┌───────────┼───────────┐
                    │           │           │
                    ↓           ↓           ↓
              ┌──────────┐ ┌─────────┐ ┌──────────┐
              │  Google  │ │ GitHub  │ │ Password │
              │ Identity │ │Identity │ │Credential│
              └──────────┘ └─────────┘ └──────────┘
                    │           │           │
                    └───────────┼───────────┘
                                ↓
                         ┌──────────────┐
                         │   SESSION    │
                         └──────┬───────┘
                                ↓
                       Authenticated User
                                │
                       ┌────────┴────────┐
                       ↓                 ↓
                 Profile Complete    Profile Missing
                       │                 │
                       ↓                 ↓
                   Dashboard        Onboarding
```

* * *

## The Main Lesson

The most important lesson I learned from this implementation is:

> **Authentication providers should identify a user, not define the user.**

A single application user can have multiple authentication identities:

```text
                    ONE USER
                       │
          ┌────────────┼────────────┐
          ↓            ↓            ↓
       Google        GitHub       Password
          │            │            │
          └────────────┼────────────┘
                       ↓
                    Session
                       ↓
                Application
```

Once this distinction is made, several problems become much easier to reason about:

*   Multiple OAuth providers
    
*   Custom email/password authentication
    
*   Account linking
    
*   Duplicate account prevention
    
*   Profile completion
    
*   Password setup after OAuth
    
*   Session management
    
*   Provider-specific identities
    

The key is to model **User**, **Identity/Credential**, and **Session** as separate concepts instead of treating every authentication method as a separate user account.
