Przejdź do treści
Security

OAuth2 and OpenID Connect - Modern Authentication

Published on:
·5 min read·Author: MDS Software Solutions Group

OAuth2 OpenID Connect

bezpieczenstwo

OAuth2 and OpenID Connect - Modern Authentication

Web application security starts with proper user authentication and authorization. OAuth 2.0 and OpenID Connect are two complementary standards that have become the foundation of modern authentication systems. In this article, we will thoroughly discuss how they work, their differences, implementation patterns, and security best practices.

What is OAuth 2.0?#

OAuth 2.0 is an authorization protocol that allows applications to obtain limited access to user accounts on external services. It is crucial to understand that OAuth 2.0 by itself is not an authentication protocol - it deals exclusively with authorization, meaning it determines what resources an application can access.

Key Roles in OAuth 2.0#

  • Resource Owner - the user who owns the protected resources
  • Client - the application requesting access to resources
  • Authorization Server - the server that issues access tokens
  • Resource Server - the server hosting protected resources
+--------+                               +---------------+
|        |--(A)- Authorization Request ->|   Resource    |
|        |                               |     Owner     |
|        |<-(B)-- Authorization Grant ---|               |
|        |                               +---------------+
|        |
|        |                               +---------------+
|        |--(C)-- Authorization Grant -->| Authorization |
| Client |                               |     Server    |
|        |<-(D)----- Access Token -------|               |
|        |                               +---------------+
|        |
|        |                               +---------------+
|        |--(E)----- Access Token ------>|    Resource   |
|        |                               |     Server    |
|        |<-(F)--- Protected Resource ---|               |
+--------+                               +---------------+

OAuth 2.0 Flows (Grant Types)#

OAuth 2.0 defines several flows, each designed for a different scenario. Choosing the right flow is critical for application security.

1. Authorization Code Flow#

This is the most secure and most commonly used flow, designed for server-side applications. The authorization code is exchanged for a token on the server side, so the token is never exposed in the browser.

// Step 1: Redirect user to the Authorization Server
const authUrl = new URL('https://auth.example.com/authorize');
authUrl.searchParams.set('response_type', 'code');
authUrl.searchParams.set('client_id', process.env.OAUTH_CLIENT_ID!);
authUrl.searchParams.set('redirect_uri', 'https://myapp.com/callback');
authUrl.searchParams.set('scope', 'openid profile email');
authUrl.searchParams.set('state', generateRandomState());

// Step 2: Handle callback - exchange code for token
// /api/auth/callback.ts
import { NextRequest, NextResponse } from 'next/server';

export async function GET(request: NextRequest) {
  const code = request.nextUrl.searchParams.get('code');
  const state = request.nextUrl.searchParams.get('state');

  // Validate state (CSRF protection)
  if (!validateState(state)) {
    return NextResponse.json({ error: 'Invalid state' }, { status: 400 });
  }

  // Exchange code for token
  const tokenResponse = await fetch('https://auth.example.com/token', {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: new URLSearchParams({
      grant_type: 'authorization_code',
      code: code!,
      redirect_uri: 'https://myapp.com/callback',
      client_id: process.env.OAUTH_CLIENT_ID!,
      client_secret: process.env.OAUTH_CLIENT_SECRET!,
    }),
  });

  const tokens = await tokenResponse.json();
  // tokens = { access_token, refresh_token, id_token, expires_in }

  // Store tokens in a secure session
  await createSession(tokens);

  return NextResponse.redirect('/dashboard');
}

2. Authorization Code Flow with PKCE#

PKCE (Proof Key for Code Exchange) is an extension to the Authorization Code Flow that adds an additional layer of security. It is mandatory for SPA and mobile applications, where the client_secret cannot be safely stored.

import crypto from 'crypto';

// Generate code_verifier and code_challenge
function generatePKCE() {
  const codeVerifier = crypto.randomBytes(32).toString('base64url');
  const codeChallenge = crypto
    .createHash('sha256')
    .update(codeVerifier)
    .digest('base64url');

  return { codeVerifier, codeChallenge };
}

// Step 1: Redirect with PKCE challenge
const { codeVerifier, codeChallenge } = generatePKCE();

// Store codeVerifier in session (httpOnly cookie or server-side storage)
cookies().set('pkce_verifier', codeVerifier, {
  httpOnly: true,
  secure: true,
  sameSite: 'lax',
  maxAge: 600, // 10 minutes
});

