Tech With Htunn
  • Blog Content
  • ๐Ÿค–Artificial Intelligence
    • ๐Ÿง Building an Intelligent Agent with Local LLMs and Azure OpenAI
    • ๐Ÿ“ŠRevolutionizing IoT Monitoring: My Personal Journey with LLM-Powered Observability
  • ๐Ÿ“˜Core Concepts
    • ๐Ÿ”„Understanding DevSecOps
    • โฌ…๏ธShifting Left in DevSecOps
    • ๐Ÿ“ฆUnderstanding Containerization
    • โš™๏ธWhat is Site Reliability Engineering?
    • โฑ๏ธUnderstanding Toil in SRE
    • ๐Ÿ”What is Identity and Access Management?
    • ๐Ÿ“ŠMicrosoft Graph API: An Overview
    • ๐Ÿ”„Understanding Identity Brokers
  • ๐Ÿ”ŽSecurity Testing
    • ๐Ÿ”SAST vs DAST: Understanding the Differences
    • ๐ŸงฉSoftware Composition Analysis (SCA)
    • ๐Ÿ“‹Software Bill of Materials (SBOM)
    • ๐ŸงชDependency Scanning in DevSecOps
    • ๐ŸณContainer Scanning in DevSecOps
  • ๐Ÿ”„CI/CD Pipeline
    • ๐Ÿ”My Journey with Continuous Integration in DevOps
    • ๐Ÿš€My Journey with Continuous Delivery and Deployment in DevOps
  • ๐ŸงฎFundamentals
    • ๐Ÿ’พWhat is Data Engineering?
    • ๐Ÿ”„Understanding DataOps
    • ๐Ÿ‘ทThe Role of a Cloud Architect
    • ๐Ÿ›๏ธCloud Native Architecture
    • ๐Ÿ’ปCloud Native Applications
  • ๐Ÿ›๏ธArchitecture & Patterns
    • ๐Ÿ…Medallion Architecture in Data Engineering
    • ๐Ÿ”„ETL vs ELT Pipeline: Understanding the Differences
  • ๐Ÿ”’Authentication & Authorization
    • ๐Ÿ”‘OAuth 2.0 vs OIDC: Key Differences
    • ๐Ÿ”Understanding PKCE in OAuth 2.0
    • ๐Ÿ”„Service Provider vs Identity Provider Initiated SAML Flows
  • ๐Ÿ“‹Provisioning Standards
    • ๐Ÿ“ŠSCIM in Identity and Access Management
    • ๐Ÿ“กUnderstanding SCIM Streaming
  • ๐Ÿ—๏ธDesign Patterns
    • โšกEvent-Driven Architecture
    • ๐Ÿ”’Web Application Firewalls
  • ๐Ÿ“ŠReliability Metrics
    • ๐Ÿ’ฐError Budgets in SRE
    • ๐Ÿ“SLA vs SLO vs SLI: Understanding the Differences
    • โฑ๏ธMean Time to Recovery (MTTR)
Powered by GitBook
On this page
  • My "Aha!" Moment: Understanding the Core Difference
  • OAuth 2.0: The Authorization Framework I Rely On
  • OIDC: When I Need to Know Who's Who
  • Real-World Implementation with Keycloak and Node.js
  • Setting Up Keycloak for Both OAuth 2.0 and OIDC
  • OAuth 2.0 Implementation in Node.js
  • OIDC Implementation in Node.js
  • Key Differences I've Learned from Real-World Experience
  • 1. Purpose and Functionality
  • 2. Tokens and What They Contain
  • 3. Protocol Parameters
  • 4. Scopes vs. Claims
  • 5. Typical Use Cases
  • When to Use Which Protocol: My Decision Tree
  • Conclusion: They're Better Together
  1. Authentication & Authorization

OAuth 2.0 vs OIDC: Key Differences

PreviousETL vs ELT Pipeline: Understanding the DifferencesNextUnderstanding PKCE in OAuth 2.0

