Przejdź do treści
Bezpieczeństwo

OAuth2 i OpenID Connect - Nowoczesna autentykacja

Opublikowano:
·4 min czytania·Autor: MDS Software Solutions Group

OAuth2 OpenID Connect

bezpieczenstwo

OAuth2 i OpenID Connect - Nowoczesna autentykacja

Bezpieczenstwo aplikacji webowych zaczyna sie od prawidlowej autentykacji i autoryzacji uzytkownikow. OAuth 2.0 i OpenID Connect to dwa wzajemnie uzupelniajace sie standardy, ktore staly sie fundamentem nowoczesnych systemow uwierzytelniania. W tym artykule doglebnie omowimy ich dzialanie, roznice, implementacje i najlepsze praktyki bezpieczenstwa.

Czym jest OAuth 2.0?#

OAuth 2.0 to protokol autoryzacji, ktory pozwala aplikacjom na uzyskanie ograniczonego dostepu do kont uzytkownikow w zewnetrznych serwisach. Kluczowe jest zrozumienie, ze OAuth 2.0 sam w sobie nie jest protokolem autentykacji - zajmuje sie wylacznie autoryzacja, czyli okreslaniem, do jakich zasobow aplikacja ma dostep.

Glowne role w OAuth 2.0#

  • Resource Owner - uzytkownik, ktory posiada chronione zasoby
  • Client - aplikacja zadajaca dostepu do zasobow
  • Authorization Server - serwer wydajacy tokeny dostepu
  • Resource Server - serwer hostujacy chronione zasoby
+--------+                               +---------------+
|        |--(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 definiuje kilka flow-ow, z ktorych kazdy jest przeznaczony do innego scenariusza. Wybor odpowiedniego flow ma kluczowe znaczenie dla bezpieczenstwa aplikacji.

1. Authorization Code Flow#

Jest to najbezpieczniejszy i najczesciej uzywany flow, przeznaczony dla aplikacji serwerowych (server-side). Kod autoryzacyjny jest wymieniany na token po stronie serwera, wiec token nigdy nie jest eksponowany w przegladarce.

// Krok 1: Przekierowanie uzytkownika do 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());

// Krok 2: Obsluga callback - wymiana kodu na 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');

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

  // Wymiana kodu na 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 }

  // Zapisanie tokenow w bezpiecznej sesji
  await createSession(tokens);

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

2. Authorization Code Flow z PKCE#

PKCE (Proof Key for Code Exchange) to rozszerzenie Authorization Code Flow, ktore dodaje dodatkowa warstwe bezpieczenstwa. Jest obowiazkowe dla aplikacji SPA i mobilnych, gdzie nie mozna bezpiecznie przechowywac client_secret.

import crypto from 'crypto';

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

  return { codeVerifier, codeChallenge };
}

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

// Zapisz codeVerifier w sesji (httpOnly cookie lub server-side storage)
cookies().set('pkce_verifier', codeVerifier, {
  httpOnly: true,
  secure: true,
  sameSite: 'lax',
  maxAge: 600, // 10 minut
});

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());

// Krok 2: Wymiana kodu z 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, // Zamiast client_secret
  }),
});

3. Client Credentials Flow#

Ten flow jest przeznaczony do komunikacji miedzy serwisami (M2M), gdzie nie ma interakcji uzytkownika. Aplikacja autoryzuje sie bezposrednio swoimi danymi uwierzytelniajacymi.

// Komunikacja server-to-server
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;
}

// Uzycie w mikroserwisie
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 (przestarzaly)#

Implicit Flow byl pierwotnie zaprojektowany dla aplikacji SPA, ale jest uznawany za przestarzaly ze wzgledu na problemy z bezpieczenstwem. Token dostepu jest zwracany bezposrednio w URL (fragment), co narazaja go na wyciek. Zamiast tego nalezy uzywac Authorization Code Flow z PKCE.

// PRZESTARZALE - nie uzywac w nowych projektach!
// Token jest widoczny w URL:
https://myapp.com/callback#access_token=eyJhbGc...&token_type=bearer

OpenID Connect (OIDC) - warstwa autentykacji#

OpenID Connect to warstwa tozsamosci zbudowana na OAuth 2.0. O ile OAuth 2.0 odpowiada na pytanie "do czego masz dostep?", OIDC odpowiada na pytanie "kim jestes?".

Kluczowe rozszerzenia OIDC#

  • ID Token - JWT zawierajacy informacje o tozsamosci uzytkownika
  • UserInfo Endpoint - endpoint do pobierania danych profilu
  • Standardowe scopes - openid, profile, email, address, phone
  • Discovery - automatyczne wykrywanie konfiguracji providera (.well-known/openid-configuration)
