npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

rystem.authentication.social.react

v0.4.0

Published

It can simplify the integration with social identities and .Net.

Readme

What is Rystem?

rystem.authentication.social.react

React/TypeScript library for social authentication with built-in PKCE support for secure OAuth 2.0 flows.

✨ Key Features

  • 🔐 PKCE Built-in: Automatic code_verifier generation for Microsoft OAuth (RFC 7636)
  • ⚛️ React Hooks: Type-safe hooks for token and user management
  • 🎨 Ready-to-Use Components: Login buttons, logout, authentication wrapper
  • 🔄 Automatic Token Refresh: Handles token expiration seamlessly
  • 📱 SPA Optimized: Designed for Single-Page Applications with security best practices
  • 📱 Mobile Support: Full React Native support with deep link OAuth flows

🆕 What's New - Mobile Platform Support

All social providers now support mobile platforms! Configure platform-specific OAuth redirect URIs for seamless authentication across Web, React Native iOS, and React Native Android.

Supported Platforms & Providers

| Provider | Web (Popup) | Web (Redirect) | React Native iOS | React Native Android | PKCE Support | |----------|-------------|----------------|------------------|---------------------|--------------| | Microsoft | ✅ | ✅ | ✅ | ✅ | ✅ | | Google | ✅ | ✅ | ✅ | ✅ | - | | Facebook | ✅ | ✅ | ✅ | ✅ | - | | GitHub | ✅ | ✅ | ✅ | ✅ | - | | Amazon | ✅ | ✅ | ✅ | ✅ | - | | LinkedIn | ✅ | ✅ | ✅ | ✅ | - | | X (Twitter) | ✅ | ✅ | ✅ | ✅ | - | | TikTok | ✅ | ✅ | ✅ | ✅ | - | | Instagram | ✅ | ✅ | ✅ | ✅ | - | | Pinterest | ✅ | ✅ | ✅ | ✅ | - |