const authUrl = new URL('https://auth.example.com/authorize');
authUrl.searchParams.set('response_type', 'code');
authUrl.searchParams.set('client_id', process.env.OAUTH_CLIENT_ID!);
authUrl.searchParams.set('redirect_uri', 'https://myapp.com/callback');
authUrl.searchParams.set('scope', 'openid profile email');
authUrl.searchParams.set('code_challenge', codeChallenge);
authUrl.searchParams.set('code_challenge_method', 'S256');
authUrl.searchParams.set('state', generateRandomState());

// Step 2: Exchange code with code_verifier
const tokenResponse = await fetch('https://auth.example.com/token', {
  method: 'POST',
  headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
  body: new URLSearchParams({
    grant_type: 'authorization_code',
    code: authorizationCode,
    redirect_uri: 'https://myapp.com/callback',
    client_id: process.env.OAUTH_CLIENT_ID!,
    code_verifier: codeVerifier, // Instead of client_secret
  }),
});

3. Client Credentials Flow#

This flow is designed for service-to-service communication (M2M), where there is no user interaction. The application authorizes itself directly with its own credentials.

// Server-to-server communication
async function getM2MAccessToken(): Promise<string> {
  const response = await fetch('https://auth.example.com/token', {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: new URLSearchParams({
      grant_type: 'client_credentials',
      client_id: process.env.SERVICE_CLIENT_ID!,
      client_secret: process.env.SERVICE_CLIENT_SECRET!,
      scope: 'api:read api:write',
    }),
  });

  const data = await response.json();
  return data.access_token;
}

// Usage in a microservice
async function callInternalAPI() {
  const token = await getM2MAccessToken();

  const response = await fetch('https://internal-api.example.com/data', {
    headers: {
      Authorization: `Bearer ${token}`,
    },
  });

  return response.json();
}

4. Implicit Flow (Deprecated)#

The Implicit Flow was originally designed for SPA applications, but it is considered deprecated due to security concerns. The access token is returned directly in the URL fragment, exposing it to leakage. Instead, you should use Authorization Code Flow with PKCE.

// DEPRECATED - do not use in new projects!
// Token is visible in the URL:
https://myapp.com/callback#access_token=eyJhbGc...&token_type=bearer

OpenID Connect (OIDC) - The Identity Layer#

OpenID Connect is an identity layer built on top of OAuth 2.0. While OAuth 2.0 answers the question "what can you access?", OIDC answers the question "who are you?".

Key OIDC Extensions#

  • ID Token - a JWT containing user identity information
  • UserInfo Endpoint - an endpoint for retrieving profile data
  • Standard scopes - openid, profile, email, address, phone
  • Discovery - automatic provider configuration detection (.well-known/openid-configuration)
// Fetch OIDC Discovery configuration
async function getOIDCConfig(issuer: string) {
  const response = await fetch(
    `${issuer}/.well-known/openid-configuration`
  );
  return response.json();
}

// Example Discovery response
// {
//   "issuer": "https://auth.example.com",
//   "authorization_endpoint": "https://auth.example.com/authorize",
//   "token_endpoint": "https://auth.example.com/token",
//   "userinfo_endpoint": "https://auth.example.com/userinfo",
//   "jwks_uri": "https://auth.example.com/.well-known/jwks.json",
//   "scopes_supported": ["openid", "profile", "email"],
//   "response_types_supported": ["code", "id_token", "token id_token"]
// }

JWT Tokens - Access, Refresh, and ID Token#

JSON Web Tokens (JWT) are the standard token format in the OAuth 2.0 / OIDC ecosystem. They consist of three parts separated by dots: Header, Payload, and Signature.

Access Token#

The Access Token authorizes access to protected resources. It should have a short lifespan (typically 5-60 minutes).

// Example Access Token payload
{
  "iss": "https://auth.example.com",
  "sub": "user_123",
  "aud": "https://api.example.com",
  "exp": 1708300800,
  "iat": 1708297200,
  "scope": "openid profile email api:read",
  "client_id": "my-app"
}

Refresh Token#

The Refresh Token is used to renew access tokens without requiring user interaction again. It should be stored securely (e.g., in an httpOnly cookie on the server side).

