</>
APIGuiden.se
Kombinationslas pa ett datortangentbord symboliserar API-säkerhet

Foto av Sasun Bughdaryan via Unsplash

Säkerhet13 min2026-03-13

API-autentisering med OAuth 2.0: Från teori till produktion

Implementera OAuth 2.0 korrekt i ditt API. Authorization Code Flow med PKCE, token-rotation, scope-design och vanliga säkerhetsfällor att undvika.

OAuth 2.0PKCESäkerhetAutentisering

OAuth 2.0 i modern API-arkitektur

OAuth 2.0 har blivit de facto-standarden för API-auktorisering, men specifikationen är omfattande och implementationsmisstag skapar allvarliga säkerhetshål. Det räcker inte att förstå flödet på pappret — du behöver förstå varför varje steg existerar och vilka attacker det skyddar mot.

2026 innebär det i praktiken: Authorization Code Flow med PKCE för alla klienttyper (inte bara publika), korta access token-livstider, refresh token-rotation och strikta redirect URI-valideringar. Implicit Flow och Resource Owner Password Flow bör betraktas som deprecated.

Se även vår artikel om API-autentisering för en bredare överblick av autentiseringsmetoder.

Authorization Code Flow med PKCE

PKCE (Proof Key for Code Exchange) skyddar mot authorization code interception-attacker. Klienten genererar ett slumpmässigt code_verifier, hashar det till en code_challenge, och skickar challenge:en vid authorization-begäran. Vid token-utbytet skickas original-verifier:n, och auktoriseringsservern verifierar att de matchar.

Detta är obligatoriskt 2026 — även för konfidentiella klienter med client_secret. OAuth 2.1 (utkastet) kräver PKCE för alla flöden.

auth/oauth-flow.ts
typescript
// OAuth 2.0 + PKCE implementation
import crypto from "crypto";
import { Router } from "express";

const router = Router();
const AUTH_SERVER = "https://auth.example.com";
const stateStore = new Map<string, {
  verifier: string;
  returnTo: string;
}>();

// Steg 1: Initiera inloggning
router.get("/auth/login", (req, res) => {
  const verifier = crypto
    .randomBytes(32).toString("base64url");
  const challenge = crypto
    .createHash("sha256")
    .update(verifier)
    .digest("base64url");
  const state = crypto
    .randomBytes(16).toString("hex");

  stateStore.set(state, {
    verifier,
    returnTo: req.query.returnTo?.toString() || "/",
  });

  const url = new URL(
    AUTH_SERVER + "/oauth/authorize"
  );
  url.searchParams.set("response_type", "code");
  url.searchParams.set("client_id",
    process.env.OAUTH_CLIENT_ID!);
  url.searchParams.set("redirect_uri",
    process.env.OAUTH_REDIRECT_URI!);
  url.searchParams.set("scope",
    "openid profile email api:read");
  url.searchParams.set("state", state);
  url.searchParams.set("code_challenge", challenge);
  url.searchParams.set(
    "code_challenge_method", "S256");

  res.redirect(url.toString());
});

// Steg 2: Hantera callback
router.get("/auth/callback", async (req, res) => {
  const { code, state, error } = req.query;

  if (error) {
    return res.status(400).json({
      error: req.query.error_description || error,
    });
  }

  const stored = stateStore.get(state as string);
  if (!stored) {
    return res.status(400).json({
      error: "Ogiltig state",
    });
  }
  stateStore.delete(state as string);

  const tokenRes = await fetch(
    AUTH_SERVER + "/oauth/token", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        grant_type: "authorization_code",
        client_id: process.env.OAUTH_CLIENT_ID,
        client_secret: process.env.OAUTH_CLIENT_SECRET,
        code,
        redirect_uri: process.env.OAUTH_REDIRECT_URI,
        code_verifier: stored.verifier,
      }),
    });

  const tokens = await tokenRes.json();
  res.cookie("access_token", tokens.access_token, {
    httpOnly: true, secure: true,
    sameSite: "lax",
    maxAge: tokens.expires_in * 1000,
  });

  res.redirect(stored.returnTo);
});

Scope-design för granulär åtkomstkontroll

Scopes definierar vad en access token får göra. Dålig scope-design leder antingen till för breda tokens (säkerhetsrisk) eller för granulära scopes (användbarhetsproblem).

