It can simplify the integration with social identities and .Net.
npm install rystem.authentication.social.reactReact/TypeScript library for social authentication with built-in PKCE support for secure OAuth 2.0 flows.
- š 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
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.
| Provider | Web (Popup) | Web (Redirect) | React Native iOS | React Native Android | PKCE Support |
|----------|-------------|----------------|------------------|---------------------|--------------|
| Microsoft | ā
| ā
| ā
| ā
| ā
|
| Google | ā
| ā
| ā
| ā
| - |
| Facebook | ā
| ā
| ā
| ā
| - |
| GitHub | ā
| ā
| ā
| ā
| - |
| Amazon | ā
| ā
| ā
| ā
| - |
| LinkedIn | ā
| ā
| ā
| ā
| - |
| X (Twitter) | ā
| ā
| ā
| ā
| - |
| TikTok | ā
| ā
| ā
| ā
| - |
| Instagram | ā
| ā
| ā
| ā
| - |
| Pinterest | ā
| ā
| ā
| ā
| - |
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
``typescript
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.
`bash`
npm install rystem.authentication.social.react
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
`typescript
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}));Authentication error: ${error.message}
alert();
};
// Automatic token refresh when expired
x.automaticRefresh = true;
});
function Root() {
return (
);
}
export default Root;
`
`typescript
import { useSocialToken, useSocialUser, SocialLoginButtons, SocialLogoutButton } from 'rystem.authentication.social.react';
export const App = () => {
const token = useSocialToken();
const user = useSocialUser();
return (
Access Token: {token.accessToken}
š PKCE Support (Microsoft OAuth)
$3
The library automatically implements PKCE for Microsoft OAuth:
1. Code Verifier Generation: When user clicks Microsoft login button
`typescript
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
`typescript
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
`typescript
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
$3
For custom implementations:
`typescript
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
$3
Get current JWT token for API requests:
`typescript
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}
}
});
}
`$3
Get authenticated user information:
`typescript
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});
}
`$3
Force token refresh:
`typescript
import { useContext } from 'react';
import { SocialLoginContextRefresh } from 'rystem.authentication.social.react';const forceRefresh = useContext(SocialLoginContextRefresh);
const handleRefresh = async () => {
await forceRefresh();
console.log('Token refreshed!');
};
`$3
Programmatic logout:
`typescript
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
$3
Renders all configured provider buttons:
`typescript
import { SocialLoginButtons } from 'rystem.authentication.social.react';
`$3
`typescript
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
];
`$3
`typescript
import { MicrosoftButton, GoogleButton } from 'rystem.authentication.social.react';
`$3
`typescript
import { SocialLogoutButton } from 'rystem.authentication.social.react';Sign Out
`š§ Advanced Configuration
$3
The library now supports platform-specific configuration for Web, iOS, and Android (including React Native):
`typescript
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:
`typescript
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):
`xml
CFBundleURLTypes
CFBundleURLSchemes
msauth
CFBundleURLName
com.keyserdsoze.fantasoccer
`Android (
AndroidManifest.xml):
`xml
`$3
Choose between popup and redirect modes:
`typescript
// 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
$3
Use built-in utilities for platform detection:
`typescript
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');
}
`$3
`typescript
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
$3
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)$3
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$3
iOS Bundle ID Format:
`
msauth://com.yourcompany.yourapp/auth
`Android Package Name Format:
`
yourapp://oauth/callback
`š How Platform Configuration Works
$3
When a user clicks a social login button, the library determines the OAuth redirect URI using this priority order:
`typescript
// 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 || ''};
}
`$3
1. Setup Configuration:
`typescript
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";
});
`2. 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=
&code_challenge_method=S256
`3. 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 state4. 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 token5. User Logged In:
- Token stored in AsyncStorage (React Native)
-
useSocialToken() and useSocialUser() hooks update
- App navigates to /account/login (or dashboard)$3
`typescript
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;
}
`$3
| 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 |$3
ā
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$3
Check which redirect URI is being used:
`typescript
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
`typescript
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.
$3
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
`$3
No configuration needed - the library automatically uses
LocalStorageService:`typescript
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";
});
`$3
Implement
IStorageService for custom storage (secure storage, Redis, etc.):`typescript
import { setupSocialLogin, IStorageService } from 'rystem.authentication.social.react';// Example: Secure Storage for React Native
class SecureStorageService implements IStorageService {
async get(key: string): Promise {
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 {
try {
await SecureStore.setItemAsync(key, value);
} catch (error) {
console.error('SecureStorage set error:', error);
}
}
async remove(key: string): Promise {
try {
await SecureStore.deleteItemAsync(key);
} catch (error) {
console.error('SecureStorage remove error:', error);
}
}
async has(key: string): Promise {
const value = await this.get(key);
return value !== null;
}
async clear(): Promise {
// 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";
});
`$3
Perfect for unit tests without persisting data:
`typescript
import { IStorageService } from 'rystem.authentication.social.react';class MockStorageService implements IStorageService {
private storage = new Map();
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
});
`$3
For server-side rendering or distributed systems:
`typescript
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 {
return await this.client.get(key);
}
async set(key: string, value: string): Promise {
await this.client.set(key, value, { EX: 3600 }); // 1 hour expiry
}
async remove(key: string): Promise {
await this.client.del(key);
}
async has(key: string): Promise {
const exists = await this.client.exists(key);
return exists === 1;
}
async clear(): Promise {
await this.client.flushAll();
}
}
setupSocialLogin(x => {
x.storageService = new RedisStorageService();
// ... rest of config
});
`$3
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 |$3
| 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 |$3
Add encryption layer on top of any storage:
`typescript
class EncryptedStorageService implements IStorageService {
constructor(
private baseStorage: IStorageService,
private encryptionKey: string
) {}
async get(key: string): Promise {
const encrypted = await this.baseStorage.get(key);
if (!encrypted) return null;
return this.decrypt(encrypted, this.encryptionKey);
}
async set(key: string, value: string): Promise {
const encrypted = this.encrypt(value, this.encryptionKey);
await this.baseStorage.set(key, encrypted);
}
async remove(key: string): Promise {
await this.baseStorage.remove(key);
}
async has(key: string): Promise {
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
});
`STORAGE_ARCHITECTURE.md for detailed technical documentation.---
š§ Custom Routing Service
š§ Custom Routing Service
$3
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 stateSolution: The
IRoutingService abstraction provides a unified interface for:
- URL Parameter Reading (OAuth callback detection)
- Navigation Operations (redirects, return URLs, cleanup)$3
By default, the library uses
WindowRoutingService which uses native browser APIs:`typescript
// ā
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';
});
`$3
| 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 ā
|src/services/:
- ReactRouterRoutingService.example.ts - React Router v6+ (unified)
- NextAppRouterRoutingService.example.ts - Next.js App Router v13+ (unified)
- MockRoutingService.example.ts - Unit testing with verification methodsCopy these files to your project and remove the
.example extension.---
$3
If you're using React Router v6+, use this unified routing service:
`typescript
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
`typescript
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 (
My App
);
}// Wrap with Router
const Root = () => (
);
export default Root;
`---
$3
For Next.js 13+ App Router with client components:
`typescript
'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
`typescript
'use client'; // ā
Must be a Client Componentimport { 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 (
Login
);
}
`---
$3
For unit tests, use the mock service with verification methods:
`typescript
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'
]);
`---
$3
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:
`typescript
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.
---
$3
Check your routing service is properly initialized:
`typescript
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.---
$3
Before (without custom routing service):
`typescript
// ā 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):
`typescript
// ā
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!
`---
$3
| 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) ā
|---
$3
`typescript
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 ;
};
`$3
`typescript
interface CustomSocialUser {
username: string;
isAuthenticated: boolean;
displayName: string;
avatar: string;
roles: string[];
}const MyComponent = () => {
const user = useSocialUser();
return (

{user.displayName}
Roles: {user.roles.join(', ')}
);
};
`šØ Dark Mode & Theming
The modern social login buttons support automatic dark mode with three detection methods:
$3
The buttons automatically adapt to the user's system preference:
`css
/ No JavaScript needed - CSS handles it automatically /
@media (prefers-color-scheme: dark) {
/ Dark mode styles applied automatically /
}
`$3
Control the theme programmatically by setting the
data-theme attribute on any parent element:`typescript
import { MicrosoftButton } from 'rystem.authentication.social.react';export const ThemedLoginPage = () => {
const [isDark, setIsDark] = useState(false);
return (
);
};
`$3
Use CSS classes for framework integration (Tailwind, etc.):
`typescript
export const TailwindThemedLogin = () => {
return (
{/ Tailwind dark mode /}
);
};
`$3
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)$3
Override the default colors using CSS variables:
`css
/ 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%);
}
`$3
| 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 |$3
#### Next.js with
next-themes`typescript
import { useTheme } from 'next-themes';
import { SocialLoginButtons } from 'rystem.authentication.social.react';export const NextJsLogin = () => {
const { theme, setTheme } = useTheme();
return (
);
};
`#### React Context Theme Provider
`typescript
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 (
setIsDark(!isDark) }}>
{children}
);
};
// Usage
export const App = () => (
);
`#### Tailwind CSS Integration
`typescript
// tailwind.config.js
module.exports = {
darkMode: 'class', // Enable class-based dark mode
// ...
};// Component
export const TailwindLogin = () => {
const [darkMode, setDarkMode] = useState(false);
return (
onClick={() => setDarkMode(!darkMode)}
className="mb-4 px-4 py-2 bg-gray-200 dark:bg-gray-700"
>
Toggle Dark Mode
{/ Buttons automatically adapt to .dark class /}
);
};
`$3
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
$3
`typescript
import { render } from '@testing-library/react';
import { MicrosoftButton } from 'rystem.authentication.social.react';test('button renders correctly in dark mode', () => {
const { container } = render(
);
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
`typescript
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 (
{token.isExpired ? (
Welcome! Please login
) : (
Welcome back, {user.username}!
Access Token: {token.accessToken.substring(0, 20)}...
Expires: {token.expiresIn.toLocaleString()}
šŖ Logout
)}
);
};
`š OAuth Provider Configuration
$3
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$3
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$3
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- Complete Docs: https://rystem.net/mcp/tools/auth-social-typescript.md
- OAuth Flow Diagram: https://rystem.net/mcp/prompts/auth-flow.md
- PKCE RFC: RFC 7636