// Token refresh implementation
async function refreshAccessToken(refreshToken: string) {
  const response = await fetch('https://auth.example.com/token', {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: new URLSearchParams({
      grant_type: 'refresh_token',
      refresh_token: refreshToken,
      client_id: process.env.OAUTH_CLIENT_ID!,
      client_secret: process.env.OAUTH_CLIENT_SECRET!,
    }),
  });

  if (!response.ok) {
    throw new Error('Token refresh failed - user must re-authenticate');
  }

  const tokens = await response.json();
  return {
    accessToken: tokens.access_token,
    refreshToken: tokens.refresh_token, // Rotation - new refresh token
    expiresAt: Date.now() + tokens.expires_in * 1000,
  };
}

ID Token#

The ID Token is a JWT specific to OpenID Connect that contains user identity information (claims).

// Example ID Token payload
{
  "iss": "https://auth.example.com",
  "sub": "user_123",
  "aud": "my-client-id",
  "exp": 1708300800,
  "iat": 1708297200,
  "auth_time": 1708297100,
  "nonce": "abc123",
  "name": "John Smith",
  "email": "john@example.com",
  "email_verified": true,
  "picture": "https://example.com/avatar.jpg"
}

Scopes and Claims#

Scopes define what type of access is being requested. Claims are specific pieces of information contained in tokens.

// Standard OIDC scopes and associated claims
const oidcScopes = {
  openid: ['sub'],                    // Required - user identifier
  profile: ['name', 'family_name', 'given_name', 'picture', 'locale'],
  email: ['email', 'email_verified'],
  address: ['address'],
  phone: ['phone_number', 'phone_number_verified'],
};

// Custom scopes (defined in the Authorization Server)
// scope: "api:read api:write admin"

// Requesting specific claims in an OIDC request
const claimsRequest = {
  id_token: {
    email: { essential: true },
    email_verified: { essential: true },
    given_name: null,     // requested but not required
  },
  userinfo: {
    picture: null,
    locale: null,
  },
};

JWT Token Validation#

Proper token validation is critical for security. Never trust a token without full verification.

import { jwtVerify, createRemoteJWKSet } from 'jose';

// Create JWKS client (caches public keys)
const JWKS = createRemoteJWKSet(
  new URL('https://auth.example.com/.well-known/jwks.json')
);

interface TokenPayload {
  sub: string;
  email: string;
  scope: string;
  iss: string;
  aud: string;
  exp: number;
}

async function validateAccessToken(token: string): Promise<TokenPayload> {
  try {
    const { payload } = await jwtVerify(token, JWKS, {
      issuer: 'https://auth.example.com',      // Verify issuer
      audience: 'https://api.example.com',       // Verify audience
      clockTolerance: 30,                         // 30s clock tolerance
    });

    return payload as unknown as TokenPayload;
  } catch (error) {
    if (error instanceof Error) {
      if (error.message.includes('expired')) {
        throw new Error('TOKEN_EXPIRED');
      }
      if (error.message.includes('signature')) {
        throw new Error('INVALID_SIGNATURE');
      }
    }
    throw new Error('TOKEN_VALIDATION_FAILED');
  }
}

// Validation middleware for Next.js API Routes
import { NextRequest, NextResponse } from 'next/server';

export async function authMiddleware(request: NextRequest) {
  const authHeader = request.headers.get('authorization');

  if (!authHeader?.startsWith('Bearer ')) {
    return NextResponse.json(
      { error: 'Missing authorization header' },
      { status: 401 }
    );
  }

  const token = authHeader.slice(7);

  try {
    const payload = await validateAccessToken(token);

    // Check required scopes
    const requiredScopes = ['api:read'];
    const tokenScopes = payload.scope.split(' ');
    const hasRequiredScopes = requiredScopes.every(s =>
      tokenScopes.includes(s)
    );

    if (!hasRequiredScopes) {
      return NextResponse.json(
        { error: 'Insufficient permissions' },
        { status: 403 }
      );
    }

    // Token is valid - continue
    const headers = new Headers(request.headers);
    headers.set('x-user-id', payload.sub);
    headers.set('x-user-email', payload.email);

    return NextResponse.next({ headers });
  } catch (error) {
    return NextResponse.json(
      { error: 'Invalid token' },
      { status: 401 }
    );
  }
}