En praktisk tumregel: definiera scopes baserat på resurs + operation. Använd hierarkiska scopes för att förenkla. Till exempel kan 'products:write' implicit inkludera 'products:read'. Dokumentera scope-hierarkin tydligt i din OpenAPI-spec.

Undvik scope-explosion: 10-20 scopes täcker de flesta API:er. Om du har 100+ scopes har du troligen modellerat fel.

middleware/scopes.ts
typescript
// Hierarkisk scope-validering
const SCOPE_HIERARCHY: Record<string, string[]> = {
  "admin": [
    "products:write", "products:read",
    "orders:write", "orders:read",
    "users:manage",
  ],
  "products:write": ["products:read"],
  "orders:write": ["orders:read"],
};

function expandScopes(scopes: string[]): Set<string> {
  const expanded = new Set<string>();
  const queue = [...scopes];

  while (queue.length > 0) {
    const scope = queue.pop()!;
    if (expanded.has(scope)) continue;
    expanded.add(scope);
    const children = SCOPE_HIERARCHY[scope];
    if (children) queue.push(...children);
  }
  return expanded;
}

function requireScope(...required: string[]) {
  return (req: Request, res: Response, next: NextFunction) => {
    const tokenScopes = req.user?.scopes || [];
    const expanded = expandScopes(tokenScopes);
    const missing = required.filter(
      (s) => !expanded.has(s)
    );

    if (missing.length > 0) {
      return res.status(403).json({
        type: "/errors/insufficient-scope",
        title: "Otillräckliga rättigheter",
        status: 403,
        detail: "Saknade scopes: " + missing.join(", "),
      });
    }
    next();
  };
}

// Användning
router.get("/products",
  requireScope("products:read"), listProducts);
router.post("/products",
  requireScope("products:write"), createProduct);

Refresh token-rotation

Refresh tokens har lång livstid och är högriskvariabler. Token-rotation mitigerar risken: varje gång en refresh token används utfärdas en ny, och den gamla ogiltigförklaras.

Om en gammal refresh token används igen (replay-attack) vet du att den troligtvis stulits. Invalidera hela token-familjen omedelbart — tvinga användaren att autentisera sig på nytt.

Spara aldrig refresh tokens i localStorage eller sessionStorage. Använd httpOnly secure cookies med SameSite-attribut.

auth/refresh-rotation.ts
typescript
// Refresh token rotation med replay-detection
async function refreshTokens(
  token: string
): Promise<{ accessToken: string; refreshToken: string }> {
  const stored = await db.refreshTokens.findUnique({
    where: { tokenHash: hashToken(token) },
  });

  if (!stored) {
    throw new ApiError(401, "Ogiltig refresh token");
  }

  // Replay-detection
  if (stored.usedAt) {
    // Komprometterad familj — invalidera allt
    await db.refreshTokens.updateMany({
      where: { familyId: stored.familyId },
      data: { revokedAt: new Date() },
    });
    throw new ApiError(401,
      "Token komprometterad — logga in igen"
    );
  }

  await db.refreshTokens.update({
    where: { id: stored.id },
    data: { usedAt: new Date() },
  });

  // Nytt token-par i samma familj
  const newRefreshToken = crypto.randomUUID();
  await db.refreshTokens.create({
    data: {
      tokenHash: hashToken(newRefreshToken),
      userId: stored.userId,
      familyId: stored.familyId,
      expiresAt: new Date(
        Date.now() + 7 * 24 * 60 * 60 * 1000
      ),
    },
  });

  return {
    accessToken: generateAccessToken(stored.userId),
    refreshToken: newRefreshToken,
  };
}

Vanliga misstag att undvika

Redirect URI-validering: acceptera aldrig dynamiska redirect URI:er. Varje tillåten URI ska vara exakt registrerad — inga wildcards, inga query-parametrar. Öppna redirects är fortfarande en av de vanligaste OAuth-sårbarheterna.

Access token i URL: skicka aldrig tokens som query-parametrar. De hamnar i serverloggar, browser history och Referer-headers. Använd Authorization-headern.

Token-livstid: håll access tokens korta (15-60 minuter). Ett stulet access token med 24 timmars livstid ger angriparen en hel dag. Med 15 minuter begränsas skadan drastiskt.

Client-side token-lagring: localStorage är sårbart för XSS. httpOnly cookies är det säkraste alternativet för webbapplikationer.

För en djupare genomgång av rate limiting som säkerhetskomplement, läs vår guide om rate limiting och API-throttling.