Last updated 20 hours ago

When I first dove into the world of authentication and authorization protocols, I found myself confused by all the acronyms and standards. OAuth 2.0, OIDC, JWT, SAML... it was overwhelming. After years of implementing these protocols in production environments, I've come to appreciate the nuances between them, particularly OAuth 2.0 and OpenID Connect (OIDC). Let me share what I've learned through my own experiences.

My "Aha!" Moment: Understanding the Core Difference

I remember the exact moment when the difference between OAuth 2.0 and OIDC finally clicked for me. I was implementing authentication for a microservices architecture, and a senior architect asked me:

"Are you trying to let users access resources or are you trying to know who they are?"

That simple question illuminated everything:

  • OAuth 2.0 answers the question: "What can this application do on behalf of this user?" (Authorization)

  • OIDC answers the question: "Who is this user?" (Authentication)

OAuth 2.0: The Authorization Framework I Rely On

In my early development days, I built a dashboard application that needed to access users' Google Calendar data without storing their Google passwords. This was a perfect use case for OAuth 2.0.

OAuth 2.0 is fundamentally about delegated authorization. It's a protocol that allows a user to grant a third-party application limited access to their resources without sharing their credentials. Think of it like a hotel key card system โ€“ the key gives you access to specific areas without giving you ownership of the hotel.

The core OAuth 2.0 flow I use most often looks like this:

When implementing OAuth 2.0 with Keycloak, I focus on obtaining and using access tokens to request resources. The access token doesn't tell me much about the user โ€“ it's mostly about what the user has authorized my application to do.

OIDC: When I Need to Know Who's Who

A few years later, I was building a SaaS platform that needed user profiles, role-based access, and single sign-on capabilities. That's when I turned to OpenID Connect.

OIDC extends OAuth 2.0 by adding a critical component: standardized user identity. It introduces the ID token, which contains verified claims about who the user is. In my mental model, OIDC takes OAuth 2.0 and says: "Great job handling authorization! Now let's add authentication too."

My typical OIDC flow looks like this:

The game-changer with OIDC is the ID token (typically a JWT) that contains standardized claims about the user โ€“ their identifier, name, email, and other profile information. This token is signed, allowing my application to verify the user's identity locally without making additional API calls.

Real-World Implementation with Keycloak and Node.js

Let me share how I implement both protocols using Keycloak as my identity provider and Node.js for my applications.

Setting Up Keycloak for Both OAuth 2.0 and OIDC

First, I always configure Keycloak with both protocols in mind:

  1. I create a dedicated realm for my application environment

  2. I set up a client with these key settings:

Client ID: myapp
Client Protocol: openid-connect (supports both OAuth 2.0 and OIDC)
Access Type: confidential (for server applications) or public (for SPAs/mobile)
Standard Flow Enabled: ON (for authorization code flow)
Valid Redirect URIs: https://myapp.com/callback
  1. Then I define roles and groups to model my application's permission structure

OAuth 2.0 Implementation in Node.js

When I'm focused purely on authorization using OAuth 2.0, my implementation looks something like this:

// oauth2-example.js
const express = require('express');
const session = require('express-session');
const axios = require('axios');
const app = express();

// Configure session middleware
app.use(session({
  secret: 'my-secret-key',
  resave: false,
  saveUninitialized: true
}));

// Keycloak OAuth 2.0 configuration
const keycloakConfig = {
  authServerUrl: 'https://keycloak.mycompany.com/auth',
  realm: 'my-realm',
  clientId: 'my-client',
  clientSecret: 'my-client-secret', // Only for confidential clients
  redirectUri: 'http://localhost:3000/callback'
};

// Construct the OAuth endpoints
const authEndpoint = `${keycloakConfig.authServerUrl}/realms/${keycloakConfig.realm}/protocol/openid-connect/auth`;
const tokenEndpoint = `${keycloakConfig.authServerUrl}/realms/${keycloakConfig.realm}/protocol/openid-connect/token`;