Identity Providers#

Auth0#

Auth0 is one of the most popular IDaaS (Identity as a Service) providers. It offers ready-made SDKs and quick integration.

// Auth0 configuration with Next.js
// npm install @auth0/nextjs-auth0

// app/api/auth/[auth0]/route.ts
import { handleAuth, handleLogin, handleLogout } from '@auth0/nextjs-auth0';

export const GET = handleAuth({
  login: handleLogin({
    authorizationParams: {
      scope: 'openid profile email',
      audience: process.env.AUTH0_AUDIENCE,
    },
  }),
  logout: handleLogout({
    returnTo: process.env.AUTH0_BASE_URL,
  }),
});

// Usage in a Server Component
import { getSession } from '@auth0/nextjs-auth0';

export default async function DashboardPage() {
  const session = await getSession();

  if (!session) {
    redirect('/api/auth/login');
  }

  return (
    <div>
      <h1>Welcome, {session.user.name}</h1>
      <p>Email: {session.user.email}</p>
    </div>
  );
}

Keycloak#

Keycloak is an open-source identity management solution that can be hosted on your own infrastructure (self-hosted).

// Keycloak configuration with next-auth
import NextAuth from 'next-auth';
import KeycloakProvider from 'next-auth/providers/keycloak';

export const { handlers, auth, signIn, signOut } = NextAuth({
  providers: [
    KeycloakProvider({
      clientId: process.env.KEYCLOAK_CLIENT_ID!,
      clientSecret: process.env.KEYCLOAK_CLIENT_SECRET!,
      issuer: process.env.KEYCLOAK_ISSUER, // e.g. https://keycloak.example.com/realms/my-realm
    }),
  ],
  callbacks: {
    async jwt({ token, account }) {
      if (account) {
        token.accessToken = account.access_token;
        token.refreshToken = account.refresh_token;
        token.expiresAt = account.expires_at;
      }

      // Automatic token refresh
      if (Date.now() < (token.expiresAt as number) * 1000) {
        return token;
      }

      return refreshKeycloakToken(token);
    },
    async session({ session, token }) {
      session.accessToken = token.accessToken as string;
      return session;
    },
  },
});

Azure AD (Microsoft Entra ID)#

Azure AD is Microsoft's identity management solution, popular in enterprise environments.

// Azure AD configuration with MSAL
import { ConfidentialClientApplication } from '@azure/msal-node';

const msalConfig = {
  auth: {
    clientId: process.env.AZURE_AD_CLIENT_ID!,
    authority: `https://login.microsoftonline.com/${process.env.AZURE_AD_TENANT_ID}`,
    clientSecret: process.env.AZURE_AD_CLIENT_SECRET!,
  },
};

const msalClient = new ConfidentialClientApplication(msalConfig);

// Generate authorization URL
async function getAuthUrl() {
  return msalClient.getAuthCodeUrl({
    scopes: ['openid', 'profile', 'email', 'User.Read'],
    redirectUri: 'https://myapp.com/api/auth/callback',
    state: generateRandomState(),
  });
}

// Exchange code for token
async function handleCallback(code: string) {
  const result = await msalClient.acquireTokenByCode({
    code,
    scopes: ['openid', 'profile', 'email', 'User.Read'],
    redirectUri: 'https://myapp.com/api/auth/callback',
  });

  return {
    accessToken: result.accessToken,
    account: result.account,
    idTokenClaims: result.idTokenClaims,
  };
}

Implementation in Next.js with NextAuth.js#

NextAuth.js (Auth.js) is the most popular library for handling authentication in Next.js applications. It supports multiple providers and simplifies the entire process.

// auth.ts - central configuration
import NextAuth from 'next-auth';
import Google from 'next-auth/providers/google';
import GitHub from 'next-auth/providers/github';
import { PrismaAdapter } from '@auth/prisma-adapter';
import { prisma } from '@/lib/prisma';

