Prerequisites
- Basic understanding of JavaScript ๐
- TypeScript installation โก
- VS Code or preferred IDE ๐ป
What you'll learn
- Understand the concept fundamentals ๐ฏ
- Apply the concept in real projects ๐๏ธ
- Debug common issues ๐
- Write type-safe code โจ
๐ฏ Introduction
Welcome to this exciting tutorial on building an Authentication Service with OAuth implementation! ๐ In this guide, weโll explore how to create a secure, type-safe authentication system using TypeScript and OAuth 2.0.
Youโll discover how OAuth can transform your applicationโs security and user experience. Whether youโre building web applications ๐, mobile apps ๐ฑ, or APIs ๐ฅ๏ธ, understanding OAuth authentication is essential for modern development.
By the end of this tutorial, youโll feel confident implementing OAuth in your own projects! Letโs dive in! ๐โโ๏ธ
๐ Understanding OAuth 2.0
๐ค What is OAuth?
OAuth is like having a valet key for your car ๐. Instead of giving someone your master key (password), you give them limited access to specific features. Think of it as a secure way to let apps access your data without sharing your credentials.
In TypeScript terms, OAuth is a protocol that enables secure authorization between services. This means you can:
- โจ Let users log in with Google, GitHub, or Facebook
- ๐ Access user data without storing passwords
- ๐ก๏ธ Provide granular permissions and scopes
๐ก Why Use OAuth?
Hereโs why developers love OAuth:
- Enhanced Security ๐: No password storage needed
- Better User Experience ๐ป: One-click login with existing accounts
- Reduced Development Time ๐: Leverage existing auth providers
- Trust Building ๐ง: Users trust established providers
Real-world example: Imagine building a productivity app ๐. With OAuth, users can sign in with their Google account and automatically sync their calendar - no new passwords to remember!
๐ง Basic OAuth Flow and Types
๐ OAuth Grant Types
Letโs start with the main OAuth flows:
// ๐ OAuth grant types
type OAuthGrantType =
| "authorization_code" // ๐ Web applications
| "implicit" // ๐ฑ Single-page apps (deprecated)
| "client_credentials" // ๐ค Machine-to-machine
| "refresh_token"; // ๐ Token renewal
// ๐จ OAuth configuration interface
interface OAuthConfig {
clientId: string; // ๐ Your app's ID
clientSecret: string; // ๐ Secret key (keep safe!)
redirectUri: string; // ๐ Where to redirect after auth
scope: string[]; // ๐ฏ Permissions requested
provider: string; // ๐ข OAuth provider (Google, GitHub, etc.)
}
๐ก Explanation: The authorization code flow is the most secure and commonly used for web applications!
๐ฏ Basic OAuth Client
Hereโs a simple OAuth client structure:
// ๐๏ธ OAuth client class
class OAuthClient {
private config: OAuthConfig;
constructor(config: OAuthConfig) {
this.config = config;
console.log(`๐ OAuth client initialized for ${config.provider}`);
}
// ๐ Generate authorization URL
getAuthUrl(state: string): string {
const params = new URLSearchParams({
client_id: this.config.clientId,
redirect_uri: this.config.redirectUri,
response_type: 'code',
scope: this.config.scope.join(' '),
state: state // ๐ก๏ธ Prevent CSRF attacks
});
return `https://provider.com/oauth/authorize?${params}`;
}
// ๐ฏ Exchange code for tokens
async exchangeCodeForTokens(code: string): Promise<TokenResponse> {
console.log("๐ Exchanging authorization code for tokens...");
// Implementation details in practical examples
return {} as TokenResponse;
}
}
๐ก Practical Examples
๐ Example 1: Google OAuth Integration
Letโs build a complete Google OAuth implementation:
// ๐จ Token response structure
interface TokenResponse {
access_token: string;
refresh_token?: string;
expires_in: number;
token_type: string;
scope: string;
}
// ๐ค User profile from OAuth
interface OAuthProfile {
id: string;
email: string;
name: string;
picture?: string;
provider: string;
}
// ๐๏ธ Google OAuth service
class GoogleOAuthService {
private readonly config: OAuthConfig = {
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
redirectUri: 'http://localhost:3000/auth/google/callback',
scope: ['email', 'profile'],
provider: 'google'
};
private readonly authUrl = 'https://accounts.google.com/o/oauth2/v2/auth';
private readonly tokenUrl = 'https://oauth2.googleapis.com/token';
// ๐ฏ Step 1: Get authorization URL
getAuthorizationUrl(): string {
const state = this.generateSecureState(); // ๐ก๏ธ CSRF protection
const params = new URLSearchParams({
client_id: this.config.clientId,
redirect_uri: this.config.redirectUri,
response_type: 'code',
scope: this.config.scope.join(' '),
state,
access_type: 'offline', // ๐ Get refresh token
prompt: 'consent'
});
console.log("๐ Redirecting to Google OAuth...");
return `${this.authUrl}?${params}`;
}
// ๐ Step 2: Exchange code for tokens
async exchangeCodeForTokens(code: string): Promise<TokenResponse> {
const payload = {
code,
client_id: this.config.clientId,
client_secret: this.config.clientSecret,
redirect_uri: this.config.redirectUri,
grant_type: 'authorization_code'
};
try {
const response = await fetch(this.tokenUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
const tokens = await response.json();
console.log("โ
Tokens received successfully!");
return tokens;
} catch (error) {
console.error("โ Token exchange failed:", error);
throw error;
}
}
// ๐ค Step 3: Get user profile
async getUserProfile(accessToken: string): Promise<OAuthProfile> {
const response = await fetch('https://www.googleapis.com/oauth2/v2/userinfo', {
headers: { Authorization: `Bearer ${accessToken}` }
});
const profile = await response.json();
console.log(`๐ Welcome, ${profile.name}!`);
return {
id: profile.id,
email: profile.email,
name: profile.name,
picture: profile.picture,
provider: 'google'
};
}
// ๐ก๏ธ Generate secure state parameter
private generateSecureState(): string {
return Math.random().toString(36).substring(2, 15);
}
}
// ๐ฎ Using the service
const googleAuth = new GoogleOAuthService();
const authUrl = googleAuth.getAuthorizationUrl();
console.log("๐ Visit this URL to authenticate:", authUrl);
๐ฏ Try it yourself: Add GitHub OAuth support using the same pattern!
๐ฎ Example 2: JWT Token Management
Letโs handle tokens securely:
// ๐ JWT token manager
interface JWTPayload {
sub: string; // ๐ค User ID
email: string; // ๐ง User email
exp: number; // โฐ Expiration timestamp
iat: number; // ๐ Issued at timestamp
scope: string[]; // ๐ฏ Permissions
}
class TokenManager {
private tokens: Map<string, TokenData> = new Map();
// ๐พ Token storage structure
interface TokenData {
accessToken: string;
refreshToken?: string;
expiresAt: Date;
userId: string;
}
// ๐ Store tokens securely
storeTokens(userId: string, tokens: TokenResponse): void {
const expiresAt = new Date(Date.now() + tokens.expires_in * 1000);
this.tokens.set(userId, {
accessToken: tokens.access_token,
refreshToken: tokens.refresh_token,
expiresAt,
userId
});
console.log(`โ
Tokens stored for user ${userId}`);
console.log(`โฐ Expires at: ${expiresAt.toLocaleString()}`);
}
// ๐ Check if token needs refresh
needsRefresh(userId: string): boolean {
const tokenData = this.tokens.get(userId);
if (!tokenData) return true;
// ๐ Refresh if expires in less than 5 minutes
const fiveMinutes = 5 * 60 * 1000;
const needsRefresh = tokenData.expiresAt.getTime() - Date.now() < fiveMinutes;
if (needsRefresh) {
console.log("๐ Token expires soon, needs refresh!");
}
return needsRefresh;
}
// ๐ฏ Get valid access token
async getAccessToken(userId: string): Promise<string> {
if (this.needsRefresh(userId)) {
await this.refreshAccessToken(userId);
}
const tokenData = this.tokens.get(userId);
if (!tokenData) {
throw new Error("โ No tokens found for user");
}
return tokenData.accessToken;
}
// ๐ Refresh access token
private async refreshAccessToken(userId: string): Promise<void> {
const tokenData = this.tokens.get(userId);
if (!tokenData?.refreshToken) {
throw new Error("โ No refresh token available");
}
console.log("๐ Refreshing access token...");
// Implementation would call OAuth provider's token endpoint
// This is a simplified example
const newTokens: TokenResponse = {
access_token: "new_access_token_" + Date.now(),
refresh_token: tokenData.refreshToken,
expires_in: 3600,
token_type: "Bearer",
scope: "email profile"
};
this.storeTokens(userId, newTokens);
console.log("โ
Token refreshed successfully!");
}
}
๐ Advanced Concepts
๐งโโ๏ธ Advanced OAuth Security
When youโre ready to level up, implement these security measures:
// ๐ฏ Advanced OAuth security manager
class OAuthSecurityManager {
// ๐ก๏ธ PKCE (Proof Key for Code Exchange) for public clients
generatePKCEChallenge(): { verifier: string; challenge: string } {
const verifier = this.generateRandomString(128);
const challenge = this.base64UrlEncode(
crypto.createHash('sha256').update(verifier).digest()
);
return { verifier, challenge };
}
// ๐ Validate state parameter
validateState(receivedState: string, storedState: string): boolean {
if (receivedState !== storedState) {
console.error("โ State mismatch - possible CSRF attack!");
return false;
}
console.log("โ
State validated successfully");
return true;
}
// ๐จ Generate cryptographically secure random string
private generateRandomString(length: number): string {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~';
let result = '';
const randomValues = crypto.getRandomValues(new Uint8Array(length));
for (let i = 0; i < length; i++) {
result += chars[randomValues[i] % chars.length];
}
return result;
}
// ๐ง Base64 URL encoding
private base64UrlEncode(buffer: Buffer): string {
return buffer.toString('base64')
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '');
}
}
๐๏ธ Multi-Provider OAuth System
For the brave developers, hereโs a multi-provider system:
// ๐ Universal OAuth provider system
type OAuthProvider = 'google' | 'github' | 'facebook' | 'microsoft';
interface ProviderConfig {
authUrl: string;
tokenUrl: string;
userInfoUrl: string;
scope: string[];
}
class UniversalOAuthService {
private providers: Map<OAuthProvider, ProviderConfig> = new Map([
['google', {
authUrl: 'https://accounts.google.com/o/oauth2/v2/auth',
tokenUrl: 'https://oauth2.googleapis.com/token',
userInfoUrl: 'https://www.googleapis.com/oauth2/v2/userinfo',
scope: ['email', 'profile']
}],
['github', {
authUrl: 'https://github.com/login/oauth/authorize',
tokenUrl: 'https://github.com/login/oauth/access_token',
userInfoUrl: 'https://api.github.com/user',
scope: ['user:email']
}]
]);
// ๐ฏ Get provider-specific auth URL
getAuthUrl(provider: OAuthProvider, clientId: string): string {
const config = this.providers.get(provider);
if (!config) {
throw new Error(`โ Unknown provider: ${provider}`);
}
console.log(`๐ Generating ${provider} auth URL...`);
// Provider-specific implementation
return `${config.authUrl}?client_id=${clientId}`;
}
}
โ ๏ธ Common Pitfalls and Solutions
๐ฑ Pitfall 1: Storing Secrets in Code
// โ Wrong way - NEVER do this!
const oauthConfig = {
clientSecret: "my-super-secret-key-123", // ๐ฅ Security breach!
clientId: "my-client-id"
};
// โ
Correct way - Use environment variables!
const oauthConfig = {
clientSecret: process.env.OAUTH_CLIENT_SECRET!, // ๐ก๏ธ Secure!
clientId: process.env.OAUTH_CLIENT_ID!
};
// ๐ก Even better - validate environment variables
if (!process.env.OAUTH_CLIENT_SECRET) {
throw new Error("โ ๏ธ OAUTH_CLIENT_SECRET not configured!");
}
๐คฏ Pitfall 2: Not Validating Tokens
// โ Dangerous - trusting tokens blindly!
function handleCallback(token: string): void {
// Using token without validation
saveUserSession(token); // ๐ฅ Could be forged!
}
// โ
Safe - always validate tokens!
async function handleCallback(token: string): Promise<void> {
try {
// ๐ Verify token with provider
const isValid = await verifyTokenWithProvider(token);
if (!isValid) {
console.error("โ Invalid token!");
throw new Error("Authentication failed");
}
// โ
Token is valid, proceed safely
await saveUserSession(token);
console.log("โ
User authenticated successfully!");
} catch (error) {
console.error("โ ๏ธ Authentication error:", error);
throw error;
}
}
๐ ๏ธ Best Practices
- ๐ฏ Use HTTPS Always: OAuth requires secure connections
- ๐ Implement PKCE: Even for confidential clients
- ๐ก๏ธ Validate Everything: State, tokens, and redirects
- ๐จ Store Tokens Securely: Use encrypted storage
- โจ Handle Errors Gracefully: Provide clear user feedback
๐งช Hands-On Exercise
๐ฏ Challenge: Build a Multi-Provider Auth System
Create a complete authentication system with these features:
๐ Requirements:
- โ Support for Google and GitHub OAuth
- ๐ท๏ธ User profile merging (same email = same user)
- ๐ค Session management with refresh tokens
- ๐ Token expiration handling
- ๐จ Provider-specific user avatars
๐ Bonus Points:
- Add Microsoft/Azure AD support
- Implement account linking
- Create an admin dashboard for OAuth analytics
๐ก Solution
๐ Click to see solution
// ๐ฏ Complete multi-provider OAuth system!
interface AuthProvider {
name: OAuthProvider;
getAuthUrl(): string;
exchangeCode(code: string): Promise<TokenResponse>;
getUserInfo(token: string): Promise<OAuthProfile>;
}
// ๐๏ธ Main authentication service
class AuthenticationService {
private providers: Map<OAuthProvider, AuthProvider> = new Map();
private sessions: Map<string, UserSession> = new Map();
// ๐ค User session structure
interface UserSession {
userId: string;
email: string;
providers: OAuthProvider[];
tokens: Map<OAuthProvider, TokenData>;
avatar?: string;
lastActive: Date;
}
// โ Register a provider
registerProvider(provider: AuthProvider): void {
this.providers.set(provider.name, provider);
console.log(`โ
Registered ${provider.name} provider`);
}
// ๐ Initiate OAuth flow
async authenticate(provider: OAuthProvider): Promise<string> {
const authProvider = this.providers.get(provider);
if (!authProvider) {
throw new Error(`โ Provider ${provider} not configured`);
}
return authProvider.getAuthUrl();
}
// ๐ Handle OAuth callback
async handleCallback(
provider: OAuthProvider,
code: string
): Promise<UserSession> {
const authProvider = this.providers.get(provider);
if (!authProvider) {
throw new Error(`โ Unknown provider: ${provider}`);
}
// ๐ Exchange code for tokens
const tokens = await authProvider.exchangeCode(code);
// ๐ค Get user profile
const profile = await authProvider.getUserInfo(tokens.access_token);
// ๐ Find or create user session
let session = this.findSessionByEmail(profile.email);
if (!session) {
// ๐ New user
session = {
userId: this.generateUserId(),
email: profile.email,
providers: [provider],
tokens: new Map([[provider, tokens]]),
avatar: profile.picture,
lastActive: new Date()
};
console.log(`๐ New user registered: ${profile.name}`);
} else {
// ๐ Link account
if (!session.providers.includes(provider)) {
session.providers.push(provider);
console.log(`๐ Linked ${provider} to existing account`);
}
session.tokens.set(provider, tokens);
session.lastActive = new Date();
}
this.sessions.set(session.userId, session);
return session;
}
// ๐ Find session by email
private findSessionByEmail(email: string): UserSession | undefined {
for (const session of this.sessions.values()) {
if (session.email === email) {
return session;
}
}
return undefined;
}
// ๐ Generate unique user ID
private generateUserId(): string {
return `user_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
// ๐ Get authentication stats
getStats(): void {
console.log("๐ Authentication Stats:");
console.log(` ๐ฅ Total users: ${this.sessions.size}`);
console.log(` ๐ Providers configured: ${this.providers.size}`);
const providerCounts = new Map<OAuthProvider, number>();
for (const session of this.sessions.values()) {
for (const provider of session.providers) {
providerCounts.set(provider, (providerCounts.get(provider) || 0) + 1);
}
}
console.log(" ๐ Users by provider:");
for (const [provider, count] of providerCounts) {
console.log(` ${provider}: ${count} users`);
}
}
}
// ๐ฎ Test the system!
const authService = new AuthenticationService();
// Register providers
authService.registerProvider(new GoogleAuthProvider());
authService.registerProvider(new GitHubAuthProvider());
// Simulate authentication
const authUrl = await authService.authenticate('google');
console.log("๐ Redirect user to:", authUrl);
๐ Key Takeaways
Youโve learned so much! Hereโs what you can now do:
- โ Implement OAuth 2.0 with confidence ๐ช
- โ Handle multiple providers like a pro ๐ก๏ธ
- โ Secure your authentication with best practices ๐ฏ
- โ Debug OAuth issues effectively ๐
- โ Build production-ready auth systems! ๐
Remember: Security is not optional - itโs essential! Always validate, always use HTTPS, and always keep secrets secret. ๐ค
๐ค Next Steps
Congratulations! ๐ Youโve mastered OAuth implementation in TypeScript!
Hereโs what to do next:
- ๐ป Implement the multi-provider exercise
- ๐๏ธ Add OAuth to your existing projects
- ๐ Explore OAuth 2.1 specifications
- ๐ Learn about OpenID Connect (OIDC)
Remember: Every secure app starts with proper authentication. Keep building, keep securing, and most importantly, keep your users safe! ๐
Happy coding! ๐๐โจ