// Step 1: Redirect user to the authorization endpoint
app.get('/login', (req, res) => {
  // Store a random state value to prevent CSRF attacks
  req.session.oauthState = Math.random().toString(36).substring(2);
  
  // Build the authorization URL - notice specific OAuth 2.0 scope
  const authUrl = new URL(authEndpoint);
  authUrl.searchParams.append('client_id', keycloakConfig.clientId);
  authUrl.searchParams.append('redirect_uri', keycloakConfig.redirectUri);
  authUrl.searchParams.append('response_type', 'code');
  authUrl.searchParams.append('state', req.session.oauthState);
  authUrl.searchParams.append('scope', 'calendar-read user-profile'); // Resource-specific scopes
  
  res.redirect(authUrl.toString());
});

// Step 2: Handle the callback and exchange code for tokens
app.get('/callback', async (req, res) => {
  // Verify state to prevent CSRF attacks
  if (req.query.state !== req.session.oauthState) {
    return res.status(403).send('Invalid state parameter');
  }
  
  try {
    // Exchange authorization code for access token
    const tokenResponse = await axios.post(tokenEndpoint, new URLSearchParams({
      grant_type: 'authorization_code',
      client_id: keycloakConfig.clientId,
      client_secret: keycloakConfig.clientSecret,
      code: req.query.code,
      redirect_uri: keycloakConfig.redirectUri
    }));
    
    // Store tokens in session
    req.session.tokens = {
      access_token: tokenResponse.data.access_token,
      refresh_token: tokenResponse.data.refresh_token
    };
    
    res.redirect('/calendar');
  } catch (error) {
    console.error('Token exchange failed:', error.response?.data || error);
    res.status(500).send('Authentication failed');
  }
});

// Step 3: Use access token to access protected resources
app.get('/calendar', async (req, res) => {
  if (!req.session.tokens?.access_token) {
    return res.redirect('/login');
  }
  
  try {
    // Call a resource API using the access token
    const calendarResponse = await axios.get('https://api.myapp.com/calendar', {
      headers: { Authorization: `Bearer ${req.session.tokens.access_token}` }
    });
    
    res.send(`
      <h1>Your Calendar Events</h1>
      <pre>${JSON.stringify(calendarResponse.data, null, 2)}</pre>
      <a href="/logout">Logout</a>
    `);
  } catch (error) {
    if (error.response?.status === 401) {
      // Token expired - could add refresh token logic here
      return res.redirect('/login');
    }
    res.status(500).send('Error fetching calendar data');
  }
});

app.listen(3000, () => console.log('Server running on http://localhost:3000'));

In this OAuth 2.0 implementation, I'm primarily focused on obtaining an access token to call APIs. I don't extract or validate user identity from the tokens - I just pass them along to resource servers.

OIDC Implementation in Node.js

When I need authentication and user identity, my OIDC implementation looks like this:

// oidc-example.js
const express = require('express');
const session = require('express-session');
const axios = require('axios');
const jwt = require('jsonwebtoken');
const jwksClient = require('jwks-rsa');
const app = express();

app.use(session({
  secret: 'my-secret-key',
  resave: false,
  saveUninitialized: true
}));

// Keycloak OIDC configuration
const keycloakConfig = {
  authServerUrl: 'https://keycloak.mycompany.com/auth',
  realm: 'my-realm',
  clientId: 'my-client',
  clientSecret: 'my-client-secret',
  redirectUri: 'http://localhost:3000/callback'
};

// Construct the OIDC endpoints
const authEndpoint = `${keycloakConfig.authServerUrl}/realms/${keycloakConfig.realm}/protocol/openid-connect/auth`;
const tokenEndpoint = `${keycloakConfig.authServerUrl}/realms/${keycloakConfig.realm}/protocol/openid-connect/token`;
const jwksUri = `${keycloakConfig.authServerUrl}/realms/${keycloakConfig.realm}/protocol/openid-connect/certs`;

