OAuth2 i OpenID Connect - Nowoczesna autentykacja
OAuth2 OpenID Connect
bezpieczenstwoOAuth2 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.
Zespół ekspertów programistycznych specjalizujących się w nowoczesnych technologiach webowych.