export const { handlers, auth, signIn, signOut } = NextAuth({
  adapter: PrismaAdapter(prisma),
  session: { strategy: 'jwt' },
  providers: [
    Google({
      clientId: process.env.GOOGLE_CLIENT_ID!,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
      authorization: {
        params: {
          scope: 'openid email profile',
          access_type: 'offline',     // For refresh token
          prompt: 'consent',
        },
      },
    }),
    GitHub({
      clientId: process.env.GITHUB_CLIENT_ID!,
      clientSecret: process.env.GITHUB_CLIENT_SECRET!,
    }),
  ],
  callbacks: {
    async jwt({ token, user, account }) {
      if (user) {
        token.role = user.role;
        token.id = user.id;
      }
      if (account) {
        token.accessToken = account.access_token;
      }
      return token;
    },
    async session({ session, token }) {
      if (session.user) {
        session.user.role = token.role as string;
        session.user.id = token.id as string;
      }
      return session;
    },
    async authorized({ auth, request }) {
      const isLoggedIn = !!auth?.user;
      const isProtected = request.nextUrl.pathname.startsWith('/dashboard');

      if (isProtected && !isLoggedIn) {
        return Response.redirect(new URL('/login', request.nextUrl));
      }

      return true;
    },
  },
  pages: {
    signIn: '/login',
    error: '/auth/error',
  },
});

// middleware.ts
export { auth as middleware } from '@/auth';

export const config = {
  matcher: ['/dashboard/:path*', '/api/protected/:path*'],
};

Session Management#

Proper session management is just as important as authentication itself. Here are proven approaches.

// Secure session storage with encrypted cookies
import { SignJWT, jwtVerify } from 'jose';
import { cookies } from 'next/headers';

const secretKey = new TextEncoder().encode(process.env.SESSION_SECRET!);

interface SessionData {
  userId: string;
  email: string;
  role: string;
  accessToken: string;
  refreshToken: string;
  expiresAt: number;
}

// Create session
async function createSession(data: SessionData) {
  const session = await new SignJWT(data as unknown as Record<string, unknown>)
    .setProtectedHeader({ alg: 'HS256' })
    .setExpirationTime('7d')
    .setIssuedAt()
    .sign(secretKey);

  cookies().set('session', session, {
    httpOnly: true,           // Not accessible from JavaScript
    secure: true,             // HTTPS only
    sameSite: 'lax',          // CSRF protection
    maxAge: 60 * 60 * 24 * 7, // 7 days
    path: '/',
  });
}

// Read session
async function getSession(): Promise<SessionData | null> {
  const sessionCookie = cookies().get('session')?.value;
  if (!sessionCookie) return null;

  try {
    const { payload } = await jwtVerify(sessionCookie, secretKey);
    return payload as unknown as SessionData;
  } catch {
    return null;
  }
}

// Refresh session with automatic token renewal
async function refreshSession(): Promise<SessionData | null> {
  const session = await getSession();
  if (!session) return null;

  // Check if access token expires within 5 minutes
  if (session.expiresAt - Date.now() < 5 * 60 * 1000) {
    try {
      const newTokens = await refreshAccessToken(session.refreshToken);
      const updatedSession = {
        ...session,
        accessToken: newTokens.accessToken,
        refreshToken: newTokens.refreshToken,
        expiresAt: newTokens.expiresAt,
      };
      await createSession(updatedSession);
      return updatedSession;
    } catch {
      // Refresh token expired - log out user
      await destroySession();
      return null;
    }
  }

  return session;
}

// Destroy session
async function destroySession() {
  cookies().delete('session');
}

CSRF Protection#

Cross-Site Request Forgery (CSRF) is an attack where a malicious site performs unauthorized actions on behalf of a logged-in user. In the context of OAuth 2.0, CSRF protection is critical.

// CSRF protection with state parameter in OAuth flow
import crypto from 'crypto';
import { cookies } from 'next/headers';

function generateState(): string {
  const state = crypto.randomBytes(32).toString('hex');

  cookies().set('oauth_state', state, {
    httpOnly: true,
    secure: true,
    sameSite: 'lax',
    maxAge: 600, // 10 minutes
  });

  return state;
}

function validateState(receivedState: string | null): boolean {
  const storedState = cookies().get('oauth_state')?.value;
  cookies().delete('oauth_state'); // Single use

  if (!storedState || !receivedState) return false;

  // Comparison resistant to timing attacks
  return crypto.timingSafeEqual(
    Buffer.from(storedState),
    Buffer.from(receivedState)
  );
}