// Set up JWKS client for ID token validation
const jwks = jwksClient({
  jwksUri: jwksUri
});

// Helper function to verify ID token
async function verifyIdToken(idToken) {
  // Decode the token header to get the key ID
  const decoded = jwt.decode(idToken, { complete: true });
  if (!decoded || !decoded.header || !decoded.header.kid) {
    throw new Error('Invalid token');
  }

  // Get the signing key from Keycloak's JWKS
  const key = await jwks.getSigningKey(decoded.header.kid);
  const signingKey = key.publicKey || key.rsaPublicKey;

  // Verify the token
  return jwt.verify(idToken, signingKey, {
    audience: keycloakConfig.clientId,
    issuer: `${keycloakConfig.authServerUrl}/realms/${keycloakConfig.realm}`
  });
}

// Step 1: Redirect user to the authentication endpoint
app.get('/login', (req, res) => {
  req.session.oidcState = Math.random().toString(36).substring(2);
  req.session.oidcNonce = Math.random().toString(36).substring(2);
  
  // Build the authorization URL - notice OIDC-specific scopes
  const authUrl = new URL(authEndpoint);
  authUrl.searchParams.append('client_id', keycloakConfig.clientId);
  authUrl.searchParams.append('redirect_uri', keycloakConfig.redirectUri);
  authUrl.searchParams.append('response_type', 'code');
  authUrl.searchParams.append('state', req.session.oidcState);
  authUrl.searchParams.append('nonce', req.session.oidcNonce); // OIDC specific!
  authUrl.searchParams.append('scope', 'openid profile email'); // OIDC scopes
  
  res.redirect(authUrl.toString());
});

// Step 2: Handle the callback and process the ID token
app.get('/callback', async (req, res) => {
  if (req.query.state !== req.session.oidcState) {
    return res.status(403).send('Invalid state parameter');
  }
  
  try {
    // Exchange code for tokens
    const tokenResponse = await axios.post(tokenEndpoint, new URLSearchParams({
      grant_type: 'authorization_code',
      client_id: keycloakConfig.clientId,
      client_secret: keycloakConfig.clientSecret,
      code: req.query.code,
      redirect_uri: keycloakConfig.redirectUri
    }));
    
    // Get all tokens - note the ID token which is OIDC-specific
    const { access_token, refresh_token, id_token } = tokenResponse.data;
    
    // Verify and decode the ID token
    const userInfo = await verifyIdToken(id_token);
    
    // Verify the nonce to prevent replay attacks (OIDC specific)
    if (userInfo.nonce !== req.session.oidcNonce) {
      return res.status(403).send('Invalid nonce');
    }
    
    // Store user info and tokens in session
    req.session.user = {
      sub: userInfo.sub,
      name: userInfo.name,
      email: userInfo.email,
      preferred_username: userInfo.preferred_username
    };
    
    req.session.tokens = { access_token, refresh_token, id_token };
    
    res.redirect('/dashboard');
  } catch (error) {
    console.error('Authentication failed:', error);
    res.status(500).send('Authentication failed');
  }
});

// Step 3: Display user information from the ID token
app.get('/dashboard', (req, res) => {
  if (!req.session.user) {
    return res.redirect('/login');
  }
  
  res.send(`
    <h1>Welcome, ${req.session.user.name}!</h1>
    <p>Email: ${req.session.user.email}</p>
    <p>Username: ${req.session.user.preferred_username}</p>
    <p>User ID: ${req.session.user.sub}</p>
    <a href="/profile">View Profile API Data</a>
    <br><br>
    <a href="/logout">Logout</a>
  `);
});