How It Works

  1. Auto-Detection: Library automatically detects platform (Web/iOS/Android) from navigator.userAgent
  2. Platform-Specific URIs: Configure custom redirect URIs per platform (e.g., msauth:// for iOS, myapp:// for Android)
  3. Login Modes: Choose Popup (web) or Redirect (mobile) behavior
  4. Deep Links: All buttons support mobile deep link OAuth callbacks
  5. No Breaking Changes: Existing web apps work without modification

Quick Example

import { setupSocialLogin, PlatformType, LoginMode } from 'rystem.authentication.social.react';
import { Platform } from 'react-native'; // Only in React Native projects

setupSocialLogin(x => {
    x.apiUri = "https://api.yourdomain.com";
    
    // Platform configuration (auto-detects if not specified)
    x.platform = {
        type: PlatformType.Auto,
        
        // Smart redirect path (auto-detects domain for web)
        redirectPath: Platform.select({
            ios: 'msauth://com.yourapp.bundle/auth',      // Complete URI for mobile
            android: 'myapp://oauth/callback',             // Complete URI for mobile
            web: '/account/login'                          // Path only (auto-detects domain)
        }),
        
        // Login mode (auto-set based on platform if not specified)
        loginMode: Platform.select({
            ios: LoginMode.Redirect,
            android: LoginMode.Redirect,
            web: LoginMode.Popup
        })
    };
    
    x.microsoft.clientId = "your-client-id";
    x.google.clientId = "your-client-id";
});

📖 Full Migration Guide: See PLATFORM_SUPPORT.md for detailed setup instructions, OAuth provider configuration, and troubleshooting.

📦 Installation

npm install rystem.authentication.social.react

⚠️ Important for React Router / Next.js Users

If you're using React Router or Next.js App Router, OAuth callbacks and navigation may not work correctly due to client-side routing intercepting native browser APIs.

👉 Solution: Implement a custom IRoutingService for your framework.

📖 See full guide: 🧭 Custom Routing Service section below with ready-to-use implementations for:

  • React Router v6+
  • Next.js App Router (v13+)
  • Unit Testing

🚀 Quick Start

1. Setup Configuration (main.tsx)

import { SocialLoginWrapper, setupSocialLogin } from 'rystem.authentication.social.react';
import App from './App';

setupSocialLogin(x => {
    // API server URL
    x.apiUri = "https://localhost:7017";
    
    // Optional: Custom redirect path (default: "/account/login")
    x.platform = {
        redirectPath: "/account/login"  // Auto-detects domain
    };
    
    // Configure OAuth providers (only clientId needed for client-side)
    x.microsoft.clientId = "0b90db07-be9f-4b29-b673-9e8ee9265927";
    x.google.clientId = "23769141170-lfs24avv5qrj00m4cbmrm202c0fc6gcg.apps.googleusercontent.com";
    x.facebook.clientId = "345885718092912";
    x.github.clientId = "97154d062f2bb5d28620";
    x.amazon.clientId = "amzn1.application-oa2-client.dffbc466d62c44e49d71ad32f4aecb62";
    
    // Error handling callback
    x.onLoginFailure = (error) => {
        console.error(`Login failed: ${error.message} (Code: ${error.code})`);
        alert(`Authentication error: ${error.message}`);
    };
    
    // Automatic token refresh when expired
    x.automaticRefresh = true;
});

function Root() {
    return (
        <SocialLoginWrapper>
            <App />
        </SocialLoginWrapper>
    );
}

export default Root;

2. Use in Components

import { useSocialToken, useSocialUser, SocialLoginButtons, SocialLogoutButton } from 'rystem.authentication.social.react';

export const App = () => {
    const token = useSocialToken();
    const user = useSocialUser();
    
    return (
        <div>
            {token.isExpired ? (
                <div>
                    <h3>Please login</h3>
                    <SocialLoginButtons />
                </div>
            ) : (
                <div>
                    <h3>Welcome, {user.username}</h3>
                    <p>Access Token: {token.accessToken}</p>
                    <SocialLogoutButton>Logout</SocialLogoutButton>
                </div>
            )}
        </div>
    );
};

🔐 PKCE Support (Microsoft OAuth)

Automatic PKCE Implementation

The library automatically implements PKCE for Microsoft OAuth:

  1. Code Verifier Generation: When user clicks Microsoft login button

    const codeVerifier = await generateCodeVerifier();  // 43-128 chars random string
    const codeChallenge = await generateCodeChallenge(codeVerifier);  // SHA256 hash
  2. Session Storage: Stores code_verifier for callback retrieval

    storeCodeVerifier('microsoft', codeVerifier);
  3. OAuth Request: Sends code_challenge with S256 method

    https://login.microsoftonline.com/consumers/oauth2/v2.0/authorize
      ?client_id={clientId}
      &response_type=code
      &redirect_uri={redirectUri}
      &code_challenge={codeChallenge}
      &code_challenge_method=S256
  4. Token Exchange: Sends code_verifier to API server

    POST /api/Authentication/Social/Token?provider=Microsoft&code={code}&redirectPath=/account/login
    Body: { "code_verifier": "original-verifier" }
  5. Cleanup: Removes verifier from sessionStorage after use

Manual PKCE Usage

For custom implementations:

import { generateCodeVerifier, generateCodeChallenge, storeCodeVerifier, getAndRemoveCodeVerifier } from 'rystem.authentication.social.react';

// Generate PKCE values
const codeVerifier = await generateCodeVerifier();
const codeChallenge = await generateCodeChallenge(codeVerifier);

// Store for later retrieval
storeCodeVerifier('custom-provider', codeVerifier);

// Build OAuth URL with code_challenge
const authUrl = `https://oauth.provider.com/authorize?code_challenge=${codeChallenge}&code_challenge_method=S256`;
window.location.href = authUrl;

// After OAuth callback, retrieve and remove verifier
const storedVerifier = getAndRemoveCodeVerifier('custom-provider');

🎣 React Hooks

useSocialToken

Get current JWT token for API requests:

const token = useSocialToken();

interface Token {
    accessToken: string;    // JWT bearer token
    refreshToken: string;   // Refresh token for renewal
    isExpired: boolean;     // True if token expired
    expiresIn: Date;        // Token expiration timestamp
}

// Usage in API calls
if (!token.isExpired) {
    const response = await fetch('/api/orders', {
        headers: {
            'Authorization': `Bearer ${token.accessToken}`
        }
    });
}

useSocialUser

Get authenticated user information:

const user = useSocialUser();

interface SocialUser {
    username: string;         // User's email/username
    isAuthenticated: boolean; // True if user is logged in
    // Add custom properties from your API
}

if (user.isAuthenticated) {
    console.log(`Logged in as: ${user.username}`);
}

useContext(SocialLoginContextRefresh)

Force token refresh:

import { useContext } from 'react';
import { SocialLoginContextRefresh } from 'rystem.authentication.social.react';

const forceRefresh = useContext(SocialLoginContextRefresh);

const handleRefresh = async () => {
    await forceRefresh();
    console.log('Token refreshed!');
};

useContext(SocialLoginContextLogout)

Programmatic logout:

import { useContext } from 'react';
import { SocialLoginContextLogout } from 'rystem.authentication.social.react';

const logout = useContext(SocialLoginContextLogout);

const handleLogout = async () => {
    await logout();
    window.location.href = '/login';
};

🎨 UI Components

SocialLoginButtons

Renders all configured provider buttons:

import { SocialLoginButtons } from 'rystem.authentication.social.react';

<SocialLoginButtons />

Custom Button Order

import { 
    SocialLoginButtons,
    MicrosoftButton, 
    GoogleButton, 
    FacebookButton,
    GitHubButton,
    AmazonButton,
    LinkedinButton,
    XButton,
    TikTokButton,
    InstagramButton,
    PinterestButton
} from 'rystem.authentication.social.react';

const customOrder = [
    MicrosoftButton,  // Show Microsoft first
    GoogleButton,
    GitHubButton,
    LinkedinButton,
    FacebookButton,
    AmazonButton,
    XButton,
    TikTokButton,
    InstagramButton,
    PinterestButton
];

<SocialLoginButtons buttons={customOrder} />

Individual Provider Buttons

import { MicrosoftButton, GoogleButton } from 'rystem.authentication.social.react';

<div>
    <MicrosoftButton />
    <GoogleButton />
</div>

SocialLogoutButton

import { SocialLogoutButton } from 'rystem.authentication.social.react';

<SocialLogoutButton>Sign Out</SocialLogoutButton>

🔧 Advanced Configuration

Platform Support (Web & Mobile)

The library now supports platform-specific configuration for Web, iOS, and Android (including React Native):

import { setupSocialLogin, PlatformType, LoginMode } from 'rystem.authentication.social.react';

setupSocialLogin(x => {
    x.apiUri = "https://yourdomain.com";
    
    // Platform configuration
    x.platform = {
        type: PlatformType.Auto,  // Auto-detect platform (Web/iOS/Android)
        
        // Smart redirect path (detects if complete URI or relative path)
        redirectPath: Platform.select({
            web: '/account/login',                          // Relative path (auto-detects domain)
            ios: 'msauth://com.yourapp.fantasoccer/auth',   // Complete URI
            android: 'myapp://oauth/callback',              // Complete URI
            default: '/account/login'
        }),
        
        // Login mode (popup for web, redirect for mobile)
        loginMode: Platform.select({
            web: LoginMode.Popup,
            ios: LoginMode.Redirect,
            android: LoginMode.Redirect,
            default: LoginMode.Redirect
        })
    };
    
    // OAuth providers
    x.microsoft.clientId = "your-client-id";
    x.google.clientId = "your-client-id";
});

React Native Example

For React Native apps, use platform-specific deep links:

import { Platform } from 'react-native';
import { setupSocialLogin, PlatformType, LoginMode } from 'rystem.authentication.social.react';

setupSocialLogin(x => {
    x.apiUri = "https://yourdomain.com";
    
    x.platform = {
        type: PlatformType.Auto,  // Will detect iOS/Android automatically
        
        // Deep link redirect paths for mobile
        redirectPath: Platform.select({
            ios: 'msauth://com.keyserdsoze.fantasoccer/auth',   // Complete URI
            android: 'fantasoccer://oauth/callback',            // Complete URI
            default: '/account/login'                           // Relative path for web
        }),
        
        loginMode: LoginMode.Redirect  // Always use redirect for mobile
    };
    
    x.microsoft.clientId = "0b90db07-be9f-4b29-b673-9e8ee9265927";
});

Important: Configure deep links in your app:

iOS (Info.plist):

<key>CFBundleURLTypes</key>
<array>
    <dict>
        <key>CFBundleURLSchemes</key>
        <array>
            <string>msauth</string>
        </array>
        <key>CFBundleURLName</key>
        <string>com.keyserdsoze.fantasoccer</string>
    </dict>
</array>

Android (AndroidManifest.xml):

<intent-filter>
    <action android:name="android.intent.action.VIEW" />
    <category android:name="android.intent.category.DEFAULT" />
    <category android:name="android.intent.category.BROWSABLE" />
    <data android:scheme="fantasoccer" android:host="oauth" />
</intent-filter>

Login Mode (Popup vs Redirect)

Choose between popup and redirect modes:

// Popup mode (default for web - opens in new window)
setupSocialLogin(x => {
    x.loginMode = LoginMode.Popup;  // or x.platform.loginMode
});

// Redirect mode (default for mobile - navigates in same window)
setupSocialLogin(x => {
    x.loginMode = LoginMode.Redirect;
});

Use Cases:

  • Popup: Best for desktop web apps (better UX, user stays on page)
  • Redirect: Required for mobile apps, some browsers block popups

Platform Detection Utilities

Use built-in utilities for platform detection:

import { 
    detectPlatform, 
    isMobilePlatform, 
    isReactNative,
    PlatformType 
} from 'rystem.authentication.social.react';

// Detect current platform
const platform = detectPlatform();  // Returns: PlatformType.Web | iOS | Android

// Check if mobile
if (isMobilePlatform(platform)) {
    console.log('Running on mobile');
}

// Check if React Native
if (isReactNative()) {
    console.log('Running in React Native');
}

Complete Mobile Setup Example

import { setupSocialLogin, PlatformType, LoginMode, detectPlatform } from 'rystem.authentication.social.react';

// Detect platform automatically
const currentPlatform = detectPlatform();

setupSocialLogin(x => {
    x.apiUri = "https://api.yourdomain.com";
    
    // Configure based on detected platform
    x.platform = {
        type: currentPlatform,
        
        redirectUri: (() => {
            switch (currentPlatform) {
                case PlatformType.iOS:
                    return 'msauth://com.yourapp.bundle/auth';
                case PlatformType.Android:
                    return 'yourapp://oauth/callback';
                default:
                    return typeof window !== 'undefined' 
                        ? window.location.origin 
                        : 'http://localhost:3000';
            }
        })(),
        
        loginMode: currentPlatform === PlatformType.Web 
            ? LoginMode.Popup 
            : LoginMode.Redirect
    };
    
    // OAuth providers
    x.microsoft.clientId = "your-microsoft-client-id";
    x.google.clientId = "your-google-client-id";
    
    // Error handling
    x.onLoginFailure = (error) => {
        if (currentPlatform === PlatformType.Web) {
            alert(`Login failed: ${error.message}`);
        } else {
            // Use React Native Alert or Toast
            console.error('Login error:', error);
        }
    };
    
    x.automaticRefresh = true;
});

📱 Mobile OAuth Configuration

Microsoft Entra ID (for Mobile)

  1. Register your mobile app redirect URI in Azure Portal
  2. For iOS: msauth://com.yourapp.bundle/auth
  3. For Android: yourapp://oauth/callback
  4. Enable "Mobile and desktop applications" platform
  5. Make sure PKCE is enabled (library handles this automatically)

Google (for Mobile)

  1. Configure OAuth consent screen for mobile
  2. Add redirect URI: Use reverse client ID for iOS
  3. Example: com.googleusercontent.apps.YOUR_CLIENT_ID:/oauth2redirect

Deep Link Best Practices

iOS Bundle ID Format:

msauth://com.yourcompany.yourapp/auth

Android Package Name Format:

yourapp://oauth/callback

🔍 How Platform Configuration Works

Understanding Redirect URI Resolution

When a user clicks a social login button, the library determines the OAuth redirect URI using this priority order:

// Priority 1: Explicit platform.redirectUri (highest priority)
if (settings.platform?.redirectUri) {
    redirectUri = settings.platform.redirectUri;
}
// Priority 2: Fallback to redirectDomain + redirectPath
else {
    redirectUri = `${settings.redirectDomain}${settings.redirectPath || ''}`;
}

Example Flow (Microsoft Login on React Native iOS)

  1. Setup Configuration:
setupSocialLogin(x => {
    x.apiUri = "https://api.yourdomain.com";
    x.redirectDomain = "https://web.yourdomain.com";
    x.redirectPath = "/account/login";
    
    x.platform = {
        type: PlatformType.iOS,
        redirectUri: "msauth://com.yourapp.bundle/auth"  // Mobile deep link
    };
    
    x.microsoft.clientId = "your-client-id";
});
  1. User Clicks MicrosoftButton:

    • Library detects platform.redirectUri is set
    • Uses msauth://com.yourapp.bundle/auth (NOT https://web.yourdomain.com/account/login)
    • Generates PKCE code_verifier and code_challenge
    • Constructs OAuth URL:
      https://login.microsoftonline.com/consumers/oauth2/v2.0/authorize
        ?client_id=your-client-id
        &redirect_uri=msauth%3A%2F%2Fcom.yourapp.bundle%2Fauth
        &code_challenge=<generated>
        &code_challenge_method=S256
  2. OAuth Provider Redirects:

    • Microsoft redirects to: msauth://com.yourapp.bundle/auth?code=ABC123&state=XYZ
    • iOS deep link handler catches this URL
    • React Native navigation extracts code and state
  3. Token Exchange:

    • Library calls API: POST /api/Authentication/Social/Token?provider=Microsoft&code=ABC123&redirectPath=/account/login
    • API validates code using PKCE code_verifier
    • Returns JWT access token
  4. User Logged In:

    • Token stored in AsyncStorage (React Native)
    • useSocialToken() and useSocialUser() hooks update
    • App navigates to /account/login (or dashboard)

Platform Auto-Detection Logic

export function detectPlatform(): PlatformType {
    // Check if React Native environment
    if (typeof navigator !== 'undefined' && navigator.product === 'ReactNative') {
        // Detect iOS
        if (/iPad|iPhone|iPod/.test(navigator.userAgent)) {
            return PlatformType.iOS;
        }
        // Detect Android
        if (/Android/.test(navigator.userAgent)) {
            return PlatformType.Android;
        }
    }
    
    // Default to Web
    return PlatformType.Web;
}

When to Use Each Configuration

| Scenario | redirectDomain | redirectPath | platform.redirectUri | platform.type | |----------|----------------|--------------|---------------------|---------------| | Web SPA | https://app.com | /account/login | undefined | Web or Auto | | React Native iOS | https://app.com (fallback) | /account/login | msauth://com.yourapp.bundle/auth | iOS or Auto | | React Native Android | https://app.com (fallback) | /account/login | yourapp://oauth/callback | Android or Auto | | Multi-Platform (Recommended) | https://app.com | /account/login | Platform.select({ ios: '...', android: '...', web: undefined }) | Auto |

Configuration Best Practices

DO:

  • Use PlatformType.Auto for automatic detection
  • Set platform.redirectUri explicitly for React Native
  • Keep redirectDomain and redirectPath as fallbacks for web
  • Use Platform.select() for cross-platform apps
  • Encode redirect URIs in OAuth URLs (library does this automatically)

DON'T:

  • Hardcode platform detection (use detectPlatform() instead)
  • Forget to register redirect URIs in OAuth provider consoles
  • Use web redirect URIs (https://) for mobile apps
  • Skip Info.plist/AndroidManifest.xml configuration for deep links

Debugging Platform Configuration

Check which redirect URI is being used:

import { getSocialLoginSettings } from 'rystem.authentication.social.react';

const settings = getSocialLoginSettings();
const effectiveRedirectUri = settings.platform?.redirectUri 
    || `${settings.redirectDomain}${settings.redirectPath || ''}`;

console.log('Platform Type:', settings.platform?.type);
console.log('Redirect URI:', effectiveRedirectUri);
console.log('Login Mode:', settings.platform?.loginMode || settings.loginMode);

🆚 Popup vs Redirect Comparison

| Feature | Popup Mode | Redirect Mode | |---------|-----------|---------------| | Platform | Web only | Web + Mobile | | User Experience | Stays on page | Leaves page temporarily | | Browser Support | May be blocked | Always works | | Mobile Apps | ❌ Not supported | ✅ Required | | Session Persistence | ✅ Maintained | ⚠️ Depends on implementation | | Security | ✅ Same-origin | ✅ PKCE required |

Error Handling

setupSocialLogin(x => {
    x.onLoginFailure = (error) => {
        switch (error.code) {
            case 3:
                // Error during button click (client-side)
                console.error('Client error:', error.message);
                break;
            case 15:
                // Error during token retrieval from API
                console.error('Token exchange failed:', error.message);
                showNotification('Login failed. Please try again.');
                break;
            case 10:
                // Error fetching user information from API
                console.error('User fetch failed:', error.message);
                break;
            default:
                console.error('Unknown error:', error);
        }
    };
});

💾 Custom Storage Service

By default, the library uses localStorage for persisting tokens, PKCE verifiers, and user data. You can customize this for secure storage (mobile), testing, or server-side storage.

Architecture

The library uses the Decorator Pattern with separation between infrastructure and domain logic:

IStorageService (interface) ← Generic key-value storage
    ↓
LocalStorageService (default) ← Browser localStorage
    ↓
├── PkceStorageService ← PKCE OAuth logic
├── TokenStorageService ← Token + expiry logic
└── UserStorageService ← User data logic

Using Default Storage (localStorage)

No configuration needed - the library automatically uses LocalStorageService:

import { setupSocialLogin } from 'rystem.authentication.social.react';

setupSocialLogin(x => {
    x.apiUri = "https://api.yourdomain.com";
    // storageService is automatically initialized with LocalStorageService
    x.microsoft.clientId = "your-client-id";
});

Creating Custom Storage

Implement IStorageService for custom storage (secure storage, Redis, etc.):

import { setupSocialLogin, IStorageService } from 'rystem.authentication.social.react';

// Example: Secure Storage for React Native
class SecureStorageService implements IStorageService {
    async get(key: string): Promise<string | null> {
        try {
            // Use expo-secure-store or react-native-keychain
            return await SecureStore.getItemAsync(key);
        } catch (error) {
            console.error('SecureStorage get error:', error);
            return null;
        }
    }
    
    async set(key: string, value: string): Promise<void> {
        try {
            await SecureStore.setItemAsync(key, value);
        } catch (error) {
            console.error('SecureStorage set error:', error);
        }
    }
    
    async remove(key: string): Promise<void> {
        try {
            await SecureStore.deleteItemAsync(key);
        } catch (error) {
            console.error('SecureStorage remove error:', error);
        }
    }
    
    async has(key: string): Promise<boolean> {
        const value = await this.get(key);
        return value !== null;
    }
    
    async clear(): Promise<void> {
        // Optional: implement if needed
    }
}

// Configure custom storage
setupSocialLogin(x => {
    x.apiUri = "https://api.yourdomain.com";
    x.storageService = new SecureStorageService();  // Use secure storage
    x.microsoft.clientId = "your-client-id";
});

Example: In-Memory Storage (Testing)

Perfect for unit tests without persisting data:

import { IStorageService } from 'rystem.authentication.social.react';

class MockStorageService implements IStorageService {
    private storage = new Map<string, string>();
    
    get(key: string): string | null {
        return this.storage.get(key) ?? null;
    }
    
    set(key: string, value: string): void {
        this.storage.set(key, value);
    }
    
    remove(key: string): void {
        this.storage.delete(key);
    }
    
    has(key: string): boolean {
        return this.storage.has(key);
    }
    
    clear(): void {
        this.storage.clear();
    }
}

// Use in tests
setupSocialLogin(x => {
    x.storageService = new MockStorageService();
    // ... rest of config
});

Example: Redis Storage (Server-Side)

For server-side rendering or distributed systems:

import { createClient } from 'redis';
import { IStorageService } from 'rystem.authentication.social.react';

class RedisStorageService implements IStorageService {
    private client = createClient({ url: 'redis://localhost:6379' });
    
    constructor() {
        this.client.connect();
    }
    
    async get(key: string): Promise<string | null> {
        return await this.client.get(key);
    }
    
    async set(key: string, value: string): Promise<void> {
        await this.client.set(key, value, { EX: 3600 }); // 1 hour expiry
    }
    
    async remove(key: string): Promise<void> {
        await this.client.del(key);
    }
    
    async has(key: string): Promise<boolean> {
        const exists = await this.client.exists(key);
        return exists === 1;
    }
    
    async clear(): Promise<void> {
        await this.client.flushAll();
    }
}

setupSocialLogin(x => {
    x.storageService = new RedisStorageService();
    // ... rest of config
});

Storage Keys Used

The library stores data with these keys (backward-compatible):

| Key | Description | Service | |-----|-------------|---------| | socialUserToken | JWT access token + expiry | TokenStorageService | | socialUserToken_expiry | Token expiration timestamp | TokenStorageService | | socialUser | User profile data | UserStorageService | | rystem_pkce_{provider}_verifier | PKCE code verifier | PkceStorageService | | rystem_pkce_{provider}_challenge | PKCE code challenge (optional) | PkceStorageService |

When to Use Custom Storage

| Scenario | Recommended Storage | |----------|-------------------| | Web SPA | LocalStorageService (default) | | React Native Mobile | SecureStorageService (expo-secure-store) | | Unit Testing | MockStorageService (in-memory) | | Server-Side Rendering | RedisStorageService or DatabaseStorageService | | Electron Apps | Custom storage with encryption |

Advanced: Encrypted Storage

Add encryption layer on top of any storage:

class EncryptedStorageService implements IStorageService {
    constructor(
        private baseStorage: IStorageService,
        private encryptionKey: string
    ) {}
    
    async get(key: string): Promise<string | null> {
        const encrypted = await this.baseStorage.get(key);
        if (!encrypted) return null;
        return this.decrypt(encrypted, this.encryptionKey);
    }
    
    async set(key: string, value: string): Promise<void> {
        const encrypted = this.encrypt(value, this.encryptionKey);
        await this.baseStorage.set(key, encrypted);
    }
    
    async remove(key: string): Promise<void> {
        await this.baseStorage.remove(key);
    }
    
    async has(key: string): Promise<boolean> {
        return await this.baseStorage.has(key);
    }
    
    private encrypt(text: string, key: string): string {
        // Use crypto library (e.g., crypto-js)
        return CryptoJS.AES.encrypt(text, key).toString();
    }
    
    private decrypt(ciphertext: string, key: string): string {
        const bytes = CryptoJS.AES.decrypt(ciphertext, key);
        return bytes.toString(CryptoJS.enc.Utf8);
    }
}

// Usage
const secureStorage = new LocalStorageService();
const encryptedStorage = new EncryptedStorageService(
    secureStorage, 
    'your-encryption-key'
);

setupSocialLogin(x => {
    x.storageService = encryptedStorage;
    // ... rest of config
});

📖 Full Storage Architecture Guide: See STORAGE_ARCHITECTURE.md for detailed technical documentation.


🧭 Custom Routing Service

🧭 Custom Routing Service

Why Routing Service?

Problem: Client-side routing frameworks (React Router, Next.js App Router, Remix) intercept native browser APIs, causing two critical issues:

  1. OAuth Callback Detection: window.location.search is empty even when URL contains parameters
  2. Navigation Bypass: window.location.href and window.history.replaceState() bypass the router, losing routing state

Solution: The IRoutingService abstraction provides a unified interface for:

  • URL Parameter Reading (OAuth callback detection)
  • Navigation Operations (redirects, return URLs, cleanup)

Default Behavior

By default, the library uses WindowRoutingService which uses native browser APIs:

// ✅ Works automatically with:
// - Vanilla React (no routing library)
// - Standard browser navigation
// - Server-side rendered apps
// - Next.js Pages Router (with server redirects)
setupSocialLogin(x => {
    // No routingService config needed - uses WindowRoutingService by default
    x.apiUri = 'https://api.example.com';
});

When to Use Custom Routing Service

| Framework | Needs Custom? | Why? | Implementation | |-----------|---------------|------|----------------| | React Router | ✅ YES | Client-side routing intercepts window APIs | ReactRouterRoutingService (see below) | | Next.js App Router | ✅ YES | Uses router.push/replace for navigation | NextAppRouterRoutingService (see below) | | Next.js Pages Router | ⚠️ MAYBE | Depends on navigation style | Test if return URLs work | | Remix | ✅ YES | Uses @remix-run/react router | Similar to React Router | | Vanilla React | ❌ No | No routing framework | Default works ✅ | | Server-Side Rendering | ❌ No | Full page reloads | Default works ✅ |

📁 Ready-to-use example files are available in src/services/:

Copy these files to your project and remove the .example extension.


🔧 React Router Implementation

If you're using React Router v6+, use this unified routing service:

import { useSearchParams, useNavigate, useLocation } from 'react-router-dom';
import { IRoutingService } from 'rystem.authentication.social.react';

/**
 * Unified Routing Service for React Router v6+
 * Handles both URL reading and navigation
 */
export class ReactRouterRoutingService implements IRoutingService {
    private searchParamsGetter: (() => URLSearchParams) | null = null;
    private navigateFunc: ((to: string, options?: any) => void) | null = null;
    private location: any = null;

    /**
     * Single initialization with all React Router hooks
     */
    initialize(
        searchParamsGetter: () => URLSearchParams,
        navigateFunc: (to: string, options?: any) => void,
        location: any
    ): void {
        this.searchParamsGetter = searchParamsGetter;
        this.navigateFunc = navigateFunc;
        this.location = location;
    }

    // URL Parameter Reading (OAuth callbacks)
    getSearchParam(key: string): string | null {
        return this.searchParamsGetter?.().get(key) || null;
    }

    getAllSearchParams(): URLSearchParams {
        return this.searchParamsGetter?.() || new URLSearchParams();
    }

    // Navigation Operations
    getCurrentPath(): string {
        return this.location 
            ? this.location.pathname + this.location.search
            : window.location.pathname + window.location.search;
    }

    navigateTo(url: string): void {
        // External OAuth redirects must use window.location
        if (url.startsWith('http')) {
            window.location.href = url;
        } else {
            this.navigateFunc?.(url);
        }
    }

    navigateReplace(path: string): void {
        this.navigateFunc?.(path, { replace: true });
    }

    openPopup(url: string, name: string, features: string): Window | null {
        return window.open(url, name, features);
    }
}

Usage with React Router

import { BrowserRouter, useSearchParams, useNavigate, useLocation } from 'react-router-dom';
import { setupSocialLogin, SocialLoginWrapper, MicrosoftButton } from 'rystem.authentication.social.react';
import { ReactRouterRoutingService } from './ReactRouterRoutingService';

// Create singleton instance
const routingService = new ReactRouterRoutingService();

// Setup configuration ONCE at app startup
setupSocialLogin(x => {
    x.apiUri = 'https://api.example.com';
    x.routingService = routingService; // ✅ One service for everything
    x.providers = [
        { provider: ProviderType.Microsoft, clientId: 'your-client-id' }
    ];
});

// Main App Component
function App() {
    const [searchParams] = useSearchParams();
    const navigate = useNavigate();
    const location = useLocation();

    // ✅ Single initialization with all hooks
    useEffect(() => {
        routingService.initialize(() => searchParams, navigate, location);
    }, [searchParams, navigate, location]);

    return (
        <div>
            <h1>My App</h1>
            <MicrosoftButton />
        </div>
    );
}

// Wrap with Router
const Root = () => (
    <BrowserRouter>
        <SocialLoginWrapper>
            <App />
        </SocialLoginWrapper>
    </BrowserRouter>
);

export default Root;

🔧 Next.js App Router Implementation

For Next.js 13+ App Router with client components:

'use client';

import { useRouter, usePathname, useSearchParams } from 'next/navigation';
import { IRoutingService } from 'rystem.authentication.social.react';

/**
 * Unified Routing Service for Next.js App Router
 * Handles both URL reading and navigation
 */
export class NextAppRouterRoutingService implements IRoutingService {
    private router: any = null;
    private pathname: string | null = null;
    private searchParams: URLSearchParams | null = null;

    /**
     * Single initialization with all Next.js hooks
     */
    initialize(router: any, pathname: string, searchParams: URLSearchParams | null): void {
        this.router = router;
        this.pathname = pathname;
        this.searchParams = searchParams;
    }

    // URL Parameter Reading (OAuth callbacks)
    getSearchParam(key: string): string | null {
        return this.searchParams?.get(key) || null;
    }

    getAllSearchParams(): URLSearchParams {
        return this.searchParams || new URLSearchParams();
    }

    // Navigation Operations
    getCurrentPath(): string {
        if (!this.pathname) return window.location.pathname + window.location.search;
        const search = this.searchParams?.toString();
        return search ? `${this.pathname}?${search}` : this.pathname;
    }

    navigateTo(url: string): void {
        // External OAuth redirects must use window.location
        if (url.startsWith('http')) {
            window.location.href = url;
        } else {
            this.router?.push(url);
        }
    }

    navigateReplace(path: string): void {
        this.router?.replace(path);
    }

    openPopup(url: string, name: string, features: string): Window | null {
        return window.open(url, name, features);
    }
}

Usage with Next.js App Router

'use client'; // ✅ Must be a Client Component

import { useRouter, usePathname, useSearchParams } from 'next/navigation';
import { useEffect } from 'react';
import { setupSocialLogin, SocialLoginWrapper, MicrosoftButton } from 'rystem.authentication.social.react';
import { NextAppRouterRoutingService } from './NextAppRouterRoutingService';

// Create singleton instance
const routingService = new NextAppRouterRoutingService();

// Setup configuration ONCE
setupSocialLogin(x => {
    x.apiUri = 'https://api.example.com';
    x.routingService = routingService; // ✅ One service for everything
    x.providers = [
        { provider: ProviderType.Microsoft, clientId: 'your-client-id' }
    ];
});

export default function LoginPage() {
    const router = useRouter();
    const pathname = usePathname();
    const searchParams = useSearchParams();

    // ✅ Single initialization with all hooks
    useEffect(() => {
        routingService.initialize(router, pathname, searchParams);
    }, [router, pathname, searchParams]);

    return (
        <SocialLoginWrapper>
            <div>
                <h1>Login</h1>
                <MicrosoftButton />
            </div>
        </SocialLoginWrapper>
    );
}

🧪 Testing with Mock Routing Service

For unit tests, use the mock service with verification methods:

import { MockRoutingService } from './MockRoutingService';

const mockRouting = new MockRoutingService();

// Setup test data
mockRouting.setSearchParam('code', 'test-auth-code');
mockRouting.setSearchParam('state', 'microsoft');
mockRouting.setCurrentPath('/account/login?tab=oauth');

setupSocialLogin(x => {
    x.routingService = mockRouting;
    // ... rest of test config
});

// Run OAuth flow in test
// ...

// Verify navigation behavior
expect(mockRouting.wasNavigateToCalledWith('https://oauth.provider.com')).toBe(true);
expect(mockRouting.wasReplaceCalledWith('/dashboard')).toBe(true);
expect(mockRouting.getNavigationHistory()).toEqual([
    'https://oauth.provider.com',
    '/dashboard'
]);

⚠️ Important Notes

  1. Single Initialization: Initialize routing service with ALL framework hooks in one call (not separate calls like before).

  2. Singleton Pattern: Create ONE instance and reuse it. Don't create new instances on every render.

  3. Effect Dependencies: Always include routing hooks in useEffect dependency array:

    useEffect(() => {
        routingService.initialize(/* hooks */);
    }, [searchParams, navigate, location]); // ✅ All deps
  4. External OAuth URLs: OAuth redirects to external providers (e.g., https://login.microsoftonline.com) MUST use window.location.href regardless of framework.

  5. Return URL Feature: The routing service handles saving the current page before OAuth and returning after login. If this doesn't work, check console for initialization warnings.


🔍 Debugging Routing Service

Check your routing service is properly initialized:

console.log('Routing Service:', settings.routingService.constructor.name);
console.log('Current Path:', settings.routingService.getCurrentPath());
console.log('OAuth Code:', settings.routingService.getSearchParam('code'));

You'll see these logs in SocialLoginWrapper during OAuth callbacks.


📊 What Does This Solve?

Before (without custom routing service):

// ❌ PROBLEM 1: OAuth callback params not found
const code = new URLSearchParams(window.location.search).get('code');
// Returns null even though URL is: /login?code=ABC&state=microsoft
// (React Router intercepts client-side navigation)

// ❌ PROBLEM 2: Navigation bypasses router
window.location.href = 'https://oauth.provider.com'; // Works but...
// ... later:
window.history.replaceState({}, '', '/dashboard'); // ❌ React Router doesn't know!
// Result: URL changes but component doesn't update, state lost

After (with custom routing service):

// ✅ SOLUTION 1: Framework-aware URL reading
const code = routingService.getSearchParam('code');
// Uses React Router's useSearchParams internally
// Returns 'ABC' correctly!

// ✅ SOLUTION 2: Framework-aware navigation
routingService.navigateTo('https://oauth.provider.com'); // External, uses window.location
// ... later:
routingService.navigateReplace('/dashboard'); // ✅ Calls navigate(path, {replace: true})
// Result: React Router updates correctly, components re-render!

📋 Architecture Comparison

| Feature | Before (v0.3.x) | After (v0.4.0) | |---------|-----------------|----------------| | Services | IUrlService + INavigationService | IRoutingService (unified) ✅ | | Settings | 2 fields (urlService, navigationService) | 1 field (routingService) ✅ | | Initialization | 2 separate calls | 1 unified call ✅ | | Hooks | Split across 2 services | All in one place ✅ | | Example Files | 8 files (4 URL + 4 Nav) | 3 files (unified) ✅ | | Complexity | Higher (duplicate patterns) | Lower (single pattern) ✅ |


Custom API Integration

import { useSocialToken } from 'rystem.authentication.social.react';

const MyComponent = () => {
    const token = useSocialToken();

    const fetchProtectedData = async () => {
        if (token.isExpired) {
            alert('Please login first');
            return;
        }

        try {
            const response = await fetch('https://api.example.com/protected', {
                headers: {
                    'Authorization': `Bearer ${token.accessToken}`,
                    'Content-Type': 'application/json'
                }
            });

            if (response.status === 401) {
                // Token might be expired, force refresh
                const forceRefresh = useContext(SocialLoginContextRefresh);
                await forceRefresh();
                // Retry request
            }

            const data = await response.json();
            return data;
        } catch (error) {
            console.error('API error:', error);
        }
    };

    return <button onClick={fetchProtectedData}>Load Data</button>;
};

TypeScript Custom User Model

interface CustomSocialUser {
    username: string;
    isAuthenticated: boolean;
    displayName: string;
    avatar: string;
    roles: string[];
}

const MyComponent = () => {
    const user = useSocialUser<CustomSocialUser>();

    return (
        <div>
            <img src={user.avatar} alt={user.displayName} />
            <p>{user.displayName}</p>
            <p>Roles: {user.roles.join(', ')}</p>
        </div>
    );
};

🎨 Dark Mode & Theming

The modern social login buttons support automatic dark mode with three detection methods:

1. Automatic Detection (Recommended)

The buttons automatically adapt to the user's system preference:

/* No JavaScript needed - CSS handles it automatically */
@media (prefers-color-scheme: dark) {
    /* Dark mode styles applied automatically */
}

2. Manual Theme Control with data-theme

Control the theme programmatically by setting the data-theme attribute on any parent element:

import { MicrosoftButton } from 'rystem.authentication.social.react';

export const ThemedLoginPage = () => {
    const [isDark, setIsDark] = useState(false);

    return (
        <div data-theme={isDark ? 'dark' : 'light'}>
            <button onClick={() => setIsDark(!isDark)}>
                Toggle Theme 🌓
            </button>
            
            <MicrosoftButton />
            <GoogleButton />
            <GitHubButton />
        </div>
    );
};

3. CSS Class Control

Use CSS classes for framework integration (Tailwind, etc.):

export const TailwindThemedLogin = () => {
    return (
        <div className="dark"> {/* Tailwind dark mode */}
            <MicrosoftButton />
            <GoogleButton />
        </div>
    );
};

Theme Priority

The buttons check for dark mode in this order:

  1. [data-theme="dark"] attribute (highest priority)
  2. .dark-mode CSS class
  3. @media (prefers-color-scheme: dark) system preference (fallback)

Custom Theme Colors

Override the default colors using CSS variables:

/* Light mode customization */
:root {
    --rsb-microsoft-bg: #2f2f2f;
    --rsb-microsoft-color: #ffffff;
    --rsb-hover-brightness: 1.1;
}

/* Dark mode customization */
[data-theme="dark"] {
    --rsb-microsoft-bg: #404040;
    --rsb-microsoft-color: #e0e0e0;
    --rsb-hover-brightness: 1.15;
}

/* Specific provider override */
[data-theme="dark"] .rystem-social-button--google {
    background: linear-gradient(135deg, #434343 0%, #363636 100%);
}

Available CSS Variables

| Variable | Default (Light) | Default (Dark) | Description | |----------|----------------|----------------|-------------| | --rsb-background | Provider brand color | Darker shade | Button background | | --rsb-text | #ffffff | #e0e0e0 | Button text color | | --rsb-hover-brightness | 1.05 | 1.15 | Hover effect intensity | | --rsb-focus-ring | Provider color | Lighter shade | Keyboard focus outline | | --rsb-shadow | rgba(0,0,0,0.1) | rgba(0,0,0,0.3) | Button shadow | | --rsb-disabled-opacity | 0.6 | 0.5 | Disabled state opacity |

Framework Integration Examples

Next.js with next-themes

import { useTheme } from 'next-themes';
import { SocialLoginButtons } from 'rystem.authentication.social.react';

export const NextJsLogin = () => {
    const { theme, setTheme } = useTheme();

    return (
        <div data-theme={theme}>
            <button onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}>
                Toggle Theme
            </button>
            <SocialLoginButtons />
        </div>
    );
};

React Context Theme Provider

const ThemeContext = createContext({ isDark: false, toggle: () => {} });

export const ThemeProvider = ({ children }) => {
    const [isDark, setIsDark] = useState(
        () => window.matchMedia('(prefers-color-scheme: dark)').matches
    );

    useEffect(() => {
        const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
        const handler = (e) => setIsDark(e.matches);
        mediaQuery.addEventListener('change', handler);
        return () => mediaQuery.removeEventListener('change', handler);
    }, []);

    return (
        <ThemeContext.Provider value={{ isDark, toggle: () => setIsDark(!isDark) }}>
            <div data-theme={isDark ? 'dark' : 'light'}>
                {children}
            </div>
        </ThemeContext.Provider>
    );
};

// Usage
export const App = () => (
    <ThemeProvider>
        <LoginPage />
    </ThemeProvider>
);

Tailwind CSS Integration

// tailwind.config.js
module.exports = {
    darkMode: 'class', // Enable class-based dark mode
    // ...
};

// Component
export const TailwindLogin = () => {
    const [darkMode, setDarkMode] = useState(false);

    return (
        <div className={darkMode ? 'dark' : ''}>
            <div className="bg-white dark:bg-gray-900 min-h-screen">
                <button 
                    onClick={() => setDarkMode(!darkMode)}
                    className="mb-4 px-4 py-2 bg-gray-200 dark:bg-gray-700"
                >
                    Toggle Dark Mode
                </button>
                
                {/* Buttons automatically adapt to .dark class */}
                <SocialLoginButtons />
            </div>
        </div>
    );
};

Accessibility

The buttons maintain WCAG 2.1 AA contrast ratios in both light and dark modes:

  • Light mode: 4.5:1 minimum contrast
  • Dark mode: 4.5:1 minimum contrast
  • Focus indicators: 3:1 contrast with background
  • Hover states: Clearly visible in both modes

Testing Dark Mode

import { render } from '@testing-library/react';
import { MicrosoftButton } from 'rystem.authentication.social.react';

test('button renders correctly in dark mode', () => {
    const { container } = render(
        <div data-theme="dark">
            <MicrosoftButton />
        </div>
    );
    
    const button = container.querySelector('.rystem-social-button--microsoft');
    expect(button).toBeInTheDocument();
    
    // Check computed styles
    const styles = window.getComputedStyle(button);
    expect(styles.backgroundColor).toBeTruthy();
});

📝 Complete Example

import { useState, useContext } from 'react';
import { 
    SocialLoginButtons, 
    SocialLoginContextLogout, 
    SocialLoginContextRefresh, 
    SocialLogoutButton, 
    useSocialToken, 
    useSocialUser,
    MicrosoftButton,
    GoogleButton,
    GitHubButton
} from 'rystem.authentication.social.react';

const customButtons = [MicrosoftButton, GoogleButton, GitHubButton];

export const Dashboard = () => {
    const token = useSocialToken();
    const user = useSocialUser();
    const forceRefresh = useContext(SocialLoginContextRefresh);
    const logout = useContext(SocialLoginContextLogout);
    const [count, setCount] = useState(0);
    
    return (
        <div className="dashboard">
            {token.isExpired ? (
                <div className="login-section">
                    <h2>Welcome! Please login</h2>
                    <SocialLoginButtons buttons={customButtons} />
                </div>
            ) : (
                <div className="user-section">
                    <h2>Welcome back, {user.username}!</h2>
                    
                    <div className="token-info">
                        <p><strong>Access Token:</strong> {token.accessToken.substring(0, 20)}...</p>
                        <p><strong>Expires:</strong> {token.expiresIn.toLocaleString()}</p>
                    </div>
                    
                    <div className="actions">
                        <button onClick={() => setCount(count + 1)}>
                            Counter: {count}
                        </button>
                        <button onClick={() => forceRefresh()}>
                            🔄 Force Refresh Token
                        </button>
                        <SocialLogoutButton>
                            🚪 Logout
                        </SocialLogoutButton>
                    </div>
                </div>
            )}
        </div>
    );
};

🌐 OAuth Provider Configuration

Microsoft Entra ID (Azure AD)

  1. Go to Azure Portal → Azure Active Directory → App registrations
  2. Create new registration (Single-page application)
  3. Set Redirect URI: https://yourdomain.com/account/login
  4. Under Authentication:
    • Enable "ID tokens"
    • Enable "Access tokens"
    • Add redirect URI with type "Single-page application"
  5. Copy Application (client) ID
  6. No client secret needed - PKCE handles security

Google

  1. Go to Google Cloud Console
  2. Create OAuth 2.0 Client ID (Web application)
  3. Add Authorized redirect URI: https://yourdomain.com/account/login
  4. Copy Client ID

GitHub

  1. Go to GitHub Settings → OAuth Apps
  2. Create new OAuth App
  3. Set Authorization callback URL: https://yourdomain.com/account/login
  4. Copy Client ID

🔗 Related Packages

  • API Server: Rystem.Authentication.Social - Backend OAuth validation with PKCE support
  • Blazor Client: Rystem.Authentication.Social.Blazor - Blazor Server/WASM components
  • Abstractions: Rystem.Authentication.Social.Abstractions - Shared models

📚 More Information