// Additional CSRF protection with Double Submit Cookie
function generateCSRFToken(): string {
  const token = crypto.randomBytes(32).toString('hex');

  cookies().set('csrf_token', token, {
    httpOnly: true,
    secure: true,
    sameSite: 'strict',
    maxAge: 3600,
  });

  return token; // Returned to the client as meta tag or hidden field
}

// Middleware validating CSRF token
export function validateCSRF(request: NextRequest): boolean {
  const cookieToken = request.cookies.get('csrf_token')?.value;
  const headerToken = request.headers.get('x-csrf-token');

  if (!cookieToken || !headerToken) return false;

  return crypto.timingSafeEqual(
    Buffer.from(cookieToken),
    Buffer.from(headerToken)
  );
}

Security Best Practices#

When implementing OAuth 2.0 and OIDC, follow these security guidelines:

1. Token Storage#

// GOOD - tokens in httpOnly cookies (server-side)
cookies().set('access_token', token, {
  httpOnly: true,    // No JS access
  secure: true,      // HTTPS only
  sameSite: 'strict', // CSRF protection
  maxAge: 900,       // 15 minutes
});

// BAD - tokens in localStorage (vulnerable to XSS)
// localStorage.setItem('access_token', token);

// BAD - tokens in regular cookies (accessible from JS)
// document.cookie = `access_token=${token}`;

2. Minimal Scopes#

// GOOD - request minimal permissions
const scopes = 'openid profile email';

// BAD - requesting everything "just in case"
// const scopes = 'openid profile email admin api:full';

3. Token Rotation#

// Implementing refresh token rotation
async function rotateTokens(refreshToken: string) {
  const response = await fetch('https://auth.example.com/token', {
    method: 'POST',
    body: new URLSearchParams({
      grant_type: 'refresh_token',
      refresh_token: refreshToken,
      client_id: process.env.OAUTH_CLIENT_ID!,
    }),
  });

  const tokens = await response.json();

  // The old refresh token is automatically invalidated
  // If the old token is reused,
  // the entire token family is invalidated (theft detection)
  return tokens;
}

4. Redirect URI Validation#

// Whitelist of allowed redirect_uris
const ALLOWED_REDIRECTS = [
  'https://myapp.com/callback',
  'https://myapp.com/auth/callback',
];

function validateRedirectUri(uri: string): boolean {
  return ALLOWED_REDIRECTS.includes(uri);
}

5. Secure Logout#

// Full logout - local + Identity Provider side
async function logout() {
  // 1. Remove local session
  await destroySession();

  // 2. Revoke refresh token at the Authorization Server
  await fetch('https://auth.example.com/revoke', {
    method: 'POST',
    body: new URLSearchParams({
      token: refreshToken,
      token_type_hint: 'refresh_token',
      client_id: process.env.OAUTH_CLIENT_ID!,
    }),
  });

  // 3. Redirect to RP-Initiated Logout (OIDC)
  const logoutUrl = new URL('https://auth.example.com/logout');
  logoutUrl.searchParams.set('id_token_hint', idToken);
  logoutUrl.searchParams.set(
    'post_logout_redirect_uri',
    'https://myapp.com'
  );

  redirect(logoutUrl.toString());
}

Summary#

OAuth 2.0 and OpenID Connect are powerful tools for building secure authentication systems. Key takeaways:

  • Use Authorization Code Flow with PKCE for web and mobile applications
  • Never use Implicit Flow in new projects
  • Store tokens securely - httpOnly cookies, never localStorage
  • Validate tokens fully - issuer, audience, signature, expiration
  • Implement refresh token rotation with theft detection
  • Protect against CSRF - state parameter, SameSite cookies
  • Apply the principle of least privilege - minimal scopes
  • Choose the right provider - Auth0 for quick start, Keycloak for full control, Azure AD for the Microsoft ecosystem

Proper authentication implementation is the foundation of every application's security. Regardless of the chosen solution, it is always worth investing time in understanding the protocols and their correct implementation.


Need help implementing secure authentication in your application? The MDS Software Solutions Group team specializes in designing and implementing authentication systems based on OAuth 2.0 and OpenID Connect. Whether you are building a new application or modernizing an existing system, get in touch with us - we will help you select and implement the optimal solution for your project.

Author
MDS Software Solutions Group

Team of programming experts specializing in modern web technologies.

OAuth2 and OpenID Connect - Modern Authentication | MDS Software Solutions Group | MDS Software Solutions Group