// Step 4: Use the access token to call APIs (same as OAuth 2.0)
app.get('/profile', async (req, res) => {
  if (!req.session.tokens?.access_token) {
    return res.redirect('/login');
  }
  
  try {
    const profileResponse = await axios.get('https://api.myapp.com/profile', {
      headers: { Authorization: `Bearer ${req.session.tokens.access_token}` }
    });
    
    res.send(`
      <h1>Your Profile Data</h1>
      <pre>${JSON.stringify(profileResponse.data, null, 2)}</pre>
      <a href="/dashboard">Back to Dashboard</a>
    `);
  } catch (error) {
    res.status(500).send('Error fetching profile data');
  }
});

app.get('/logout', (req, res) => {
  req.session.destroy();
  res.redirect('/');
});

app.listen(3000, () => console.log('Server running on http://localhost:3000'));

In my OIDC implementation, I'm not only obtaining an access token but also:

  1. Requesting and validating an ID token

  2. Verifying the token signature using Keycloak's public keys

  3. Checking the nonce value to prevent replay attacks

  4. Extracting standardized user information

  5. Building a user session based on verified identity claims

Key Differences I've Learned from Real-World Experience

After implementing both protocols across dozens of projects, here are the key differences I've observed:

1. Purpose and Functionality

OAuth 2.0:

  • Focused on getting permission to access resources

  • Primarily concerned with "what can be accessed"

  • Provides access tokens for API authorization

  • Doesn't standardize user information

OIDC:

  • Focused on establishing user identity

  • Primarily concerned with "who is accessing"

  • Provides ID tokens containing verified user information

  • Still includes access tokens for API authorization

2. Tokens and What They Contain

In my OAuth 2.0 implementations, I work primarily with:

  • Access Token: Used to access resources, contains permissions (scopes)

  • Refresh Token: Used to get new access tokens when they expire

In my OIDC implementations, I work with both of the above, plus:

  • ID Token: Contains verified claims about the user (sub, name, email, etc.)

3. Protocol Parameters

When implementing OIDC, I've noticed these additional parameters:

  • nonce: Prevents replay attacks with ID tokens

  • openid scope: Required to engage the OIDC protocol

  • Additional standard scopes: profile, email, address, phone

4. Scopes vs. Claims

One distinction that tripped me up initially:

  • In OAuth 2.0, scopes define what actions can be performed (e.g., calendar.read)

  • In OIDC, claims are pieces of user information (e.g., name, email)

5. Typical Use Cases

In my work, I choose between them based on the primary need:

I Choose OAuth 2.0 When:

  • I need API authorization between services

  • User identity isn't required

  • The focus is on machine-to-machine permissions

I Choose OIDC When:

  • I need user login functionality

  • I need verified user identity information

  • I'm building user-facing applications with profiles

When to Use Which Protocol: My Decision Tree

After years of implementing both protocols, here's my simple decision tree:

  1. Do you need to know who the user is?

    • Yes โ†’ Use OIDC

    • No, I just need permission to access resources โ†’ Use OAuth 2.0

  2. Are you building a user-facing application with login?

    • Yes โ†’ Use OIDC

    • No, it's a backend service or API โ†’ OAuth 2.0 is sufficient

  3. Do you need standardized user profile information?

    • Yes โ†’ Use OIDC

    • No โ†’ OAuth 2.0 is sufficient

Conclusion: They're Better Together

In my real-world implementations, I've found that OAuth 2.0 and OIDC aren't competing standardsโ€”they're complementary. OIDC builds on OAuth 2.0 to create a complete identity solution.

The beauty is that with modern identity providers like Keycloak, you don't have to choose one or the other. When you configure your Keycloak client as an "openid-connect" client, you get the best of both worlds:

  • Authentication via ID tokens (OIDC)

  • Authorization via access tokens (OAuth 2.0)

I've found that most modern web applications benefit from using both protocols together. Start with OIDC for authentication and user identity, then use the same access tokens for OAuth 2.0-style API authorization.

Having implemented both protocols in production systems, I've come to appreciate how they work together to solve the complete identity and access management puzzle.

๐Ÿ”’
๐Ÿ”‘