// Pobranie konfiguracji OIDC Discovery
async function getOIDCConfig(issuer: string) {
  const response = await fetch(
    `${issuer}/.well-known/openid-configuration`
  );
  return response.json();
}

// Przykladowa odpowiedz Discovery
// {
//   "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 Tokeny - Access, Refresh i ID Token#

JSON Web Tokens (JWT) sa standardowym formatem tokenow w ekosystemie OAuth 2.0 / OIDC. Skladaja sie z trzech czesci oddzielonych kropkami: Header, Payload i Signature.

Access Token#

Access Token autoryzuje dostep do chronionych zasobow. Powinien miec krotki czas zycia (typowo 5-60 minut).

// Przykladowy payload Access Token
{
  "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#

Refresh Token sluzy do odnawiania access tokenow bez ponownej interakcji uzytkownika. Powinien byc przechowywany bezpiecznie (np. w httpOnly cookie po stronie serwera).

// Odswiezanie tokenow
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 - nowy refresh token
    expiresAt: Date.now() + tokens.expires_in * 1000,
  };
}

ID Token#

ID Token to JWT specyficzny dla OpenID Connect, ktory zawiera informacje o tozsamosci uzytkownika (claims).

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

Scopes i Claims#

Scopes definiuja, jakiego rodzaju dostep jest zadany. Claims to konkretne informacje zawarte w tokenach.

// Standardowe OIDC scopes i powiazane claims
const oidcScopes = {
  openid: ['sub'],                    // Wymagany - identyfikator uzytkownika
  profile: ['name', 'family_name', 'given_name', 'picture', 'locale'],
  email: ['email', 'email_verified'],
  address: ['address'],
  phone: ['phone_number', 'phone_number_verified'],
};

// Customowe scopes (definiowane w Authorization Server)
// scope: "api:read api:write admin"

// Zadanie konkretnych claims w zapytaniu OIDC
const claimsRequest = {
  id_token: {
    email: { essential: true },
    email_verified: { essential: true },
    given_name: null,     // zadany, ale nie wymagany
  },
  userinfo: {
    picture: null,
    locale: null,
  },
};

Walidacja tokenow JWT#

Prawidlowa walidacja tokenow jest krytyczna dla bezpieczenstwa. Nigdy nie nalezy ufac tokenowi bez pelnej weryfikacji.

import { jwtVerify, createRemoteJWKSet } from 'jose';

// Tworzenie JWKS client (cache-uje klucze publiczne)
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',      // Weryfikacja issuer
      audience: 'https://api.example.com',       // Weryfikacja audience
      clockTolerance: 30,                         // 30s tolerancji zegara
    });

    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');
  }
}

// Middleware do walidacji w 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);

    // Sprawdzenie wymaganych 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 prawidlowy - kontynuuj
    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 }
    );
  }
}

Dostawcy tozsamosci (Identity Providers)#

Auth0#

Auth0 to jeden z najpopularniejszych dostawcow IDaaS (Identity as a Service). Oferuje gotowe SDK i szybka integracje.

// next.config.ts - konfiguracja Auth0 z 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,
  }),
});

// Uzycie w komponencie serwerowym
import { getSession } from '@auth0/nextjs-auth0';

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

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

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

Keycloak#

Keycloak to open-source'owe rozwiazanie do zarzadzania tozsamoscia, ktore mozna hostowac na wlasnej infrastrukturze (self-hosted).

// Konfiguracja Keycloak z 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, // np. 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;
      }

      // Automatyczne odswiezanie tokenu
      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 to rozwiazanie Microsoftu do zarzadzania tozsamoscia, popularne w srodowiskach korporacyjnych.

// Konfiguracja Azure AD z 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);

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

// Wymiana kodu na 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,
  };
}

Implementacja w Next.js z NextAuth.js#

NextAuth.js (Auth.js) to najpopularniejsza biblioteka do obslugi autentykacji w aplikacjach Next.js. Wspiera wielu providerow i upraszcza caly proces.

// auth.ts - centralna konfiguracja
import NextAuth from 'next-auth';
import Google from 'next-auth/providers/google';
import GitHub from 'next-auth/providers/github';
import Credentials from 'next-auth/providers/credentials';
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',     // Dla 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*'],
};

Zarzadzanie sesjami#

Prawidlowe zarzadzanie sesjami jest rownie wazne jak sama autentykacja. Oto sprawdzone podejscia.

// Bezpieczne przechowywanie sesji z szyfrowanymi 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;
}

// Tworzenie sesji
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,           // Niedostepny z JavaScript
    secure: true,             // Tylko HTTPS
    sameSite: 'lax',          // Ochrona CSRF
    maxAge: 60 * 60 * 24 * 7, // 7 dni
    path: '/',
  });
}

// Odczyt sesji
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;
  }
}

// Odswiezanie sesji z automatycznym odnowieniem tokenu
async function refreshSession(): Promise<SessionData | null> {
  const session = await getSession();
  if (!session) return null;

  // Sprawdz czy access token wygasa w ciagu 5 minut
  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 wygasl - wyloguj uzytkownika
      await destroySession();
      return null;
    }
  }

  return session;
}

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

Ochrona CSRF#

Cross-Site Request Forgery (CSRF) to atak, w ktorym zlosliwa strona wykonuje nieautoryzowane zadania w imieniu zalogowanego uzytkownika. W kontekscie OAuth 2.0 ochrona CSRF jest krytyczna.

// Ochrona CSRF z parametrem state w 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 minut
  });

  return state;
}

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

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

  // Porownanie odporne na timing attacks
  return crypto.timingSafeEqual(
    Buffer.from(storedState),
    Buffer.from(receivedState)
  );
}

// Dodatkowa ochrona CSRF z 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; // Zwracany do klienta jako meta tag lub hidden field
}

// Middleware walidujace 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)
  );
}

Najlepsze praktyki bezpieczenstwa#

Implementujac OAuth 2.0 i OIDC, nalezy przestrzegac nastepujacych zasad bezpieczenstwa:

1. Przechowywanie tokenow#

// DOBRZE - tokeny w httpOnly cookies (server-side)
cookies().set('access_token', token, {
  httpOnly: true,    // Brak dostepu z JS
  secure: true,      // Tylko HTTPS
  sameSite: 'strict', // Ochrona CSRF
  maxAge: 900,       // 15 minut
});

// ZLE - tokeny w localStorage (podatne na XSS)
// localStorage.setItem('access_token', token);

// ZLE - tokeny w zwyklych cookies (dostepne z JS)
// document.cookie = `access_token=${token}`;

2. Minimalne scopes#

// DOBRZE - zadaj minimalnych uprawnien
const scopes = 'openid profile email';

// ZLE - zadanie wszystkiego "na wszelki wypadek"
// const scopes = 'openid profile email admin api:full';

3. Token Rotation#

// Implementacja 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();

  // Stary refresh token jest automatycznie uniewazniany
  // Jezeli stary token zostanie uzyty ponownie,
  // cala rodzina tokenow jest uniewazniana (wykrywanie kradziezy)
  return tokens;
}

4. Walidacja redirect_uri#

// Whitelist dozwolonych redirect_uri
const ALLOWED_REDIRECTS = [
  'https://myapp.com/callback',
  'https://myapp.com/auth/callback',
];

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

5. Bezpieczne wylogowanie#

// Pelne wylogowanie - lokalne + po stronie Identity Provider
async function logout() {
  // 1. Usun lokalna sesje
  await destroySession();

  // 2. Uniewazn refresh token w 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. Przekieruj do 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());
}

Podsumowanie#

OAuth 2.0 i OpenID Connect to potezne narzedzia do budowania bezpiecznych systemow autentykacji. Kluczowe wnioski:

  • Uzywaj Authorization Code Flow z PKCE dla aplikacji webowych i mobilnych
  • Nigdy nie uzywaj Implicit Flow w nowych projektach
  • Przechowuj tokeny bezpiecznie - httpOnly cookies, nigdy localStorage
  • Waliduj tokeny pelni - issuer, audience, signature, expiration
  • Implementuj refresh token rotation z wykrywaniem kradziezy
  • Chron przed CSRF - parametr state, SameSite cookies
  • Stosuj zasade najmniejszych uprawnien - minimalne scopes
  • Wybierz odpowiedniego providera - Auth0 dla szybkiego startu, Keycloak dla pelnej kontroli, Azure AD dla ekosystemu Microsoft

Prawidlowa implementacja autentykacji to fundament bezpieczenstwa kazdej aplikacji. Niezaleznie od wybranego rozwiazania, zawsze warto inwestowac czas w zrozumienie protokolow i ich prawidlowe wdrozenie.


Potrzebujesz pomocy z implementacja bezpiecznej autentykacji w swojej aplikacji? Zespol MDS Software Solutions Group specjalizuje sie w projektowaniu i wdrazaniu systemow autentykacji opartych na OAuth 2.0 i OpenID Connect. Niezaleznie od tego, czy budujecie nowa aplikacje, czy modernizujecie istniejacy system, skontaktuj sie z nami - pomozemy dobrac i wdrozyc optymalne rozwiazanie dla Waszego projektu.

Autor
MDS Software Solutions Group

Zespół ekspertów programistycznych specjalizujących się w nowoczesnych technologiach webowych.

OAuth2 i OpenID Connect - Nowoczesna autentykacja | MDS Software Solutions Group | MDS Software Solutions Group