Wie du Zugangstokens in deinem API-Service oder Backend validierst
Die Validierung von Zugangstokens (Access tokens) ist ein entscheidender Bestandteil der Durchsetzung der rollenbasierten Zugangskontrolle (RBAC) in Logto. Diese Anleitung führt dich durch die Überprüfung von von Logto ausgestellten JWTs in deinem Backend / deiner API, einschließlich der Prüfung von Signatur, Aussteller (Issuer), Zielgruppe (Audience), Ablauf, Berechtigungen (Scopes) und Organisationskontext.
Bevor du beginnst
- Diese Anleitung setzt voraus, dass du mit den RBAC-Konzepten von Logto vertraut bist.
- Wenn du API-Ressourcen schützt, wird vorausgesetzt, dass du die Anleitung Globale API-Ressourcen schützen durchlaufen hast.
- Wenn du In-App-Funktionen oder Workflows (Nicht-API-Berechtigungen) schützt, wird vorausgesetzt, dass du die Anleitung Organisations-(Nicht-API)-Berechtigungen schützen durchlaufen hast.
- Wenn du API-Ressourcen auf Organisationsebene schützt, wird vorausgesetzt, dass du die Anleitung API-Ressourcen auf Organisationsebene schützen durchlaufen hast.
Schritt 1: Konstanten und Hilfsfunktionen initialisieren
Definiere die notwendigen Konstanten und Hilfsfunktionen in deinem Code, um die Token-Extraktion und -Validierung zu handhaben. Eine gültige Anfrage muss einen Authorization
-Header in der Form Bearer <access_token>
enthalten.
- Node.js
- Python
- Go
- Java
- .NET
- PHP
- Ruby
- Rust
import { IncomingHttpHeaders } from 'http';
const JWKS_URI = 'https://your-tenant.logto.app/oidc/jwks';
const ISSUER = 'https://your-tenant.logto.app/oidc';
export class AuthInfo {
constructor(
public sub: string,
public clientId?: string,
public organizationId?: string,
public scopes: string[] = [],
public audience: string[] = []
) {}
}
export class AuthorizationError extends Error {
name = 'AuthorizationError';
constructor(
message: string,
public status = 403
) {
super(message);
}
}
export function extractBearerTokenFromHeaders({ authorization }: IncomingHttpHeaders): string {
const bearerPrefix = 'Bearer ';
if (!authorization) {
throw new AuthorizationError('Authorization-Header fehlt', 401);
}
if (!authorization.startsWith(bearerPrefix)) {
throw new AuthorizationError(`Authorization-Header muss mit "${bearerPrefix}" beginnen`, 401);
}
return authorization.slice(bearerPrefix.length);
}
JWKS_URI = 'https://your-tenant.logto.app/oidc/jwks'
ISSUER = 'https://your-tenant.logto.app/oidc'
class AuthInfo:
def __init__(self, sub: str, client_id: str = None, organization_id: str = None,
scopes: list = None, audience: list = None):
self.sub = sub
self.client_id = client_id
self.organization_id = organization_id
self.scopes = scopes or []
self.audience = audience or []
def to_dict(self):
return {
'sub': self.sub,
'client_id': self.client_id,
'organization_id': self.organization_id,
'scopes': self.scopes,
'audience': self.audience
}
class AuthorizationError(Exception):
def __init__(self, message: str, status: int = 403):
self.message = message
self.status = status
super().__init__(self.message)
def extract_bearer_token_from_headers(headers: dict) -> str:
"""
Extrahiere das Bearer-Token aus den HTTP-Headern.
Hinweis: FastAPI und Django REST Framework verfügen über eine eingebaute Token-Extraktion,
daher ist diese Funktion hauptsächlich für Flask und andere Frameworks gedacht.
"""
authorization = headers.get('authorization') or headers.get('Authorization')
if not authorization:
raise AuthorizationError('Authorization-Header fehlt', 401)
if not authorization.startswith('Bearer '):
raise AuthorizationError('Authorization-Header muss mit "Bearer " beginnen', 401)
return authorization[7:] # Entferne das Präfix 'Bearer '
package main
import (
"fmt"
"net/http"
"strings"
)
const (
JWKS_URI = "https://your-tenant.logto.app/oidc/jwks"
ISSUER = "https://your-tenant.logto.app/oidc"
)
type AuthorizationError struct {
Message string
Status int
}
func (e *AuthorizationError) Error() string {
return e.Message
}
func NewAuthorizationError(message string, status ...int) *AuthorizationError {
statusCode := http.StatusForbidden // Standardmäßig 403 Verboten
if len(status) > 0 {
statusCode = status[0]
}
return &AuthorizationError{
Message: message,
Status: statusCode,
}
}
func extractBearerTokenFromHeaders(r *http.Request) (string, error) {
const bearerPrefix = "Bearer "
authorization := r.Header.Get("Authorization")
if authorization == "" {
return "", NewAuthorizationError("Authorization-Header fehlt", http.StatusUnauthorized)
}
if !strings.HasPrefix(authorization, bearerPrefix) {
return "", NewAuthorizationError(fmt.Sprintf("Authorization-Header muss mit \"%s\" beginnen", bearerPrefix), http.StatusUnauthorized)
}
return strings.TrimPrefix(authorization, bearerPrefix), nil
}
public final class AuthConstants {
public static final String JWKS_URI = "https://your-tenant.logto.app/oidc/jwks";
public static final String ISSUER = "https://your-tenant.logto.app/oidc";
private AuthConstants() {
// Instanziierung verhindern
}
}
public class AuthorizationException extends RuntimeException {
private final int statusCode;
public AuthorizationException(String message) {
this(message, 403); // Standardmäßig 403 Verboten
}
public AuthorizationException(String message, int statusCode) {
super(message);
this.statusCode = statusCode;
}
public int getStatusCode() {
return statusCode;
}
}
namespace YourApiNamespace
{
public static class AuthConstants
{
public const string Issuer = "https://your-tenant.logto.app/oidc";
}
}
namespace YourApiNamespace.Exceptions
{
public class AuthorizationException : Exception
{
public int StatusCode { get; }
public AuthorizationException(string message, int statusCode = 403) : base(message)
{
StatusCode = statusCode;
}
}
}
<?php
class AuthConstants
{
public const JWKS_URI = 'https://your-tenant.logto.app/oidc/jwks';
public const ISSUER = 'https://your-tenant.logto.app/oidc';
}
<?php
class AuthInfo
{
public function __construct(
public readonly string $sub,
public readonly ?string $clientId = null,
public readonly ?string $organizationId = null,
public readonly array $scopes = [],
public readonly array $audience = []
) {}
public function toArray(): array
{
return [
'sub' => $this->sub,
'client_id' => $this->clientId,
'organization_id' => $this->organizationId,
'scopes' => $this->scopes,
'audience' => $this->audience,
];
}
}
<?php
class AuthorizationException extends Exception
{
public function __construct(
string $message,
public readonly int $statusCode = 403
) {
parent::__construct($message);
}
}
<?php
trait AuthHelpers
{
protected function extractBearerToken(array $headers): string
{
$authorization = $headers['authorization'][0] ?? $headers['Authorization'][0] ?? null;
if (!$authorization) {
throw new AuthorizationException('Autorisierungs-Header fehlt (Authorization header is missing)', 401);
}
if (!str_starts_with($authorization, 'Bearer ')) {
throw new AuthorizationException('Autorisierungs-Header muss mit "Bearer " beginnen (Authorization header must start with "Bearer ")', 401);
}
return substr($authorization, 7); // Entfernt das Präfix 'Bearer ' (Remove 'Bearer ' prefix)
}
}
module AuthConstants
JWKS_URI = 'https://your-tenant.logto.app/oidc/jwks'
ISSUER = 'https://your-tenant.logto.app/oidc'
end
class AuthInfo
attr_accessor :sub, :client_id, :organization_id, :scopes, :audience
def initialize(sub, client_id = nil, organization_id = nil, scopes = [], audience = [])
@sub = sub
@client_id = client_id
@organization_id = organization_id
@scopes = scopes
@audience = audience
end
def to_h
{
sub: @sub,
client_id: @client_id,
organization_id: @organization_id,
scopes: @scopes,
audience: @audience
}
end
end
class AuthorizationError < StandardError
attr_reader :status
def initialize(message, status = 403)
super(message)
@status = status
end
end
module AuthHelpers
def extract_bearer_token(request)
authorization = request.headers['Authorization']
raise AuthorizationError.new('Authorization header fehlt', 401) unless authorization
raise AuthorizationError.new('Authorization header muss mit "Bearer " beginnen', 401) unless authorization.start_with?('Bearer ')
authorization[7..-1] # Entferne das Präfix 'Bearer '
end
end
use serde::{Deserialize, Serialize};
use std::fmt;
pub const JWKS_URI: &str = "https://your-tenant.logto.app/oidc/jwks";
pub const ISSUER: &str = "https://your-tenant.logto.app/oidc";
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuthInfo {
pub sub: String,
pub client_id: Option<String>,
pub organization_id: Option<String>,
pub scopes: Vec<String>,
pub audience: Vec<String>,
}
impl AuthInfo {
pub fn new(
sub: String,
client_id: Option<String>,
organization_id: Option<String>,
scopes: Vec<String>,
audience: Vec<String>,
) -> Self {
Self {
sub,
client_id,
organization_id,
scopes,
audience,
}
}
}
#[derive(Debug)]
pub struct AuthorizationError {
pub message: String,
pub status_code: u16,
}
impl AuthorizationError {
pub fn new(message: impl Into<String>) -> Self {
Self {
message: message.into(),
status_code: 403,
}
}
pub fn with_status(message: impl Into<String>, status_code: u16) -> Self {
Self {
message: message.into(),
status_code,
}
}
}
impl fmt::Display for AuthorizationError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.message)
}
}
impl std::error::Error for AuthorizationError {}
pub fn extract_bearer_token(authorization: Option<&str>) -> Result<&str, AuthorizationError> {
let auth_header = authorization.ok_or_else(|| {
AuthorizationError::with_status("Authorization-Header fehlt", 401)
})?;
if !auth_header.starts_with("Bearer ") {
return Err(AuthorizationError::with_status(
"Authorization-Header muss mit \"Bearer \" beginnen",
401,
));
}
Ok(&auth_header[7..]) // Entferne das Präfix 'Bearer '
}
Schritt 2: Informationen zu deinem Logto-Tenant abrufen
Du benötigst die folgenden Werte, um von Logto ausgestellte Tokens zu validieren:
- JSON Web Key Set (JWKS) URI: Die URL zu den öffentlichen Schlüsseln von Logto, die zur Überprüfung der JWT-Signaturen verwendet werden.
- Aussteller (Issuer): Der erwartete Wert des Ausstellers (die OIDC-URL von Logto).
Finde zunächst den Endpunkt deines Logto-Tenants. Du findest ihn an verschiedenen Stellen:
- In der Logto-Konsole unter Einstellungen → Domains.
- In den Anwendungseinstellungen, die du in Logto konfiguriert hast, unter Einstellungen → Endpoints & Credentials.
Abruf vom OpenID Connect Discovery Endpoint
Diese Werte können vom OpenID Connect Discovery Endpoint von Logto abgerufen werden:
https://<your-logto-endpoint>/oidc/.well-known/openid-configuration
Hier ein Beispiel für eine Antwort (andere Felder zur Übersichtlichkeit ausgelassen):
{
"jwks_uri": "https://your-tenant.logto.app/oidc/jwks",
"issuer": "https://your-tenant.logto.app/oidc"
}
Im Code fest hinterlegen (nicht empfohlen)
Da Logto keine Anpassung der JWKS-URI oder des Issuers erlaubt, kannst du diese Werte in deinem Code fest hinterlegen. Für Produktionsanwendungen wird dies jedoch nicht empfohlen, da dies den Wartungsaufwand erhöht, falls sich Konfigurationen in Zukunft ändern.
- JWKS URI:
https://<your-logto-endpoint>/oidc/jwks
- Issuer:
https://<your-logto-endpoint>/oidc
Schritt 3: Das Token und die Berechtigungen validieren
Nachdem du das Token extrahiert und die OIDC-Konfiguration abgerufen hast, prüfe Folgendes:
- Signatur: Das JWT muss gültig und von Logto signiert sein (über JWKS).
- Aussteller (Issuer): Muss mit dem Aussteller deines Logto-Tenants übereinstimmen.
- Zielgruppe (Audience): Muss mit dem in Logto registrierten Ressourcenindikator der API oder dem Organisationskontext übereinstimmen, falls zutreffend.
- Ablauf: Das Token darf nicht abgelaufen sein.
- Berechtigungen (Scopes): Das Token muss die erforderlichen Berechtigungen (Scopes) für deine API / Aktion enthalten. Scopes sind durch Leerzeichen getrennte Zeichenfolgen im
scope
-Anspruch. - Organisationskontext: Wenn du API-Ressourcen auf Organisationsebene schützt, prüfe den Anspruch
organization_id
.
Siehe JSON Web Token, um mehr über die Struktur und Ansprüche von JWTs zu erfahren.
Was bei jedem Berechtigungsmodell zu prüfen ist
Die Ansprüche und Validierungsregeln unterscheiden sich je nach Berechtigungsmodell:
Berechtigungsmodell | Audience-Anspruch (aud ) | Organisations-Anspruch (organization_id ) | Zu prüfende Berechtigungen (scope ) |
---|---|---|---|
Globale API-Ressourcen | API-Ressourcenindikator | Nicht vorhanden | API-Ressourcen-Berechtigungen |
Organisation (Nicht-API) Berechtigungen | urn:logto:organization:<id> (Organisationskontext im aud -Anspruch) | Nicht vorhanden | Organisationsberechtigungen |
API-Ressourcen auf Organisationsebene | API-Ressourcenindikator | Organisations-ID (muss zur Anfrage passen) | API-Ressourcen-Berechtigungen |
Für Nicht-API-Organisationsberechtigungen wird der Organisationskontext durch den aud
-Anspruch
dargestellt (z. B. urn:logto:organization:abc123
). Der Anspruch organization_id
ist nur bei
Tokens für API-Ressourcen auf Organisationsebene vorhanden.
Validiere immer sowohl die Berechtigungen (Scopes) als auch den Kontext (Audience, Organisation) für sichere Multi-Tenant-APIs.
Die Validierungslogik hinzufügen
- Node.js
- Python
- Go
- Java
- .NET
- PHP
- Ruby
- Rust
Wir verwenden jose in diesem Beispiel, um das JWT zu validieren. Installiere es, falls du es noch nicht getan hast:
npm install jose
Oder verwende deinen bevorzugten Paketmanager (z. B. pnpm
oder yarn
).
Füge zunächst diese gemeinsamen Hilfsfunktionen hinzu, um die JWT-Validierung zu handhaben:
import { createRemoteJWKSet, jwtVerify, JWTPayload } from 'jose';
import { AuthInfo, AuthorizationError } from './auth-middleware.js';
const jwks = createRemoteJWKSet(new URL(JWKS_URI));
export async function validateJwtToken(token: string): Promise<JWTPayload> {
const { payload } = await jwtVerify(token, jwks, {
issuer: ISSUER,
});
verifyPayload(payload);
return payload;
}
export function createAuthInfo(payload: JWTPayload): AuthInfo {
const scopes = (payload.scope as string)?.split(' ') ?? [];
const audience = Array.isArray(payload.aud) ? payload.aud : payload.aud ? [payload.aud] : [];
return new AuthInfo(
payload.sub!,
payload.client_id as string,
payload.organization_id as string,
scopes,
audience
);
}
function verifyPayload(payload: JWTPayload): void {
// Implementiere hier deine Verifizierungslogik basierend auf dem Berechtigungsmodell
// Dies wird im Abschnitt zu den Berechtigungsmodellen unten gezeigt
}
Implementiere dann das Middleware, um das Zugangstoken zu überprüfen:
- Express.js
- Koa.js
- Fastify
- Hapi.js
- NestJS
import { Request, Response, NextFunction } from 'express';
import { extractBearerTokenFromHeaders, AuthorizationError } from './auth-middleware.js';
import { validateJwtToken, createAuthInfo } from './jwt-validator.js';
// Erweitere das Express Request-Interface um auth
declare global {
namespace Express {
interface Request {
auth?: AuthInfo;
}
}
}
export async function verifyAccessToken(req: Request, res: Response, next: NextFunction) {
try {
const token = extractBearerTokenFromHeaders(req.headers);
const payload = await validateJwtToken(token);
// Auth-Info im Request für generische Nutzung speichern
req.auth = createAuthInfo(payload);
next();
} catch (err: any) {
return res.status(err.status ?? 401).json({ error: err.message });
}
}
import { Context, Next } from 'koa';
import { extractBearerTokenFromHeaders, AuthorizationError } from './auth-middleware.js';
import { validateJwtToken, createAuthInfo } from './jwt-validator.js';
export async function koaVerifyAccessToken(ctx: Context, next: Next) {
try {
const token = extractBearerTokenFromHeaders(ctx.request.headers);
const payload = await validateJwtToken(token);
// Auth-Info im State für generische Nutzung speichern
ctx.state.auth = createAuthInfo(payload);
await next();
} catch (err: any) {
ctx.status = err.status ?? 401;
ctx.body = { error: err.message };
}
}
import { FastifyRequest, FastifyReply } from 'fastify';
import { extractBearerTokenFromHeaders, AuthorizationError } from './auth-middleware.js';
import { validateJwtToken, createAuthInfo } from './jwt-validator.js';
// Erweitere das Fastify Request-Interface um auth
declare module 'fastify' {
interface FastifyRequest {
auth?: AuthInfo;
}
}
export async function fastifyVerifyAccessToken(request: FastifyRequest, reply: FastifyReply) {
try {
const token = extractBearerTokenFromHeaders(request.headers);
const payload = await validateJwtToken(token);
// Auth-Info im Request für generische Nutzung speichern
request.auth = createAuthInfo(payload);
} catch (err: any) {
reply.code(err.status ?? 401).send({ error: err.message });
}
}
import { Request, ResponseToolkit } from '@hapi/hapi';
import { extractBearerTokenFromHeaders, AuthorizationError } from './auth-middleware.js';
import { validateJwtToken, createAuthInfo } from './jwt-validator.js';
export async function hapiVerifyAccessToken(request: Request, h: ResponseToolkit) {
try {
const token = extractBearerTokenFromHeaders(request.headers);
const payload = await validateJwtToken(token);
// Auth-Info in request.app für generische Nutzung speichern
request.app.auth = createAuthInfo(payload);
return h.continue;
} catch (err: any) {
return h
.response({ error: err.message })
.code(err.status ?? 401)
.takeover();
}
}
import {
Injectable,
CanActivate,
ExecutionContext,
UnauthorizedException,
ForbiddenException,
} from '@nestjs/common';
import { extractBearerTokenFromHeaders, AuthorizationError } from './auth-middleware.js';
import { validateJwtToken, createAuthInfo } from './jwt-validator.js';
@Injectable()
export class AccessTokenGuard implements CanActivate {
async canActivate(context: ExecutionContext): Promise<boolean> {
const req = context.switchToHttp().getRequest();
try {
const token = extractBearerTokenFromHeaders(req.headers);
const payload = await validateJwtToken(token);
// Auth-Info im Request für generische Nutzung speichern
req.auth = createAuthInfo(payload);
return true;
} catch (err: any) {
if (err.status === 401) throw new UnauthorizedException(err.message);
throw new ForbiddenException(err.message);
}
}
}
Implementiere gemäß deinem Berechtigungsmodell die entsprechende Verifizierungslogik in jwt-validator.ts
:
- Globale API-Ressourcen
- Organisations-(Nicht-API)-Berechtigungen
- Organisationsbezogene API-Ressourcen
function verifyPayload(payload: JWTPayload): void {
// Überprüfe, ob der Audience-Anspruch mit deinem API-Ressourcenindikator übereinstimmt
const audiences = Array.isArray(payload.aud) ? payload.aud : payload.aud ? [payload.aud] : [];
if (!audiences.includes('https://your-api-resource-indicator')) {
throw new AuthorizationError('Ungültige Zielgruppe (audience)');
}
// Überprüfe erforderliche Berechtigungen für globale API-Ressourcen
const requiredScopes = ['api:read', 'api:write']; // Ersetze durch deine tatsächlich erforderlichen Berechtigungen
const scopes = (payload.scope as string)?.split(' ') ?? [];
if (!requiredScopes.every((scope) => scopes.includes(scope))) {
throw new AuthorizationError('Unzureichende Berechtigung (scope)');
}
}
function verifyPayload(payload: JWTPayload): void {
// Überprüfe, ob der Audience-Anspruch dem Organisationsformat entspricht
const audiences = Array.isArray(payload.aud) ? payload.aud : payload.aud ? [payload.aud] : [];
const hasOrgAudience = audiences.some((aud) => aud.startsWith('urn:logto:organization:'));
if (!hasOrgAudience) {
throw new AuthorizationError('Ungültige Zielgruppe für Organisationsberechtigungen');
}
// Überprüfe, ob die Organisations-ID mit dem Kontext übereinstimmt (möglicherweise musst du dies aus dem Request-Kontext extrahieren)
const expectedOrgId = 'your-organization-id'; // Aus dem Request-Kontext extrahieren
const expectedAud = `urn:logto:organization:${expectedOrgId}`;
if (!audiences.includes(expectedAud)) {
throw new AuthorizationError('Organisation-ID stimmt nicht überein');
}
// Überprüfe erforderliche Organisationsberechtigungen
const requiredScopes = ['invite:users', 'manage:settings']; // Ersetze durch deine tatsächlich erforderlichen Berechtigungen
const scopes = (payload.scope as string)?.split(' ') ?? [];
if (!requiredScopes.every((scope) => scopes.includes(scope))) {
throw new AuthorizationError('Unzureichende Organisationsberechtigung (scope)');
}
}
function verifyPayload(payload: JWTPayload): void {
// Überprüfe, ob der Audience-Anspruch mit deinem API-Ressourcenindikator übereinstimmt
const audiences = Array.isArray(payload.aud) ? payload.aud : payload.aud ? [payload.aud] : [];
if (!audiences.includes('https://your-api-resource-indicator')) {
throw new AuthorizationError('Ungültige Zielgruppe für organisationsbezogene API-Ressourcen');
}
// Überprüfe, ob die Organisations-ID mit dem Kontext übereinstimmt (möglicherweise musst du dies aus dem Request-Kontext extrahieren)
const expectedOrgId = 'your-organization-id'; // Aus dem Request-Kontext extrahieren
const orgId = payload.organization_id as string;
if (expectedOrgId !== orgId) {
throw new AuthorizationError('Organisation-ID stimmt nicht überein');
}
// Überprüfe erforderliche Berechtigungen für organisationsbezogene API-Ressourcen
const requiredScopes = ['api:read', 'api:write']; // Ersetze durch deine tatsächlich erforderlichen Berechtigungen
const scopes = (payload.scope as string)?.split(' ') ?? [];
if (!requiredScopes.every((scope) => scopes.includes(scope))) {
throw new AuthorizationError('Unzureichende organisationsbezogene API-Berechtigungen (scope)');
}
}
Wir verwenden PyJWT, um JWTs zu validieren. Installiere es, falls du es noch nicht getan hast:
pip install pyjwt[crypto]
Füge zunächst diese gemeinsamen Hilfsfunktionen hinzu, um die JWT-Validierung zu handhaben:
import jwt
from jwt import PyJWKClient
from typing import Dict, Any
from auth_middleware import AuthInfo, AuthorizationError, JWKS_URI, ISSUER
jwks_client = PyJWKClient(JWKS_URI)
def validate_jwt_token(token: str) -> Dict[str, Any]:
"""JWT-Token validieren und Payload zurückgeben"""
try:
signing_key = jwks_client.get_signing_key_from_jwt(token)
payload = jwt.decode(
token,
signing_key.key,
algorithms=['RS256'],
issuer=ISSUER,
options={'verify_aud': False} # Die Zielgruppe wird manuell überprüft
)
verify_payload(payload)
return payload
except jwt.InvalidTokenError as e:
raise AuthorizationError(f'Ungültiges Token: {str(e)}', 401)
except Exception as e:
raise AuthorizationError(f'Token-Validierung fehlgeschlagen: {str(e)}', 401)
def create_auth_info(payload: Dict[str, Any]) -> AuthInfo:
"""AuthInfo aus JWT-Payload erstellen"""
scopes = payload.get('scope', '').split(' ') if payload.get('scope') else []
audience = payload.get('aud', [])
if isinstance(audience, str):
audience = [audience]
return AuthInfo(
sub=payload.get('sub'),
client_id=payload.get('client_id'),
organization_id=payload.get('organization_id'),
scopes=scopes,
audience=audience
)
def verify_payload(payload: Dict[str, Any]) -> None:
"""Payload basierend auf dem Berechtigungsmodell überprüfen"""
# Implementiere hier deine Überprüfungslogik basierend auf dem Berechtigungsmodell
# Dies wird im Abschnitt zu den Berechtigungsmodellen unten gezeigt
pass
Implementiere dann die Middleware, um das Zugangstoken zu überprüfen:
- FastAPI
- Flask
- Django
- Django REST Framework
from fastapi import HTTPException, Depends
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from jwt_validator import validate_jwt_token, create_auth_info
security = HTTPBearer()
async def verify_access_token(credentials: HTTPAuthorizationCredentials = Depends(security)) -> AuthInfo:
try:
token = credentials.credentials
payload = validate_jwt_token(token)
return create_auth_info(payload)
except AuthorizationError as e:
raise HTTPException(status_code=e.status, detail=str(e))
from functools import wraps
from flask import request, jsonify, g
from jwt_validator import validate_jwt_token, create_auth_info
def verify_access_token(f):
@wraps(f)
def decorated_function(*args, **kwargs):
try:
token = extract_bearer_token_from_headers(dict(request.headers))
payload = validate_jwt_token(token)
# Auth-Info im Flask-g-Objekt für generische Nutzung speichern
g.auth = create_auth_info(payload)
return f(*args, **kwargs)
except AuthorizationError as e:
return jsonify({'error': str(e)}), e.status
return decorated_function
from django.http import JsonResponse
from jwt_validator import validate_jwt_token, create_auth_info
def require_access_token(view_func):
def wrapper(request, *args, **kwargs):
try:
headers = {key.replace('HTTP_', '').replace('_', '-').lower(): value
for key, value in request.META.items() if key.startswith('HTTP_')}
token = extract_bearer_token_from_headers(headers)
payload = validate_jwt_token(token)
# Auth-Info an die Anfrage anhängen für generische Nutzung
request.auth = create_auth_info(payload)
return view_func(request, *args, **kwargs)
except AuthorizationError as e:
return JsonResponse({'error': str(e)}, status=e.status)
return wrapper
from rest_framework.authentication import TokenAuthentication
from rest_framework import exceptions
from jwt_validator import validate_jwt_token, create_auth_info
class AccessTokenAuthentication(TokenAuthentication):
keyword = 'Bearer' # Verwende 'Bearer' statt 'Token'
def authenticate_credentials(self, key):
"""
Authentifiziere das Token, indem es als JWT validiert wird.
"""
try:
payload = validate_jwt_token(key)
auth_info = create_auth_info(payload)
# Erstelle ein benutzerähnliches Objekt, das Auth-Info für generische Nutzung hält
user = type('User', (), {
'auth': auth_info,
'is_authenticated': True,
'is_anonymous': False,
'is_active': True,
})()
return (user, key)
except AuthorizationError as e:
if e.status == 401:
raise exceptions.AuthenticationFailed(str(e))
else: # 403
raise exceptions.PermissionDenied(str(e))
Implementiere gemäß deinem Berechtigungsmodell die entsprechende Überprüfungslogik in jwt_validator.py
:
- Globale API-Ressourcen
- Organisations-(Nicht-API-)Berechtigungen
- Organisationsbezogene API-Ressourcen
def verify_payload(payload: Dict[str, Any]) -> None:
"""Payload für globale API-Ressourcen überprüfen"""
# Überprüfe, ob der Audience-Claim mit deinem API-Ressourcenindikator übereinstimmt
audiences = payload.get('aud', [])
if isinstance(audiences, str):
audiences = [audiences]
if 'https://your-api-resource-indicator' not in audiences:
raise AuthorizationError('Ungültige Zielgruppe')
# Überprüfe erforderliche Berechtigungen für globale API-Ressourcen
required_scopes = ['api:read', 'api:write'] # Ersetze durch deine tatsächlich erforderlichen Berechtigungen
scopes = payload.get('scope', '').split(' ') if payload.get('scope') else []
if not all(scope in scopes for scope in required_scopes):
raise AuthorizationError('Unzureichende Berechtigung')
def verify_payload(payload: Dict[str, Any]) -> None:
"""Payload für Organisationsberechtigungen überprüfen"""
# Überprüfe, ob der Audience-Claim dem Organisationsformat entspricht
audiences = payload.get('aud', [])
if isinstance(audiences, str):
audiences = [audiences]
has_org_audience = any(aud.startswith('urn:logto:organization:') for aud in audiences)
if not has_org_audience:
raise AuthorizationError('Ungültige Zielgruppe für Organisationsberechtigungen')
# Überprüfe, ob die Organisations-ID mit dem Kontext übereinstimmt (möglicherweise musst du diese aus dem Anfragekontext extrahieren)
expected_org_id = 'your-organization-id' # Aus dem Anfragekontext extrahieren
expected_aud = f'urn:logto:organization:{expected_org_id}'
if expected_aud not in audiences:
raise AuthorizationError('Organisations-ID stimmt nicht überein')
# Überprüfe erforderliche Organisationsberechtigungen
required_scopes = ['invite:users', 'manage:settings'] # Ersetze durch deine tatsächlich erforderlichen Berechtigungen
scopes = payload.get('scope', '').split(' ') if payload.get('scope') else []
if not all(scope in scopes for scope in required_scopes):
raise AuthorizationError('Unzureichende Organisationsberechtigung')
def verify_payload(payload: Dict[str, Any]) -> None:
"""Payload für organisationsbezogene API-Ressourcen überprüfen"""
# Überprüfe, ob der Audience-Claim mit deinem API-Ressourcenindikator übereinstimmt
audiences = payload.get('aud', [])
if isinstance(audiences, str):
audiences = [audiences]
if 'https://your-api-resource-indicator' not in audiences:
raise AuthorizationError('Ungültige Zielgruppe für organisationsbezogene API-Ressourcen')
# Überprüfe, ob die Organisations-ID mit dem Kontext übereinstimmt (möglicherweise musst du diese aus dem Anfragekontext extrahieren)
expected_org_id = 'your-organization-id' # Aus dem Anfragekontext extrahieren
org_id = payload.get('organization_id')
if expected_org_id != org_id:
raise AuthorizationError('Organisations-ID stimmt nicht überein')
# Überprüfe erforderliche Berechtigungen für organisationsbezogene API-Ressourcen
required_scopes = ['api:read', 'api:write'] # Ersetze durch deine tatsächlich erforderlichen Berechtigungen
scopes = payload.get('scope', '').split(' ') if payload.get('scope') else []
if not all(scope in scopes for scope in required_scopes):
raise AuthorizationError('Unzureichende organisationsbezogene API-Berechtigungen')
Wir verwenden github.com/lestrrat-go/jwx, um JWTs zu validieren. Installiere es, falls du es noch nicht getan hast:
go mod init your-project
go get github.com/lestrrat-go/jwx/v3
Füge zunächst diese gemeinsamen Komponenten zu deiner Datei auth_middleware.go
hinzu:
import (
"context"
"strings"
"time"
"github.com/lestrrat-go/jwx/v3/jwk"
"github.com/lestrrat-go/jwx/v3/jwt"
)
var jwkSet jwk.Set
func init() {
// JWKS-Cache initialisieren
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
var err error
jwkSet, err = jwk.Fetch(ctx, JWKS_URI)
if err != nil {
panic("Failed to fetch JWKS: " + err.Error())
}
}
// validateJWTToken validiert das JWT und gibt das geparste Token zurück
func validateJWTToken(tokenString string) (jwt.Token, error) {
token, err := jwt.Parse([]byte(tokenString), jwt.WithKeySet(jwkSet))
if err != nil {
return nil, NewAuthorizationError("Invalid token: "+err.Error(), http.StatusUnauthorized)
}
// Aussteller überprüfen
if token.Issuer() != ISSUER {
return nil, NewAuthorizationError("Invalid issuer", http.StatusUnauthorized)
}
if err := verifyPayload(token); err != nil {
return nil, err
}
return token, nil
}
// Hilfsfunktionen zum Extrahieren von Token-Daten
func getStringClaim(token jwt.Token, key string) string {
if val, ok := token.Get(key); ok {
if str, ok := val.(string); ok {
return str
}
}
return ""
}
func getScopesFromToken(token jwt.Token) []string {
if val, ok := token.Get("scope"); ok {
if scope, ok := val.(string); ok && scope != "" {
return strings.Split(scope, " ")
}
}
return []string{}
}
func getAudienceFromToken(token jwt.Token) []string {
return token.Audience()
}
Implementiere anschließend das Middleware, um das Zugangstoken (Access token) zu überprüfen:
- Gin
- Echo
- Fiber
- Chi
import "github.com/gin-gonic/gin"
func VerifyAccessToken() gin.HandlerFunc {
return func(c *gin.Context) {
tokenString, err := extractBearerTokenFromHeaders(c.Request)
if err != nil {
authErr := err.(*AuthorizationError)
c.JSON(authErr.Status, gin.H{"error": authErr.Message})
c.Abort()
return
}
token, err := validateJWTToken(tokenString)
if err != nil {
authErr := err.(*AuthorizationError)
c.JSON(authErr.Status, gin.H{"error": authErr.Message})
c.Abort()
return
}
// Token im Kontext für generische Nutzung speichern
c.Set("auth", token)
c.Next()
}
}
import "github.com/labstack/echo/v4"
func VerifyAccessToken(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
tokenString, err := extractBearerTokenFromHeaders(c.Request())
if err != nil {
authErr := err.(*AuthorizationError)
return c.JSON(authErr.Status, echo.Map{"error": authErr.Message})
}
token, err := validateJWTToken(tokenString)
if err != nil {
authErr := err.(*AuthorizationError)
return c.JSON(authErr.Status, echo.Map{"error": authErr.Message})
}
// Token im Kontext für generische Nutzung speichern
c.Set("auth", token)
return next(c)
}
}
import (
"net/http"
"github.com/gofiber/fiber/v2"
)
func VerifyAccessToken(c *fiber.Ctx) error {
// Fiber-Request zu http.Request konvertieren für Kompatibilität
req := &http.Request{
Header: make(http.Header),
}
req.Header.Set("Authorization", c.Get("Authorization"))
tokenString, err := extractBearerTokenFromHeaders(req)
if err != nil {
authErr := err.(*AuthorizationError)
return c.Status(authErr.Status).JSON(fiber.Map{"error": authErr.Message})
}
token, err := validateJWTToken(tokenString)
if err != nil {
authErr := err.(*AuthorizationError)
return c.Status(authErr.Status).JSON(fiber.Map{"error": authErr.Message})
}
// Token in locals für generische Nutzung speichern
c.Locals("auth", token)
return c.Next()
}
import (
"context"
"encoding/json"
"net/http"
)
type contextKey string
const AuthContextKey contextKey = "auth"
func VerifyAccessToken(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
tokenString, err := extractBearerTokenFromHeaders(r)
if err != nil {
authErr := err.(*AuthorizationError)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(authErr.Status)
json.NewEncoder(w).Encode(map[string]string{"error": authErr.Message})
return
}
token, err := validateJWTToken(tokenString)
if err != nil {
authErr := err.(*AuthorizationError)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(authErr.Status)
json.NewEncoder(w).Encode(map[string]string{"error": authErr.Message})
return
}
// Token im Kontext für generische Nutzung speichern
ctx := context.WithValue(r.Context(), AuthContextKey, token)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
Je nach deinem Berechtigungsmodell musst du eventuell unterschiedliche verifyPayload
-Logik anwenden:
- Globale API-Ressourcen
- Organisations-(Nicht-API)-Berechtigungen
- Organisationsbezogene API-Ressourcen
func verifyPayload(token jwt.Token) error {
// Überprüfen, ob der Audience-Claim mit deinem API-Ressourcenindikator übereinstimmt
if !hasAudience(token, "https://your-api-resource-indicator") {
return NewAuthorizationError("Invalid audience")
}
// Erforderliche Berechtigungen (Scopes) für globale API-Ressourcen prüfen
requiredScopes := []string{"api:read", "api:write"} // Ersetze durch deine tatsächlichen erforderlichen Scopes
if !hasRequiredScopes(token, requiredScopes) {
return NewAuthorizationError("Insufficient scope")
}
return nil
}
func verifyPayload(token jwt.Token) error {
// Überprüfen, ob der Audience-Claim das Organisationsformat hat
if !hasOrganizationAudience(token) {
return NewAuthorizationError("Invalid audience for organization permissions")
}
// Überprüfen, ob die Organisations-ID mit dem Kontext übereinstimmt (möglicherweise musst du dies aus dem Request-Kontext extrahieren)
expectedOrgID := "your-organization-id" // Aus dem Request-Kontext extrahieren
if !hasMatchingOrganization(token, expectedOrgID) {
return NewAuthorizationError("Organization ID mismatch")
}
// Erforderliche Organisations-Scopes prüfen
requiredScopes := []string{"invite:users", "manage:settings"} // Ersetze durch deine tatsächlichen erforderlichen Scopes
if !hasRequiredScopes(token, requiredScopes) {
return NewAuthorizationError("Insufficient organization scope")
}
return nil
}
func verifyPayload(token jwt.Token) error {
// Überprüfen, ob der Audience-Claim mit deinem API-Ressourcenindikator übereinstimmt
if !hasAudience(token, "https://your-api-resource-indicator") {
return NewAuthorizationError("Invalid audience for organization-level API resources")
}
// Überprüfen, ob die Organisations-ID mit dem Kontext übereinstimmt (möglicherweise musst du dies aus dem Request-Kontext extrahieren)
expectedOrgID := "your-organization-id" // Aus dem Request-Kontext extrahieren
if !hasMatchingOrganizationID(token, expectedOrgID) {
return NewAuthorizationError("Organization ID mismatch")
}
// Erforderliche Scopes für organisationsbezogene API-Ressourcen prüfen
requiredScopes := []string{"api:read", "api:write"} // Ersetze durch deine tatsächlichen erforderlichen Scopes
if !hasRequiredScopes(token, requiredScopes) {
return NewAuthorizationError("Insufficient organization-level API scopes")
}
return nil
}
Füge diese Hilfsfunktionen zur Payload-Überprüfung hinzu:
// hasAudience prüft, ob das Token die angegebene Zielgruppe (Audience) enthält
func hasAudience(token jwt.Token, expectedAud string) bool {
audiences := token.Audience()
for _, aud := range audiences {
if aud == expectedAud {
return true
}
}
return false
}
// hasOrganizationAudience prüft, ob das Token eine Organisations-Audience hat
func hasOrganizationAudience(token jwt.Token) bool {
audiences := token.Audience()
for _, aud := range audiences {
if strings.HasPrefix(aud, "urn:logto:organization:") {
return true
}
}
return false
}
// hasRequiredScopes prüft, ob das Token alle erforderlichen Berechtigungen (Scopes) enthält
func hasRequiredScopes(token jwt.Token, requiredScopes []string) bool {
scopes := getScopesFromToken(token)
for _, required := range requiredScopes {
found := false
for _, scope := range scopes {
if scope == required {
found = true
break
}
}
if !found {
return false
}
}
return true
}
// hasMatchingOrganization prüft, ob die Audience des Tokens mit der erwarteten Organisation übereinstimmt
func hasMatchingOrganization(token jwt.Token, expectedOrgID string) bool {
expectedAud := fmt.Sprintf("urn:logto:organization:%s", expectedOrgID)
return hasAudience(token, expectedAud)
}
// hasMatchingOrganizationID prüft, ob die organization_id des Tokens mit der erwarteten übereinstimmt
func hasMatchingOrganizationID(token jwt.Token, expectedOrgID string) bool {
orgID := getStringClaim(token, "organization_id")
return orgID == expectedOrgID
}
Wir verwenden je nach Framework unterschiedliche JWT-Bibliotheken. Installiere die erforderlichen Abhängigkeiten:
- Spring Boot
- Quarkus
- Micronaut
- Vert.x Web
Füge Folgendes zu deiner pom.xml
hinzu:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-resource-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-jose</artifactId>
</dependency>
Füge Folgendes zu deiner pom.xml
hinzu:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-smallrye-jwt</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-resteasy-reactive</artifactId>
</dependency>
Füge Folgendes zu deiner pom.xml
hinzu:
<dependency>
<groupId>io.micronaut.security</groupId>
<artifactId>micronaut-security-jwt</artifactId>
</dependency>
<dependency>
<groupId>io.micronaut</groupId>
<artifactId>micronaut-http-server-netty</artifactId>
</dependency>
Füge Folgendes zu deiner pom.xml
hinzu:
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-web</artifactId>
</dependency>
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-auth-jwt</artifactId>
</dependency>
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-web-client</artifactId>
</dependency>
Implementiere anschließend die Middleware, um das Zugangstoken (Access token) zu überprüfen:
- Spring Boot
- Quarkus
- Micronaut
- Vert.x Web
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
public class JwtSecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authz -> authz
.requestMatchers("/api/protected/**").authenticated()
.anyRequest().permitAll()
)
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt.decoder(jwtDecoder()))
);
return http.build();
}
@Bean
public JwtDecoder jwtDecoder() {
return NimbusJwtDecoder.withJwkSetUri(AuthConstants.JWKS_URI)
.build();
}
}
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.stereotype.Component;
import java.util.Arrays;
import java.util.List;
@Component
public class JwtValidator {
public void verifyPayload(Jwt jwt) {
// Implementiere hier deine Überprüfungslogik basierend auf dem Berechtigungsmodell
// Dies wird im Abschnitt zu den Berechtigungsmodellen unten gezeigt
}
}
import org.eclipse.microprofile.jwt.JsonWebToken;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.ws.rs.container.ContainerRequestContext;
import jakarta.ws.rs.container.ContainerRequestFilter;
import jakarta.ws.rs.ext.Provider;
@Provider
@ApplicationScoped
public class JwtVerificationFilter implements ContainerRequestFilter {
@Inject
JsonWebToken jwt;
@Override
public void filter(ContainerRequestContext requestContext) {
if (requestContext.getUriInfo().getPath().startsWith("/api/protected")) {
try {
verifyPayload(jwt);
// JWT im Kontext speichern, um in Controllern darauf zuzugreifen
requestContext.setProperty("auth", jwt);
} catch (AuthorizationException e) {
requestContext.abortWith(
jakarta.ws.rs.core.Response.status(e.getStatusCode())
.entity("{\"error\": \"" + e.getMessage() + "\"}")
.build()
);
} catch (Exception e) {
requestContext.abortWith(
jakarta.ws.rs.core.Response.status(401)
.entity("{\"error\": \"Invalid token\"}")
.build()
);
}
}
}
private void verifyPayload(JsonWebToken jwt) {
// Implementiere hier deine Überprüfungslogik basierend auf dem Berechtigungsmodell
// Dies wird im Abschnitt zu den Berechtigungsmodellen unten gezeigt
}
}
micronaut:
security:
authentication: bearer
token:
jwt:
signatures:
jwks:
logto:
url: ${JWKS_URI:https://your-tenant.logto.app/oidc/jwks}
import io.micronaut.security.token.Claims;
import io.micronaut.security.token.validator.TokenValidator;
import jakarta.inject.Singleton;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Mono;
@Singleton
public class JwtClaimsValidator implements TokenValidator {
@Override
public Publisher<Boolean> validateToken(String token, Claims claims) {
return Mono.fromCallable(() -> {
try {
verifyPayload(claims);
return true;
} catch (AuthorizationException e) {
throw new RuntimeException(e.getMessage());
}
});
}
private void verifyPayload(Claims claims) {
// Implementiere hier deine Überprüfungslogik basierend auf dem Berechtigungsmodell
// Dies wird im Abschnitt zu den Berechtigungsmodellen unten gezeigt
}
}
import io.vertx.core.Future;
import io.vertx.core.Handler;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.auth.jwt.JWTAuth;
import io.vertx.ext.auth.jwt.JWTAuthOptions;
import io.vertx.ext.web.RoutingContext;
import io.vertx.ext.web.client.WebClient;
public class JwtAuthHandler implements Handler<RoutingContext> {
private final JWTAuth jwtAuth;
private final WebClient webClient;
public JwtAuthHandler(io.vertx.core.Vertx vertx) {
this.webClient = WebClient.create(vertx);
this.jwtAuth = JWTAuth.create(vertx, new JWTAuthOptions());
// JWKS abrufen und JWT-Auth konfigurieren
fetchJWKS().onSuccess(jwks -> {
// JWKS konfigurieren (vereinfacht – ggf. ist ein richtiger JWKS-Parser nötig)
});
}
@Override
public void handle(RoutingContext context) {
String authHeader = context.request().getHeader("Authorization");
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
context.response()
.setStatusCode(401)
.putHeader("Content-Type", "application/json")
.end("{\"error\": \"Authorization header missing or invalid\"}");
return;
}
String token = authHeader.substring(7);
jwtAuth.authenticate(new JsonObject().put("token", token))
.onSuccess(user -> {
try {
JsonObject principal = user.principal();
verifyPayload(principal);
// JWT-Principal im Kontext speichern, um in Handlern darauf zuzugreifen
context.put("auth", principal);
context.next();
} catch (AuthorizationException e) {
context.response()
.setStatusCode(e.getStatusCode())
.putHeader("Content-Type", "application/json")
.end("{\"error\": \"" + e.getMessage() + "\"}");
} catch (Exception e) {
context.response()
.setStatusCode(401)
.putHeader("Content-Type", "application/json")
.end("{\"error\": \"Invalid token\"}");
}
})
.onFailure(err -> {
context.response()
.setStatusCode(401)
.putHeader("Content-Type", "application/json")
.end("{\"error\": \"Invalid token\"}");
});
}
private Future<JsonObject> fetchJWKS() {
return webClient.getAbs(AuthConstants.JWKS_URI)
.send()
.map(response -> response.bodyAsJsonObject());
}
private void verifyPayload(JsonObject principal) {
// Implementiere hier deine Überprüfungslogik basierend auf dem Berechtigungsmodell
// Dies wird im Abschnitt zu den Berechtigungsmodellen unten gezeigt
}
}
Je nach deinem Berechtigungsmodell musst du ggf. unterschiedliche Überprüfungslogik anwenden:
- Globale API-Ressourcen
- Organisation (nicht-API) Berechtigungen
- Organisationsspezifische API-Ressourcen
- Spring Boot
- Quarkus
- Micronaut
- Vert.x Web
private void verifyPayload(Jwt jwt) {
// Überprüfe, ob der Audience-Claim mit deinem API-Ressourcenindikator übereinstimmt
List<String> audiences = jwt.getAudience();
if (!audiences.contains("https://your-api-resource-indicator")) {
throw new AuthorizationException("Invalid audience");
}
// Überprüfe erforderliche Berechtigungen (Scopes) für globale API-Ressourcen
List<String> requiredScopes = Arrays.asList("api:read", "api:write"); // Ersetze durch deine tatsächlichen erforderlichen Scopes
String scopes = jwt.getClaimAsString("scope");
List<String> tokenScopes = scopes != null ? Arrays.asList(scopes.split(" ")) : List.of();
if (!tokenScopes.containsAll(requiredScopes)) {
throw new AuthorizationException("Insufficient scope");
}
}
private void verifyPayload(JsonWebToken jwt) {
// Überprüfe, ob der Audience-Claim mit deinem API-Ressourcenindikator übereinstimmt
if (!jwt.getAudience().contains("https://your-api-resource-indicator")) {
throw new AuthorizationException("Invalid audience");
}
// Überprüfe erforderliche Berechtigungen (Scopes) für globale API-Ressourcen
List<String> requiredScopes = Arrays.asList("api:read", "api:write"); // Ersetze durch deine tatsächlichen erforderlichen Scopes
String scopes = jwt.getClaim("scope");
List<String> tokenScopes = scopes != null ? Arrays.asList(scopes.split(" ")) : List.of();
if (!tokenScopes.containsAll(requiredScopes)) {
throw new AuthorizationException("Insufficient scope");
}
}
private void verifyPayload(Claims claims) {
// Überprüfe, ob der Audience-Claim mit deinem API-Ressourcenindikator übereinstimmt
List<String> audiences = (List<String>) claims.get("aud");
if (audiences == null || !audiences.contains("https://your-api-resource-indicator")) {
throw new AuthorizationException("Invalid audience");
}
// Überprüfe erforderliche Berechtigungen (Scopes) für globale API-Ressourcen
List<String> requiredScopes = Arrays.asList("api:read", "api:write"); // Ersetze durch deine tatsächlichen erforderlichen Scopes
String scopes = (String) claims.get("scope");
List<String> tokenScopes = scopes != null ? Arrays.asList(scopes.split(" ")) : List.of();
if (!tokenScopes.containsAll(requiredScopes)) {
throw new AuthorizationException("Insufficient scope");
}
}
private void verifyPayload(JsonObject principal) {
// Überprüfe, ob der Audience-Claim mit deinem API-Ressourcenindikator übereinstimmt
JsonArray audiences = principal.getJsonArray("aud");
if (audiences == null || !audiences.contains("https://your-api-resource-indicator")) {
throw new AuthorizationException("Invalid audience");
}
// Überprüfe erforderliche Berechtigungen (Scopes) für globale API-Ressourcen
List<String> requiredScopes = Arrays.asList("api:read", "api:write"); // Ersetze durch deine tatsächlichen erforderlichen Scopes
String scopes = principal.getString("scope");
List<String> tokenScopes = scopes != null ? Arrays.asList(scopes.split(" ")) : List.of();
if (!tokenScopes.containsAll(requiredScopes)) {
throw new AuthorizationException("Insufficient scope");
}
}
- Spring Boot
- Quarkus
- Micronaut
- Vert.x Web
private void verifyPayload(Jwt jwt) {
// Überprüfe, ob der Audience-Claim das Organisationsformat hat
List<String> audiences = jwt.getAudience();
boolean hasOrgAudience = audiences.stream()
.anyMatch(aud -> aud.startsWith("urn:logto:organization:"));
if (!hasOrgAudience) {
throw new AuthorizationException("Invalid audience for organization permissions");
}
// Überprüfe, ob die Organisations-ID mit dem Kontext übereinstimmt (ggf. aus dem Request-Kontext extrahieren)
String expectedOrgId = "your-organization-id"; // Aus dem Request-Kontext extrahieren
String expectedAud = "urn:logto:organization:" + expectedOrgId;
if (!audiences.contains(expectedAud)) {
throw new AuthorizationException("Organization ID mismatch");
}
// Überprüfe erforderliche Organisations-Berechtigungen (Scopes)
List<String> requiredScopes = Arrays.asList("invite:users", "manage:settings"); // Ersetze durch deine tatsächlichen erforderlichen Scopes
String scopes = jwt.getClaimAsString("scope");
List<String> tokenScopes = scopes != null ? Arrays.asList(scopes.split(" ")) : List.of();
if (!tokenScopes.containsAll(requiredScopes)) {
throw new AuthorizationException("Insufficient organization scope");
}
}
private void verifyPayload(JsonWebToken jwt) {
// Überprüfe, ob der Audience-Claim das Organisationsformat hat
boolean hasOrgAudience = jwt.getAudience().stream()
.anyMatch(aud -> aud.startsWith("urn:logto:organization:"));
if (!hasOrgAudience) {
throw new AuthorizationException("Invalid audience for organization permissions");
}
// Überprüfe, ob die Organisations-ID mit dem Kontext übereinstimmt (ggf. aus dem Request-Kontext extrahieren)
String expectedOrgId = "your-organization-id"; // Aus dem Request-Kontext extrahieren
String expectedAud = "urn:logto:organization:" + expectedOrgId;
if (!jwt.getAudience().contains(expectedAud)) {
throw new AuthorizationException("Organization ID mismatch");
}
// Überprüfe erforderliche Organisations-Berechtigungen (Scopes)
List<String> requiredScopes = Arrays.asList("invite:users", "manage:settings"); // Ersetze durch deine tatsächlichen erforderlichen Scopes
String scopes = jwt.getClaim("scope");
List<String> tokenScopes = scopes != null ? Arrays.asList(scopes.split(" ")) : List.of();
if (!tokenScopes.containsAll(requiredScopes)) {
throw new AuthorizationException("Insufficient organization scope");
}
}
private void verifyPayload(Claims claims) {
// Überprüfe, ob der Audience-Claim das Organisationsformat hat
List<String> audiences = (List<String>) claims.get("aud");
boolean hasOrgAudience = audiences != null && audiences.stream()
.anyMatch(aud -> aud.startsWith("urn:logto:organization:"));
if (!hasOrgAudience) {
throw new AuthorizationException("Invalid audience for organization permissions");
}
// Überprüfe, ob die Organisations-ID mit dem Kontext übereinstimmt (ggf. aus dem Request-Kontext extrahieren)
String expectedOrgId = "your-organization-id"; // Aus dem Request-Kontext extrahieren
String expectedAud = "urn:logto:organization:" + expectedOrgId;
if (!audiences.contains(expectedAud)) {
throw new AuthorizationException("Organization ID mismatch");
}
// Überprüfe erforderliche Organisations-Berechtigungen (Scopes)
List<String> requiredScopes = Arrays.asList("invite:users", "manage:settings"); // Ersetze durch deine tatsächlichen erforderlichen Scopes
String scopes = (String) claims.get("scope");
List<String> tokenScopes = scopes != null ? Arrays.asList(scopes.split(" ")) : List.of();
if (!tokenScopes.containsAll(requiredScopes)) {
throw new AuthorizationException("Insufficient organization scope");
}
}
private void verifyPayload(JsonObject principal) {
// Überprüfe, ob der Audience-Claim das Organisationsformat hat
JsonArray audiences = principal.getJsonArray("aud");
boolean hasOrgAudience = false;
if (audiences != null) {
for (Object aud : audiences) {
if (aud.toString().startsWith("urn:logto:organization:")) {
hasOrgAudience = true;
break;
}
}
}
if (!hasOrgAudience) {
throw new AuthorizationException("Invalid audience for organization permissions");
}
// Überprüfe, ob die Organisations-ID mit dem Kontext übereinstimmt (ggf. aus dem Request-Kontext extrahieren)
String expectedOrgId = "your-organization-id"; // Aus dem Request-Kontext extrahieren
String expectedAud = "urn:logto:organization:" + expectedOrgId;
if (!audiences.contains(expectedAud)) {
throw new AuthorizationException("Organization ID mismatch");
}
// Überprüfe erforderliche Organisations-Berechtigungen (Scopes)
List<String> requiredScopes = Arrays.asList("invite:users", "manage:settings"); // Ersetze durch deine tatsächlichen erforderlichen Scopes
String scopes = principal.getString("scope");
List<String> tokenScopes = scopes != null ? Arrays.asList(scopes.split(" ")) : List.of();
if (!tokenScopes.containsAll(requiredScopes)) {
throw new AuthorizationException("Insufficient organization scope");
}
}
- Spring Boot
- Quarkus
- Micronaut
- Vert.x Web
private void verifyPayload(Jwt jwt) {
// Überprüfe, ob der Audience-Claim mit deinem API-Ressourcenindikator übereinstimmt
List<String> audiences = jwt.getAudience();
if (!audiences.contains("https://your-api-resource-indicator")) {
throw new AuthorizationException("Invalid audience for organization-level API resources");
}
// Überprüfe, ob die Organisations-ID mit dem Kontext übereinstimmt (ggf. aus dem Request-Kontext extrahieren)
String expectedOrgId = "your-organization-id"; // Aus dem Request-Kontext extrahieren
String orgId = jwt.getClaimAsString("organization_id");
if (!expectedOrgId.equals(orgId)) {
throw new AuthorizationException("Organization ID mismatch");
}
// Überprüfe erforderliche Berechtigungen (Scopes) für organisationsspezifische API-Ressourcen
List<String> requiredScopes = Arrays.asList("api:read", "api:write"); // Ersetze durch deine tatsächlichen erforderlichen Scopes
String scopes = jwt.getClaimAsString("scope");
List<String> tokenScopes = scopes != null ? Arrays.asList(scopes.split(" ")) : List.of();
if (!tokenScopes.containsAll(requiredScopes)) {
throw new AuthorizationException("Insufficient organization-level API scopes");
}
}
private void verifyPayload(JsonWebToken jwt) {
// Überprüfe, ob der Audience-Claim mit deinem API-Ressourcenindikator übereinstimmt
if (!jwt.getAudience().contains("https://your-api-resource-indicator")) {
throw new AuthorizationException("Invalid audience for organization-level API resources");
}
// Überprüfe, ob die Organisations-ID mit dem Kontext übereinstimmt (ggf. aus dem Request-Kontext extrahieren)
String expectedOrgId = "your-organization-id"; // Aus dem Request-Kontext extrahieren
String orgId = jwt.getClaim("organization_id");
if (!expectedOrgId.equals(orgId)) {
throw new AuthorizationException("Organization ID mismatch");
}
// Überprüfe erforderliche Berechtigungen (Scopes) für organisationsspezifische API-Ressourcen
List<String> requiredScopes = Arrays.asList("api:read", "api:write"); // Ersetze durch deine tatsächlichen erforderlichen Scopes
String scopes = jwt.getClaim("scope");
List<String> tokenScopes = scopes != null ? Arrays.asList(scopes.split(" ")) : List.of();
if (!tokenScopes.containsAll(requiredScopes)) {
throw new AuthorizationException("Insufficient organization-level API scopes");
}
}
private void verifyPayload(Claims claims) {
// Überprüfe, ob der Audience-Claim mit deinem API-Ressourcenindikator übereinstimmt
List<String> audiences = (List<String>) claims.get("aud");
if (audiences == null || !audiences.contains("https://your-api-resource-indicator")) {
throw new AuthorizationException("Invalid audience for organization-level API resources");
}
// Überprüfe, ob die Organisations-ID mit dem Kontext übereinstimmt (ggf. aus dem Request-Kontext extrahieren)
String expectedOrgId = "your-organization-id"; // Aus dem Request-Kontext extrahieren
String orgId = (String) claims.get("organization_id");
if (!expectedOrgId.equals(orgId)) {
throw new AuthorizationException("Organization ID mismatch");
}
// Überprüfe erforderliche Berechtigungen (Scopes) für organisationsspezifische API-Ressourcen
List<String> requiredScopes = Arrays.asList("api:read", "api:write"); // Ersetze durch deine tatsächlichen erforderlichen Scopes
String scopes = (String) claims.get("scope");
List<String> tokenScopes = scopes != null ? Arrays.asList(scopes.split(" ")) : List.of();
if (!tokenScopes.containsAll(requiredScopes)) {
throw new AuthorizationException("Insufficient organization-level API scopes");
}
}
private void verifyPayload(JsonObject principal) {
// Überprüfe, ob der Audience-Claim mit deinem API-Ressourcenindikator übereinstimmt
JsonArray audiences = principal.getJsonArray("aud");
if (audiences == null || !audiences.contains("https://your-api-resource-indicator")) {
throw new AuthorizationException("Invalid audience for organization-level API resources");
}
// Überprüfe, ob die Organisations-ID mit dem Kontext übereinstimmt (ggf. aus dem Request-Kontext extrahieren)
String expectedOrgId = "your-organization-id"; // Aus dem Request-Kontext extrahieren
String orgId = principal.getString("organization_id");
if (!expectedOrgId.equals(orgId)) {
throw new AuthorizationException("Organization ID mismatch");
}
// Überprüfe erforderliche Berechtigungen (Scopes) für organisationsspezifische API-Ressourcen
List<String> requiredScopes = Arrays.asList("api:read", "api:write"); // Ersetze durch deine tatsächlichen erforderlichen Scopes
String scopes = principal.getString("scope");
List<String> tokenScopes = scopes != null ? Arrays.asList(scopes.split(" ")) : List.of();
if (!tokenScopes.containsAll(requiredScopes)) {
throw new AuthorizationException("Insufficient organization-level API scopes");
}
}
Füge das erforderliche NuGet-Paket für JWT-Authentifizierung hinzu:
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.0" />
Erstelle einen Validierungsdienst, um die Tokenvalidierung zu übernehmen:
using System.Security.Claims;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using YourApiNamespace.Exceptions;
namespace YourApiNamespace.Services
{
public interface IJwtValidationService
{
Task ValidateTokenAsync(TokenValidatedContext context);
}
public class JwtValidationService : IJwtValidationService
{
public async Task ValidateTokenAsync(TokenValidatedContext context)
{
var principal = context.Principal!;
try
{
// Füge hier deine Validierungslogik basierend auf dem Berechtigungsmodell ein
ValidatePayload(principal);
}
catch (AuthorizationException)
{
throw; // Autorisierungsfehler erneut auslösen
}
catch (Exception ex)
{
throw new AuthorizationException($"Tokenvalidierung fehlgeschlagen: {ex.Message}", 401);
}
}
private void ValidatePayload(ClaimsPrincipal principal)
{
// Implementiere hier deine Überprüfungslogik basierend auf dem Berechtigungsmodell
// Dies wird im Abschnitt zu den Berechtigungsmodellen unten gezeigt
}
}
}
Konfiguriere die JWT-Authentifizierung in deiner Program.cs
:
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using YourApiNamespace.Services;
using YourApiNamespace.Exceptions;
var builder = WebApplication.CreateBuilder(args);
// Füge Dienste zum Container hinzu
builder.Services.AddControllers();
builder.Services.AddScoped<IJwtValidationService, JwtValidationService>();
// Konfiguriere JWT-Authentifizierung
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.Authority = AuthConstants.Issuer;
options.MetadataAddress = $"{AuthConstants.Issuer}/.well-known/openid_configuration";
options.RequireHttpsMetadata = true;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = AuthConstants.Issuer,
ValidateAudience = false, // Die Zielgruppe wird manuell basierend auf dem Berechtigungsmodell validiert
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ClockSkew = TimeSpan.FromMinutes(5)
};
options.Events = new JwtBearerEvents
{
OnTokenValidated = async context =>
{
var validationService = context.HttpContext.RequestServices
.GetRequiredService<IJwtValidationService>();
await validationService.ValidateTokenAsync(context);
},
OnAuthenticationFailed = context =>
{
// Behandle JWT-Bibliotheksfehler als 401
context.Response.StatusCode = 401;
context.Response.ContentType = "application/json";
context.Response.WriteAsync($"{{\"error\": \"Invalid token\"}}");
context.HandleResponse();
return Task.CompletedTask;
}
};
});
builder.Services.AddAuthorization();
var app = builder.Build();
// Globale Fehlerbehandlung für Authentifizierungs- / Autorisierungsfehler
app.Use(async (context, next) =>
{
try
{
await next();
}
catch (AuthorizationException ex)
{
context.Response.StatusCode = ex.StatusCode;
context.Response.ContentType = "application/json";
await context.Response.WriteAsync($"{{\"error\": \"{ex.Message}\"}}");
}
});
// Konfiguriere die HTTP-Request-Pipeline
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();
Implementiere entsprechend deinem Berechtigungsmodell die passende Validierungslogik in JwtValidationService
:
- Globale API-Ressourcen
- Organisation (nicht-API) Berechtigungen
- Organisationsbezogene API-Ressourcen
private void ValidatePayload(ClaimsPrincipal principal)
{
// Überprüfe, ob der Audience-Claim mit deinem API-Ressourcenindikator übereinstimmt
var audiences = principal.FindAll("aud").Select(c => c.Value).ToList();
if (!audiences.Contains("https://your-api-resource-indicator"))
{
throw new AuthorizationException("Ungültige Zielgruppe (audience)");
}
// Überprüfe erforderliche Berechtigungen für globale API-Ressourcen
var requiredScopes = new[] { "api:read", "api:write" }; // Ersetze durch deine tatsächlichen erforderlichen Berechtigungen
var tokenScopes = principal.FindFirst("scope")?.Value?.Split(' ') ?? Array.Empty<string>();
if (!requiredScopes.All(scope => tokenScopes.Contains(scope)))
{
throw new AuthorizationException("Unzureichende Berechtigung (scope)");
}
}
private void ValidatePayload(ClaimsPrincipal principal)
{
// Überprüfe, ob der Audience-Claim dem Organisationsformat entspricht
var audiences = principal.FindAll("aud").Select(c => c.Value).ToList();
var hasOrgAudience = audiences.Any(aud => aud.StartsWith("urn:logto:organization:"));
if (!hasOrgAudience)
{
throw new AuthorizationException("Ungültige Zielgruppe für Organisationsberechtigungen");
}
// Überprüfe, ob die Organisations-ID mit dem Kontext übereinstimmt (ggf. aus dem Request-Kontext extrahieren)
var expectedOrgId = "your-organization-id"; // Aus dem Request-Kontext extrahieren
var expectedAud = $"urn:logto:organization:{expectedOrgId}";
if (!audiences.Contains(expectedAud))
{
throw new AuthorizationException("Organisation-ID stimmt nicht überein");
}
// Überprüfe erforderliche Organisationsberechtigungen
var requiredScopes = new[] { "invite:users", "manage:settings" }; // Ersetze durch deine tatsächlichen erforderlichen Berechtigungen
var tokenScopes = principal.FindFirst("scope")?.Value?.Split(' ') ?? Array.Empty<string>();
if (!requiredScopes.All(scope => tokenScopes.Contains(scope)))
{
throw new AuthorizationException("Unzureichende Organisationsberechtigung (scope)");
}
}
private void ValidatePayload(ClaimsPrincipal principal)
{
// Überprüfe, ob der Audience-Claim mit deinem API-Ressourcenindikator übereinstimmt
var audiences = principal.FindAll("aud").Select(c => c.Value).ToList();
if (!audiences.Contains("https://your-api-resource-indicator"))
{
throw new AuthorizationException("Ungültige Zielgruppe für organisationsbezogene API-Ressourcen");
}
// Überprüfe, ob die Organisations-ID mit dem Kontext übereinstimmt (ggf. aus dem Request-Kontext extrahieren)
var expectedOrgId = "your-organization-id"; // Aus dem Request-Kontext extrahieren
var orgId = principal.FindFirst("organization_id")?.Value;
if (!expectedOrgId.Equals(orgId))
{
throw new AuthorizationException("Organisation-ID stimmt nicht überein");
}
// Überprüfe erforderliche Berechtigungen für organisationsbezogene API-Ressourcen
var requiredScopes = new[] { "api:read", "api:write" }; // Ersetze durch deine tatsächlichen erforderlichen Berechtigungen
var tokenScopes = principal.FindFirst("scope")?.Value?.Split(' ') ?? Array.Empty<string>();
if (!requiredScopes.All(scope => tokenScopes.Contains(scope)))
{
throw new AuthorizationException("Unzureichende organisationsbezogene API-Berechtigungen (scope)");
}
}
Wir verwenden firebase/php-jwt, um JWTs zu validieren. Installiere es mit Composer:
- Laravel
- Symfony
- Slim
composer require firebase/php-jwt
composer require firebase/php-jwt
composer require firebase/php-jwt slim/slim:"4.*" slim/psr7
Füge zunächst diese gemeinsamen Hilfsfunktionen hinzu, um die JWT-Validierung zu handhaben:
<?php
use Firebase\JWT\JWT;
use Firebase\JWT\JWK;
use Firebase\JWT\Key;
class JwtValidator
{
use AuthHelpers;
private static ?array $jwks = null;
public static function fetchJwks(): array
{
if (self::$jwks === null) {
$jwksData = file_get_contents(AuthConstants::JWKS_URI);
if ($jwksData === false) {
throw new AuthorizationException('Fehler beim Abrufen der JWKS', 401);
}
self::$jwks = json_decode($jwksData, true);
}
return self::$jwks;
}
public static function validateJwtToken(string $token): array
{
try {
$jwks = self::fetchJwks();
$keys = JWK::parseKeySet($jwks);
$decoded = JWT::decode($token, $keys);
$payload = (array) $decoded;
// Aussteller (Issuer) überprüfen
if (($payload['iss'] ?? '') !== AuthConstants::ISSUER) {
throw new AuthorizationException('Ungültiger Aussteller', 401);
}
self::verifyPayload($payload);
return $payload;
} catch (AuthorizationException $e) {
throw $e;
} catch (Exception $e) {
throw new AuthorizationException('Ungültiges Token: ' . $e->getMessage(), 401);
}
}
public static function createAuthInfo(array $payload): AuthInfo
{
$scopes = !empty($payload['scope']) ? explode(' ', $payload['scope']) : [];
$audience = $payload['aud'] ?? [];
if (is_string($audience)) {
$audience = [$audience];
}
return new AuthInfo(
sub: $payload['sub'],
clientId: $payload['client_id'] ?? null,
organizationId: $payload['organization_id'] ?? null,
scopes: $scopes,
audience: $audience
);
}
private static function verifyPayload(array $payload): void
{
// Implementiere hier deine Überprüfungslogik basierend auf dem Berechtigungsmodell
// Dies wird im Abschnitt zu den Berechtigungsmodellen unten gezeigt
}
}
Implementiere anschließend das Middleware, um das Zugangstoken (Access token) zu überprüfen:
- Laravel
- Symfony
- Slim
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
class VerifyAccessToken
{
use AuthHelpers;
public function handle(Request $request, Closure $next): Response
{
try {
$token = $this->extractBearerToken($request->headers->all());
$payload = JwtValidator::validateJwtToken($token);
// Auth-Informationen in den Request-Attributen für generische Nutzung speichern
$request->attributes->set('auth', JwtValidator::createAuthInfo($payload));
return $next($request);
} catch (AuthorizationException $e) {
return response()->json(['error' => $e->getMessage()], $e->statusCode);
}
}
}
Registriere das Middleware in app/Http/Kernel.php
:
protected $middlewareAliases = [
// ... andere Middleware
'auth.token' => \App\Http\Middleware\VerifyAccessToken::class,
];
<?php
namespace App\Security;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Http\Authenticator\AbstractAuthenticator;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport;
class JwtAuthenticator extends AbstractAuthenticator
{
use AuthHelpers;
public function supports(Request $request): ?bool
{
return $request->headers->has('authorization');
}
public function authenticate(Request $request): Passport
{
try {
$token = $this->extractBearerToken($request->headers->all());
$payload = JwtValidator::validateJwtToken($token);
$authInfo = JwtValidator::createAuthInfo($payload);
// Auth-Informationen in den Request-Attributen für generische Nutzung speichern
$request->attributes->set('auth', $authInfo);
return new SelfValidatingPassport(new UserBadge($payload['sub']));
} catch (AuthorizationException $e) {
throw new AuthenticationException($e->getMessage());
}
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
{
return null; // Weiter zum Controller
}
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
{
return new JsonResponse(['error' => $exception->getMessage()], Response::HTTP_UNAUTHORIZED);
}
}
Konfiguriere die Sicherheit in config/packages/security.yaml
:
security:
firewalls:
api:
pattern: ^/api/protected
stateless: true
custom_authenticators:
- App\Security\JwtAuthenticator
<?php
namespace App\Middleware;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Slim\Psr7\Response;
class JwtMiddleware implements MiddlewareInterface
{
use AuthHelpers;
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
try {
$headers = $request->getHeaders();
$token = $this->extractBearerToken($headers);
$payload = JwtValidator::validateJwtToken($token);
// Auth-Informationen in den Request-Attributen für generische Nutzung speichern
$request = $request->withAttribute('auth', JwtValidator::createAuthInfo($payload));
return $handler->handle($request);
} catch (AuthorizationException $e) {
$response = new Response();
$response->getBody()->write(json_encode(['error' => $e->getMessage()]));
return $response
->withHeader('Content-Type', 'application/json')
->withStatus($e->statusCode);
}
}
}
Implementiere entsprechend deinem Berechtigungsmodell die passende Überprüfungslogik in JwtValidator
:
- Globale API-Ressourcen
- Organisations-(Nicht-API-)Berechtigungen
- Organisationsbezogene API-Ressourcen
private static function verifyPayload(array $payload): void
{
// Überprüfe, ob der Audience-Anspruch (aud) mit deinem API-Ressourcenindikator übereinstimmt
$audiences = $payload['aud'] ?? [];
if (is_string($audiences)) {
$audiences = [$audiences];
}
if (!in_array('https://your-api-resource-indicator', $audiences)) {
throw new AuthorizationException('Ungültige Zielgruppe (Audience)');
}
// Überprüfe erforderliche Berechtigungen (Scopes) für globale API-Ressourcen
$requiredScopes = ['api:read', 'api:write']; // Ersetze durch deine tatsächlich benötigten Berechtigungen
$scopes = !empty($payload['scope']) ? explode(' ', $payload['scope']) : [];
foreach ($requiredScopes as $scope) {
if (!in_array($scope, $scopes)) {
throw new AuthorizationException('Unzureichende Berechtigung (Scope)');
}
}
}
private static function verifyPayload(array $payload): void
{
// Überprüfe, ob der Audience-Anspruch (aud) dem Organisationsformat entspricht
$audiences = $payload['aud'] ?? [];
if (is_string($audiences)) {
$audiences = [$audiences];
}
$hasOrgAudience = false;
foreach ($audiences as $aud) {
if (str_starts_with($aud, 'urn:logto:organization:')) {
$hasOrgAudience = true;
break;
}
}
if (!$hasOrgAudience) {
throw new AuthorizationException('Ungültige Zielgruppe (Audience) für Organisationsberechtigungen');
}
// Überprüfe, ob die Organisations-ID mit dem Kontext übereinstimmt (dies musst du ggf. aus dem Request-Kontext extrahieren)
$expectedOrgId = 'your-organization-id'; // Aus dem Request-Kontext extrahieren
$expectedAud = "urn:logto:organization:{$expectedOrgId}";
if (!in_array($expectedAud, $audiences)) {
throw new AuthorizationException('Organisations-ID stimmt nicht überein');
}
// Überprüfe erforderliche Organisationsberechtigungen (Scopes)
$requiredScopes = ['invite:users', 'manage:settings']; // Ersetze durch deine tatsächlich benötigten Berechtigungen
$scopes = !empty($payload['scope']) ? explode(' ', $payload['scope']) : [];
foreach ($requiredScopes as $scope) {
if (!in_array($scope, $scopes)) {
throw new AuthorizationException('Unzureichende Organisationsberechtigung (Scope)');
}
}
}
private static function verifyPayload(array $payload): void
{
// Überprüfe, ob der Audience-Anspruch (aud) mit deinem API-Ressourcenindikator übereinstimmt
$audiences = $payload['aud'] ?? [];
if (is_string($audiences)) {
$audiences = [$audiences];
}
if (!in_array('https://your-api-resource-indicator', $audiences)) {
throw new AuthorizationException('Ungültige Zielgruppe (Audience) für organisationsbezogene API-Ressourcen');
}
// Überprüfe, ob die Organisations-ID mit dem Kontext übereinstimmt (dies musst du ggf. aus dem Request-Kontext extrahieren)
$expectedOrgId = 'your-organization-id'; // Aus dem Request-Kontext extrahieren
$orgId = $payload['organization_id'] ?? null;
if ($expectedOrgId !== $orgId) {
throw new AuthorizationException('Organisations-ID stimmt nicht überein');
}
// Überprüfe erforderliche Berechtigungen (Scopes) für organisationsbezogene API-Ressourcen
$requiredScopes = ['api:read', 'api:write']; // Ersetze durch deine tatsächlich benötigten Berechtigungen
$scopes = !empty($payload['scope']) ? explode(' ', $payload['scope']) : [];
foreach ($requiredScopes as $scope) {
if (!in_array($scope, $scopes)) {
throw new AuthorizationException('Unzureichende organisationsbezogene API-Berechtigungen (Scopes)');
}
}
}
Wir verwenden das jwt Gem, um JWTs zu validieren. Füge es deiner Gemfile hinzu:
gem 'jwt'
# net-http ist seit Ruby 2.7 Teil der Ruby-Standardbibliothek, muss nicht explizit hinzugefügt werden
Führe dann aus:
bundle install
Füge zunächst diese gemeinsamen Hilfsfunktionen hinzu, um JWKS und Token-Validierung zu behandeln:
require 'jwt'
require 'net/http'
require 'json'
class JwtValidator
include AuthHelpers
def self.fetch_jwks
@jwks ||= begin
uri = URI(AuthConstants::JWKS_URI)
response = Net::HTTP.get_response(uri)
raise AuthorizationError.new('Failed to fetch JWKS', 401) unless response.is_a?(Net::HTTPSuccess)
jwks_data = JSON.parse(response.body)
JWT::JWK::Set.new(jwks_data)
end
end
def self.validate_jwt_token(token)
jwks = fetch_jwks
# Die JWT-Bibliothek erkennt den Algorithmus automatisch anhand des JWKS
decoded_token = JWT.decode(token, nil, true, {
iss: AuthConstants::ISSUER,
verify_iss: true,
verify_aud: false, # Die Zielgruppe wird manuell anhand des Berechtigungsmodells überprüft
jwks: jwks
})[0]
decoded_token
end
def self.create_auth_info(payload)
scopes = payload['scope']&.split(' ') || []
audience = payload['aud'] || []
AuthInfo.new(
payload['sub'],
payload['client_id'],
payload['organization_id'],
scopes,
audience
)
end
def self.verify_payload(payload)
# Implementiere hier deine Überprüfungslogik basierend auf dem Berechtigungsmodell
# Dies wird im Abschnitt zu den Berechtigungsmodellen unten gezeigt
end
end
Implementiere dann die Middleware, um das Zugangstoken zu überprüfen:
- Rails
- Sinatra
- Grape
module JwtAuthentication
extend ActiveSupport::Concern
include AuthHelpers
included do
before_action :verify_access_token, only: [:protected_action] # Füge spezifische Aktionen hinzu
end
private
def verify_access_token
begin
token = extract_bearer_token(request)
decoded_token = JwtValidator.validate_jwt_token(token)
JwtValidator.verify_payload(decoded_token)
# Auth-Info für generische Nutzung speichern
@auth = JwtValidator.create_auth_info(decoded_token)
rescue AuthorizationError => e
render json: { error: e.message }, status: e.status
rescue JWT::DecodeError, JWT::VerificationError, JWT::ExpiredSignature => e
render json: { error: 'Invalid token' }, status: 401
end
end
end
class AuthMiddleware
include AuthHelpers
def initialize(app)
@app = app
end
def call(env)
request = Rack::Request.new(env)
# Schütze nur spezifische Routen
if request.path.start_with?('/api/protected')
begin
token = extract_bearer_token(request)
decoded_token = JwtValidator.validate_jwt_token(token)
JwtValidator.verify_payload(decoded_token)
# Auth-Info im env für generische Nutzung speichern
env['auth'] = JwtValidator.create_auth_info(decoded_token)
rescue AuthorizationError => e
return [e.status, { 'Content-Type' => 'application/json' }, [{ error: e.message }.to_json]]
rescue JWT::DecodeError, JWT::VerificationError, JWT::ExpiredSignature => e
return [401, { 'Content-Type' => 'application/json' }, [{ error: 'Invalid token' }.to_json]]
end
end
@app.call(env)
end
end
module GrapeAuthHelpers
include AuthHelpers
def authenticate_user!
begin
token = extract_bearer_token(request)
decoded_token = JwtValidator.validate_jwt_token(token)
JwtValidator.verify_payload(decoded_token)
# Auth-Info für generische Nutzung speichern
@auth = JwtValidator.create_auth_info(decoded_token)
rescue AuthorizationError => e
error!({ error: e.message }, e.status)
rescue JWT::DecodeError, JWT::VerificationError, JWT::ExpiredSignature => e
error!({ error: 'Invalid token' }, 401)
end
end
def auth
@auth
end
end
Implementiere entsprechend deinem Berechtigungsmodell die passende Überprüfungslogik in JwtValidator
:
- Globale API-Ressourcen
- Organisation (nicht-API) Berechtigungen
- Organisationsbezogene API-Ressourcen
def self.verify_payload(payload)
# Überprüfe, ob der Audience-Anspruch mit deinem API-Ressourcenindikator übereinstimmt
audiences = payload['aud'] || []
unless audiences.include?('https://your-api-resource-indicator')
raise AuthorizationError.new('Invalid audience')
end
# Überprüfe erforderliche Berechtigungen für globale API-Ressourcen
required_scopes = ['api:read', 'api:write'] # Ersetze durch deine tatsächlich erforderlichen Berechtigungen
token_scopes = payload['scope']&.split(' ') || []
unless required_scopes.all? { |scope| token_scopes.include?(scope) }
raise AuthorizationError.new('Insufficient scope')
end
end
def self.verify_payload(payload)
# Überprüfe, ob der Audience-Anspruch dem Organisationsformat entspricht
audiences = payload['aud'] || []
has_org_audience = audiences.any? { |aud| aud.start_with?('urn:logto:organization:') }
unless has_org_audience
raise AuthorizationError.new('Invalid audience for organization permissions')
end
# Überprüfe, ob die Organisations-ID mit dem Kontext übereinstimmt (dies musst du ggf. aus dem Request-Kontext extrahieren)
expected_org_id = 'your-organization-id' # Aus dem Request-Kontext extrahieren
expected_aud = "urn:logto:organization:#{expected_org_id}"
unless audiences.include?(expected_aud)
raise AuthorizationError.new('Organization ID mismatch')
end
# Überprüfe erforderliche Organisationsberechtigungen
required_scopes = ['invite:users', 'manage:settings'] # Ersetze durch deine tatsächlich erforderlichen Berechtigungen
token_scopes = payload['scope']&.split(' ') || []
unless required_scopes.all? { |scope| token_scopes.include?(scope) }
raise AuthorizationError.new('Insufficient organization scope')
end
end
def self.verify_payload(payload)
# Überprüfe, ob der Audience-Anspruch mit deinem API-Ressourcenindikator übereinstimmt
audiences = payload['aud'] || []
unless audiences.include?('https://your-api-resource-indicator')
raise AuthorizationError.new('Invalid audience for organization-level API resources')
end
# Überprüfe, ob die Organisations-ID mit dem Kontext übereinstimmt (dies musst du ggf. aus dem Request-Kontext extrahieren)
expected_org_id = 'your-organization-id' # Aus dem Request-Kontext extrahieren
org_id = payload['organization_id']
unless expected_org_id == org_id
raise AuthorizationError.new('Organization ID mismatch')
end
# Überprüfe erforderliche Berechtigungen für organisationsbezogene API-Ressourcen
required_scopes = ['api:read', 'api:write'] # Ersetze durch deine tatsächlich erforderlichen Berechtigungen
token_scopes = payload['scope']&.split(' ') || []
unless required_scopes.all? { |scope| token_scopes.include?(scope) }
raise AuthorizationError.new('Insufficient organization-level API scopes')
end
end
Wir verwenden jsonwebtoken, um JWTs zu validieren. Füge die benötigten Abhängigkeiten zu deiner Cargo.toml
hinzu:
- Axum
- Actix Web
- Rocket
[dependencies]
axum = "0.7"
tokio = { version = "1.0", features = ["full"] }
tower = "0.4"
tower-http = { version = "0.5", features = ["cors"] }
jsonwebtoken = "9.0"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
reqwest = { version = "0.11", features = ["json"] }
[dependencies]
actix-web = "4.0"
tokio = { version = "1.0", features = ["full"] }
jsonwebtoken = "9.0"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
reqwest = { version = "0.11", features = ["json"] }
[dependencies]
rocket = { version = "0.5", features = ["json"] }
tokio = { version = "1.0", features = ["full"] }
jsonwebtoken = "9.0"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
reqwest = { version = "0.11", features = ["json"] }
Füge zunächst diese gemeinsamen Hilfsfunktionen hinzu, um die JWT-Validierung zu behandeln:
use crate::{AuthInfo, AuthorizationError, ISSUER, JWKS_URI};
use jsonwebtoken::{decode, decode_header, Algorithm, DecodingKey, Validation};
use serde_json::Value;
use std::collections::HashMap;
pub struct JwtValidator {
jwks: HashMap<String, DecodingKey>,
}
impl JwtValidator {
pub async fn new() -> Result<Self, AuthorizationError> {
let jwks = Self::fetch_jwks().await?;
Ok(Self { jwks })
}
async fn fetch_jwks() -> Result<HashMap<String, DecodingKey>, AuthorizationError> {
let response = reqwest::get(JWKS_URI).await.map_err(|e| {
AuthorizationError::with_status(format!("Failed to fetch JWKS: {}", e), 401)
})?;
let jwks: Value = response.json().await.map_err(|e| {
AuthorizationError::with_status(format!("Failed to parse JWKS: {}", e), 401)
})?;
let mut keys = HashMap::new();
if let Some(keys_array) = jwks["keys"].as_array() {
for key in keys_array {
if let (Some(kid), Some(kty), Some(n), Some(e)) = (
key["kid"].as_str(),
key["kty"].as_str(),
key["n"].as_str(),
key["e"].as_str(),
) {
if kty == "RSA" {
if let Ok(decoding_key) = DecodingKey::from_rsa_components(n, e) {
keys.insert(kid.to_string(), decoding_key);
}
}
}
}
}
if keys.is_empty() {
return Err(AuthorizationError::with_status("No valid keys found in JWKS", 401));
}
Ok(keys)
}
pub fn validate_jwt_token(&self, token: &str) -> Result<AuthInfo, AuthorizationError> {
let header = decode_header(token).map_err(|e| {
AuthorizationError::with_status(format!("Invalid token header: {}", e), 401)
})?;
let kid = header.kid.ok_or_else(|| {
AuthorizationError::with_status("Token missing kid claim", 401)
})?;
let key = self.jwks.get(&kid).ok_or_else(|| {
AuthorizationError::with_status("Unknown key ID", 401)
})?;
let mut validation = Validation::new(Algorithm::RS256);
validation.set_issuer(&[ISSUER]);
validation.validate_aud = false; // Wir überprüfen die Zielgruppe manuell
let token_data = decode::<Value>(token, key, &validation).map_err(|e| {
AuthorizationError::with_status(format!("Invalid token: {}", e), 401)
})?;
let claims = token_data.claims;
self.verify_payload(&claims)?;
Ok(self.create_auth_info(claims))
}
fn verify_payload(&self, claims: &Value) -> Result<(), AuthorizationError> {
// Implementiere hier deine Verifizierungslogik basierend auf dem Berechtigungsmodell
// Dies wird im Abschnitt zu Berechtigungsmodellen unten gezeigt
Ok(())
}
fn create_auth_info(&self, claims: Value) -> AuthInfo {
let scopes = claims["scope"]
.as_str()
.map(|s| s.split(' ').map(|s| s.to_string()).collect())
.unwrap_or_default();
let audience = match &claims["aud"] {
Value::Array(arr) => arr.iter().filter_map(|v| v.as_str().map(|s| s.to_string())).collect(),
Value::String(s) => vec![s.clone()],
_ => vec![],
};
AuthInfo::new(
claims["sub"].as_str().unwrap_or_default().to_string(),
claims["client_id"].as_str().map(|s| s.to_string()),
claims["organization_id"].as_str().map(|s| s.to_string()),
scopes,
audience,
)
}
}
Implementiere anschließend die Middleware, um das Zugangstoken zu überprüfen:
- Axum
- Actix Web
- Rocket
use crate::{AuthInfo, AuthorizationError, extract_bearer_token};
use crate::jwt_validator::JwtValidator;
use axum::{
extract::Request,
http::{HeaderMap, StatusCode},
middleware::Next,
response::{IntoResponse, Response},
Extension, Json,
};
use serde_json::json;
use std::sync::Arc;
pub async fn jwt_middleware(
Extension(validator): Extension<Arc<JwtValidator>>,
headers: HeaderMap,
mut request: Request,
next: Next,
) -> Result<Response, AuthorizationError> {
let authorization = headers
.get("authorization")
.and_then(|h| h.to_str().ok());
let token = extract_bearer_token(authorization)?;
let auth_info = validator.validate_jwt_token(token)?;
// Auth-Info in den Request-Extensions für generische Nutzung speichern
request.extensions_mut().insert(auth_info);
Ok(next.run(request).await)
}
impl IntoResponse for AuthorizationError {
fn into_response(self) -> Response {
let status = StatusCode::from_u16(self.status_code).unwrap_or(StatusCode::FORBIDDEN);
(status, Json(json!({ "error": self.message }))).into_response()
}
}
use crate::{AuthInfo, AuthorizationError, extract_bearer_token};
use crate::jwt_validator::JwtValidator;
use actix_web::{
dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform},
web, Error, HttpMessage, HttpResponse,
};
use futures::future::{ok, Ready};
use std::sync::Arc;
pub struct JwtMiddleware {
validator: Arc<JwtValidator>,
}
impl JwtMiddleware {
pub fn new(validator: Arc<JwtValidator>) -> Self {
Self { validator }
}
}
impl<S, B> Transform<S, ServiceRequest> for JwtMiddleware
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
S::Future: 'static,
B: 'static,
{
type Response = ServiceResponse<B>;
type Error = Error;
type InitError = ();
type Transform = JwtMiddlewareService<S>;
type Future = Ready<Result<Self::Transform, Self::InitError>>;
fn new_transform(&self, service: S) -> Self::Future {
ok(JwtMiddlewareService {
service,
validator: self.validator.clone(),
})
}
}
pub struct JwtMiddlewareService<S> {
service: S,
validator: Arc<JwtValidator>,
}
impl<S, B> Service<ServiceRequest> for JwtMiddlewareService<S>
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
S::Future: 'static,
B: 'static,
{
type Response = ServiceResponse<B>;
type Error = Error;
type Future = futures::future::LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
forward_ready!(service);
fn call(&self, req: ServiceRequest) -> Self::Future {
let validator = self.validator.clone();
Box::pin(async move {
let authorization = req
.headers()
.get("authorization")
.and_then(|h| h.to_str().ok());
match extract_bearer_token(authorization)
.and_then(|token| validator.validate_jwt_token(token))
{
Ok(auth_info) => {
// Auth-Info in den Request-Extensions für generische Nutzung speichern
req.extensions_mut().insert(auth_info);
let fut = self.service.call(req);
fut.await
}
Err(e) => {
let response = HttpResponse::build(
actix_web::http::StatusCode::from_u16(e.status_code)
.unwrap_or(actix_web::http::StatusCode::FORBIDDEN),
)
.json(serde_json::json!({ "error": e.message }));
Ok(req.into_response(response))
}
}
})
}
}
use crate::{AuthInfo, AuthorizationError, extract_bearer_token};
use crate::jwt_validator::JwtValidator;
use rocket::{
http::Status,
outcome::Outcome,
request::{self, FromRequest, Request},
State,
};
#[rocket::async_trait]
impl<'r> FromRequest<'r> for AuthInfo {
type Error = AuthorizationError;
async fn from_request(req: &'r Request<'_>) -> request::Outcome<Self, Self::Error> {
let validator = match req.guard::<&State<JwtValidator>>().await {
Outcome::Success(validator) => validator,
Outcome::Failure((status, _)) => {
return Outcome::Failure((
status,
AuthorizationError::with_status("JWT validator not found", 500),
))
}
Outcome::Forward(()) => {
return Outcome::Forward(())
}
};
let authorization = req.headers().get_one("authorization");
match extract_bearer_token(authorization)
.and_then(|token| validator.validate_jwt_token(token))
{
Ok(auth_info) => Outcome::Success(auth_info),
Err(e) => {
let status = Status::from_code(e.status_code).unwrap_or(Status::Forbidden);
Outcome::Failure((status, e))
}
}
}
}
Implementiere gemäß deinem Berechtigungsmodell die entsprechende Verifizierungslogik in JwtValidator
:
- Globale API-Ressourcen
- Organisation (nicht-API) Berechtigungen
- Organisationsbezogene API-Ressourcen
fn verify_payload(&self, claims: &Value) -> Result<(), AuthorizationError> {
// Überprüfe, ob der Audience-Claim mit deinem API-Ressourcenindikator übereinstimmt
let audiences = match &claims["aud"] {
Value::Array(arr) => arr.iter().filter_map(|v| v.as_str()).collect::<Vec<_>>(),
Value::String(s) => vec![s.as_str()],
_ => vec![],
};
if !audiences.contains(&"https://your-api-resource-indicator") {
return Err(AuthorizationError::new("Invalid audience"));
}
// Überprüfe erforderliche Berechtigungen für globale API-Ressourcen
let required_scopes = vec!["api:read", "api:write"]; // Ersetze durch deine tatsächlichen erforderlichen Berechtigungen
let scopes = claims["scope"]
.as_str()
.map(|s| s.split(' ').collect::<Vec<_>>())
.unwrap_or_default();
for required_scope in &required_scopes {
if !scopes.contains(required_scope) {
return Err(AuthorizationError::new("Insufficient scope"));
}
}
Ok(())
}
fn verify_payload(&self, claims: &Value) -> Result<(), AuthorizationError> {
// Überprüfe, ob der Audience-Claim dem Organisationsformat entspricht
let audiences = match &claims["aud"] {
Value::Array(arr) => arr.iter().filter_map(|v| v.as_str()).collect::<Vec<_>>(),
Value::String(s) => vec![s.as_str()],
_ => vec![],
};
let has_org_audience = audiences.iter().any(|aud| aud.starts_with("urn:logto:organization:"));
if !has_org_audience {
return Err(AuthorizationError::new("Invalid audience for organization permissions"));
}
// Überprüfe, ob die Organisations-ID mit dem Kontext übereinstimmt (du musst dies ggf. aus dem Request-Kontext extrahieren)
let expected_org_id = "your-organization-id"; // Aus dem Request-Kontext extrahieren
let expected_aud = format!("urn:logto:organization:{}", expected_org_id);
if !audiences.contains(&expected_aud.as_str()) {
return Err(AuthorizationError::new("Organization ID mismatch"));
}
// Überprüfe erforderliche Organisationsberechtigungen
let required_scopes = vec!["invite:users", "manage:settings"]; // Ersetze durch deine tatsächlichen erforderlichen Berechtigungen
let scopes = claims["scope"]
.as_str()
.map(|s| s.split(' ').collect::<Vec<_>>())
.unwrap_or_default();
for required_scope in &required_scopes {
if !scopes.contains(required_scope) {
return Err(AuthorizationError::new("Insufficient organization scope"));
}
}
Ok(())
}
fn verify_payload(&self, claims: &Value) -> Result<(), AuthorizationError> {
// Überprüfe, ob der Audience-Claim mit deinem API-Ressourcenindikator übereinstimmt
let audiences = match &claims["aud"] {
Value::Array(arr) => arr.iter().filter_map(|v| v.as_str()).collect::<Vec<_>>(),
Value::String(s) => vec![s.as_str()],
_ => vec![],
};
if !audiences.contains(&"https://your-api-resource-indicator") {
return Err(AuthorizationError::new("Invalid audience for organization-level API resources"));
}
// Überprüfe, ob die Organisations-ID mit dem Kontext übereinstimmt (du musst dies ggf. aus dem Request-Kontext extrahieren)
let expected_org_id = "your-organization-id"; // Aus dem Request-Kontext extrahieren
let org_id = claims["organization_id"].as_str().unwrap_or_default();
if expected_org_id != org_id {
return Err(AuthorizationError::new("Organization ID mismatch"));
}
// Überprüfe erforderliche Berechtigungen für organisationsbezogene API-Ressourcen
let required_scopes = vec!["api:read", "api:write"]; // Ersetze durch deine tatsächlichen erforderlichen Berechtigungen
let scopes = claims["scope"]
.as_str()
.map(|s| s.split(' ').collect::<Vec<_>>())
.unwrap_or_default();
for required_scope in &required_scopes {
if !scopes.contains(required_scope) {
return Err(AuthorizationError::new("Insufficient organization-level API scopes"));
}
}
Ok(())
}
Schritt 4: Middleware auf deine API anwenden
Wende die Middleware auf deine geschützten API-Routen an.
- Node.js
- Python
- Go
- Java
- .NET
- PHP
- Ruby
- Rust
- Express.js
- Koa.js
- Fastify
- Hapi.js
- NestJS
import express from 'express';
import { verifyAccessToken } from './auth-middleware.js';
const app = express();
app.get('/api/protected', verifyAccessToken, (req, res) => {
// Greife direkt über req.auth auf Authentifizierungsinformationen zu
res.json({ auth: req.auth });
});
app.get('/api/protected/detailed', verifyAccessToken, (req, res) => {
// Deine geschützte Endpunkt-Logik
res.json({
auth: req.auth,
message: 'Geschützte Daten erfolgreich abgerufen',
});
});
app.listen(3000);
import Koa from 'koa';
import Router from '@koa/router';
import { koaVerifyAccessToken } from './auth-middleware.js';
const app = new Koa();
const router = new Router();
router.get('/api/protected', koaVerifyAccessToken, (ctx) => {
// Greife direkt über ctx.state.auth auf Authentifizierungsinformationen zu
ctx.body = { auth: ctx.state.auth };
});
router.get('/api/protected/detailed', koaVerifyAccessToken, (ctx) => {
// Deine geschützte Endpunkt-Logik
ctx.body = {
auth: ctx.state.auth,
message: 'Geschützte Daten erfolgreich abgerufen',
};
});
app.use(router.routes());
app.listen(3000);
import Fastify from 'fastify';
import { fastifyVerifyAccessToken } from './auth-middleware.js';
const fastify = Fastify();
fastify.get('/api/protected', { preHandler: fastifyVerifyAccessToken }, (request, reply) => {
// Greife direkt über request.auth auf Authentifizierungsinformationen zu
reply.send({ auth: request.auth });
});
fastify.get(
'/api/protected/detailed',
{ preHandler: fastifyVerifyAccessToken },
(request, reply) => {
// Deine geschützte Endpunkt-Logik
reply.send({
auth: request.auth,
message: 'Geschützte Daten erfolgreich abgerufen',
});
}
);
fastify.listen({ port: 3000 });
import Hapi from '@hapi/hapi';
import { hapiVerifyAccessToken } from './auth-middleware.js';
const server = Hapi.server({ port: 3000 });
server.route({
method: 'GET',
path: '/api/protected',
options: {
pre: [{ method: hapiVerifyAccessToken }],
handler: (request, h) => {
// Greife auf Authentifizierungsinformationen über request.app.auth zu
return { auth: request.app.auth };
},
},
});
server.route({
method: 'GET',
path: '/api/protected/detailed',
options: {
pre: [{ method: hapiVerifyAccessToken }],
handler: (request, h) => {
// Deine geschützte Endpunkt-Logik
return {
auth: request.app.auth,
message: 'Geschützte Daten erfolgreich abgerufen',
};
},
},
});
await server.start();
import { Controller, Get, UseGuards, Req } from '@nestjs/common';
import { AccessTokenGuard } from './access-token.guard.js';
@Controller('api')
export class ProtectedController {
@Get('protected')
@UseGuards(AccessTokenGuard)
getProtected(@Req() req: any) {
// Greife auf Authentifizierungsinformationen über req.auth zu
return { auth: req.auth };
}
@Get('protected/detailed')
@UseGuards(AccessTokenGuard)
getDetailedProtected(@Req() req: any) {
// Deine geschützte Endpunkt-Logik
return {
auth: req.auth,
message: 'Geschützte Daten erfolgreich abgerufen',
};
}
}
- FastAPI
- Flask
- Django
- Django REST Framework
from fastapi import FastAPI, Depends
from auth_middleware import verify_access_token, AuthInfo
app = FastAPI()
@app.get("/api/protected")
async def protected_endpoint(auth: AuthInfo = Depends(verify_access_token)):
# Greife direkt über den auth-Parameter auf Authentifizierungsinformationen zu
return {"auth": auth.to_dict()}
@app.get("/api/protected/detailed")
async def detailed_endpoint(auth: AuthInfo = Depends(verify_access_token)):
# Deine geschützte Endpunkt-Logik
return {
"auth": auth.to_dict(),
"message": "Geschützte Daten wurden erfolgreich abgerufen"
}
from flask import Flask, g, jsonify
from auth_middleware import verify_access_token
app = Flask(__name__)
@app.route('/api/protected', methods=['GET'])
@verify_access_token
def protected_endpoint():
# Greife über g.auth auf Authentifizierungsinformationen zu
return jsonify({"auth": g.auth.to_dict()})
@app.route('/api/protected/detailed', methods=['GET'])
@verify_access_token
def detailed_endpoint():
# Deine geschützte Endpunkt-Logik
return jsonify({
"auth": g.auth.to_dict(),
"message": "Geschützte Daten wurden erfolgreich abgerufen"
})
from django.http import JsonResponse
from auth_middleware import require_access_token
@require_access_token
def protected_view(request):
# Greife über request.auth auf Authentifizierungsinformationen zu
return JsonResponse({"auth": request.auth.to_dict()})
@require_access_token
def detailed_view(request):
# Deine geschützte Endpunkt-Logik
return JsonResponse({
"auth": request.auth.to_dict(),
"message": "Geschützte Daten wurden erfolgreich abgerufen"
})
from rest_framework.decorators import api_view, authentication_classes
from rest_framework.response import Response
from auth_middleware import AccessTokenAuthentication
@api_view(['GET'])
@authentication_classes([AccessTokenAuthentication])
def protected_view(request):
# Greife über request.user.auth auf Authentifizierungsinformationen zu
return Response({"auth": request.user.auth.to_dict()})
@api_view(['GET'])
@authentication_classes([AccessTokenAuthentication])
def detailed_view(request):
# Deine geschützte Endpunkt-Logik
return Response({
"auth": request.user.auth.to_dict(),
"message": "Geschützte Daten wurden erfolgreich abgerufen"
})
Oder mit klassenbasierten Views:
from rest_framework.views import APIView
from rest_framework.response import Response
from auth_middleware import AccessTokenAuthentication
class ProtectedView(APIView):
authentication_classes = [AccessTokenAuthentication]
def get(self, request):
# Greife über request.user.auth auf Authentifizierungsinformationen zu
return Response({"auth": request.user.auth.to_dict()})
class DetailedView(APIView):
authentication_classes = [AccessTokenAuthentication]
def get(self, request):
# Deine geschützte Endpunkt-Logik
return Response({
"auth": request.user.auth.to_dict(),
"message": "Geschützte Daten wurden erfolgreich abgerufen"
})
- Gin
- Echo
- Fiber
- Chi
package main
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/lestrrat-go/jwx/v3/jwt"
)
func main() {
r := gin.Default()
// Middleware auf geschützte Routen anwenden
r.GET("/api/protected", VerifyAccessToken(), func(c *gin.Context) {
// Zugangstoken-Informationen direkt aus dem Kontext
tokenInterface, exists := c.Get("auth")
if !exists {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Token nicht gefunden"})
return
}
token := tokenInterface.(jwt.Token)
c.JSON(http.StatusOK, gin.H{
"sub": token.Subject(),
"client_id": getStringClaim(token, "client_id"),
"organization_id": getStringClaim(token, "organization_id"),
"scopes": getScopesFromToken(token),
"audience": getAudienceFromToken(token),
})
})
r.Run(":8080")
}
package main
import (
"net/http"
"github.com/labstack/echo/v4"
"github.com/lestrrat-go/jwx/v3/jwt"
)
func main() {
e := echo.New()
// Middleware auf geschützte Routen anwenden
e.GET("/api/protected", func(c echo.Context) error {
// Zugangstoken-Informationen direkt aus dem Kontext
tokenInterface := c.Get("auth")
if tokenInterface == nil {
return c.JSON(http.StatusInternalServerError, echo.Map{"error": "Token nicht gefunden"})
}
token := tokenInterface.(jwt.Token)
return c.JSON(http.StatusOK, echo.Map{
"sub": token.Subject(),
"client_id": getStringClaim(token, "client_id"),
"organization_id": getStringClaim(token, "organization_id"),
"scopes": getScopesFromToken(token),
"audience": getAudienceFromToken(token),
})
}, VerifyAccessToken)
e.Start(":8080")
}
Oder mit Routengruppen:
package main
import (
"github.com/labstack/echo/v4"
"github.com/lestrrat-go/jwx/v3/jwt"
)
func main() {
e := echo.New()
// Geschützte Routengruppe erstellen
api := e.Group("/api", VerifyAccessToken)
api.GET("/protected", func(c echo.Context) error {
// Zugangstoken-Informationen direkt aus dem Kontext
token := c.Get("auth").(jwt.Token)
return c.JSON(200, echo.Map{
"sub": token.Subject(),
"client_id": getStringClaim(token, "client_id"),
"organization_id": getStringClaim(token, "organization_id"),
"scopes": getScopesFromToken(token),
"audience": getAudienceFromToken(token),
"message": "Geschützte Daten erfolgreich abgerufen",
})
})
e.Start(":8080")
}
package main
import (
"github.com/gofiber/fiber/v2"
"github.com/lestrrat-go/jwx/v3/jwt"
)
func main() {
app := fiber.New()
// Middleware auf geschützte Routen anwenden
app.Get("/api/protected", VerifyAccessToken, func(c *fiber.Ctx) error {
// Zugangstoken-Informationen direkt aus Locals
tokenInterface := c.Locals("auth")
if tokenInterface == nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Token nicht gefunden"})
}
token := tokenInterface.(jwt.Token)
return c.JSON(fiber.Map{
"sub": token.Subject(),
"client_id": getStringClaim(token, "client_id"),
"organization_id": getStringClaim(token, "organization_id"),
"scopes": getScopesFromToken(token),
"audience": getAudienceFromToken(token),
})
})
app.Listen(":8080")
}
Oder mit Routengruppen:
package main
import (
"github.com/gofiber/fiber/v2"
"github.com/lestrrat-go/jwx/v3/jwt"
)
func main() {
app := fiber.New()
// Geschützte Routengruppe erstellen
api := app.Group("/api", VerifyAccessToken)
api.Get("/protected", func(c *fiber.Ctx) error {
// Zugangstoken-Informationen direkt aus Locals
token := c.Locals("auth").(jwt.Token)
return c.JSON(fiber.Map{
"sub": token.Subject(),
"client_id": getStringClaim(token, "client_id"),
"organization_id": getStringClaim(token, "organization_id"),
"scopes": getScopesFromToken(token),
"audience": getAudienceFromToken(token),
"message": "Geschützte Daten erfolgreich abgerufen",
})
})
app.Listen(":8080")
}
package main
import (
"encoding/json"
"net/http"
"github.com/go-chi/chi/v5"
"github.com/lestrrat-go/jwx/v3/jwt"
)
func main() {
r := chi.NewRouter()
// Middleware auf geschützte Routen anwenden
r.With(VerifyAccessToken).Get("/api/protected", func(w http.ResponseWriter, r *http.Request) {
// Zugangstoken-Informationen direkt aus dem Kontext
tokenInterface := r.Context().Value(AuthContextKey)
if tokenInterface == nil {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(map[string]string{"error": "Token nicht gefunden"})
return
}
token := tokenInterface.(jwt.Token)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"sub": token.Subject(),
"client_id": getStringClaim(token, "client_id"),
"organization_id": getStringClaim(token, "organization_id"),
"scopes": getScopesFromToken(token),
"audience": getAudienceFromToken(token),
})
})
http.ListenAndServe(":8080", r)
}
Oder mit Routengruppen:
package main
import (
"encoding/json"
"net/http"
"github.com/go-chi/chi/v5"
"github.com/lestrrat-go/jwx/v3/jwt"
)
func main() {
r := chi.NewRouter()
// Geschützte Routengruppe erstellen
r.Route("/api", func(r chi.Router) {
r.Use(VerifyAccessToken)
r.Get("/protected", func(w http.ResponseWriter, r *http.Request) {
// Zugangstoken-Informationen direkt aus dem Kontext
token := r.Context().Value(AuthContextKey).(jwt.Token)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"sub": token.Subject(),
"client_id": getStringClaim(token, "client_id"),
"organization_id": getStringClaim(token, "organization_id"),
"scopes": getScopesFromToken(token),
"audience": getAudienceFromToken(token),
"message": "Geschützte Daten erfolgreich abgerufen",
})
})
})
http.ListenAndServe(":8080", r)
}
- Spring Boot
- Quarkus
- Micronaut
- Vert.x Web
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
@RestController
public class ProtectedController {
@GetMapping("/api/protected")
public Map<String, Object> protectedEndpoint(@AuthenticationPrincipal Jwt jwt) {
// Zugriff auf Token-Informationen direkt aus dem JWT
String scopes = jwt.getClaimAsString("scope");
List<String> scopeList = scopes != null ? Arrays.asList(scopes.split(" ")) : List.of();
return Map.of(
"sub", jwt.getSubject(),
"client_id", jwt.getClaimAsString("client_id"),
"organization_id", jwt.getClaimAsString("organization_id"),
"scopes", scopeList,
"audience", jwt.getAudience()
);
}
@GetMapping("/api/protected/detailed")
public Map<String, Object> detailedEndpoint(@AuthenticationPrincipal Jwt jwt) {
// Deine Logik für den geschützten Endpunkt
return Map.of(
"sub", jwt.getSubject(),
"client_id", jwt.getClaimAsString("client_id"),
"organization_id", jwt.getClaimAsString("organization_id"),
"scopes", jwt.getClaimAsString("scope"),
"audience", jwt.getAudience(),
"message", "Geschützte Daten erfolgreich abgerufen"
);
}
}
import org.eclipse.microprofile.jwt.JsonWebToken;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.container.ContainerRequestContext;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
@Path("/api")
public class ProtectedResource {
@Inject
JsonWebToken jwt;
@GET
@Path("/protected")
@Produces(MediaType.APPLICATION_JSON)
public Map<String, Object> protectedEndpoint(@Context ContainerRequestContext requestContext) {
// Zugriff auf JWT direkt aus der Injektion oder dem Kontext
JsonWebToken token = (JsonWebToken) requestContext.getProperty("auth");
if (token == null) {
token = jwt; // Fallback auf injiziertes JWT
}
String scopes = token.getClaim("scope");
List<String> scopeList = scopes != null ? Arrays.asList(scopes.split(" ")) : List.of();
return Map.of(
"sub", token.getSubject(),
"client_id", token.<String>getClaim("client_id"),
"organization_id", token.<String>getClaim("organization_id"),
"scopes", scopeList,
"audience", token.getAudience()
);
}
@GET
@Path("/protected/detailed")
@Produces(MediaType.APPLICATION_JSON)
public Map<String, Object> detailedEndpoint() {
// Deine Logik für den geschützten Endpunkt
String scopes = jwt.getClaim("scope");
return Map.of(
"sub", jwt.getSubject(),
"client_id", jwt.<String>getClaim("client_id"),
"organization_id", jwt.<String>getClaim("organization_id"),
"scopes", scopes,
"audience", jwt.getAudience(),
"message", "Geschützte Daten erfolgreich abgerufen"
);
}
}
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
import io.micronaut.security.annotation.Secured;
import io.micronaut.security.authentication.Authentication;
import io.micronaut.security.rules.SecurityRule;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
@Controller("/api")
@Secured(SecurityRule.IS_AUTHENTICATED)
public class ProtectedController {
@Get("/protected")
public Map<String, Object> protectedEndpoint(Authentication authentication) {
// Zugriff auf Token-Informationen direkt aus Authentication
String scopes = (String) authentication.getAttributes().get("scope");
List<String> scopeList = scopes != null ? Arrays.asList(scopes.split(" ")) : List.of();
return Map.of(
"sub", authentication.getName(),
"client_id", authentication.getAttributes().get("client_id"),
"organization_id", authentication.getAttributes().get("organization_id"),
"scopes", scopeList,
"audience", authentication.getAttributes().get("aud")
);
}
@Get("/protected/detailed")
public Map<String, Object> detailedEndpoint(Authentication authentication) {
// Deine Logik für den geschützten Endpunkt
return Map.of(
"sub", authentication.getName(),
"client_id", authentication.getAttributes().get("client_id"),
"organization_id", authentication.getAttributes().get("organization_id"),
"scopes", authentication.getAttributes().get("scope"),
"audience", authentication.getAttributes().get("aud"),
"message", "Geschützte Daten erfolgreich abgerufen"
);
}
}
import io.vertx.core.AbstractVerticle;
import io.vertx.core.Promise;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.web.Router;
import io.vertx.ext.web.RoutingContext;
public class MainVerticle extends AbstractVerticle {
@Override
public void start(Promise<Void> startPromise) throws Exception {
Router router = Router.router(vertx);
// Middleware auf geschützte Routen anwenden
router.route("/api/protected*").handler(new JwtAuthHandler(vertx));
router.get("/api/protected").handler(this::protectedEndpoint);
router.get("/api/protected/detailed").handler(this::detailedEndpoint);
vertx.createHttpServer()
.requestHandler(router)
.listen(8080, result -> {
if (result.succeeded()) {
startPromise.complete();
} else {
startPromise.fail(result.cause());
}
});
}
private void protectedEndpoint(RoutingContext context) {
// Zugriff auf JWT-Principal direkt aus dem Kontext
JsonObject principal = context.get("auth");
if (principal == null) {
context.response()
.setStatusCode(500)
.putHeader("Content-Type", "application/json")
.end("{\"error\": \"JWT principal nicht gefunden\"}");
return;
}
String scopes = principal.getString("scope");
JsonObject response = new JsonObject()
.put("sub", principal.getString("sub"))
.put("client_id", principal.getString("client_id"))
.put("organization_id", principal.getString("organization_id"))
.put("scopes", scopes != null ? scopes.split(" ") : new String[0])
.put("audience", principal.getJsonArray("aud"));
context.response()
.putHeader("Content-Type", "application/json")
.end(response.encode());
}
private void detailedEndpoint(RoutingContext context) {
// Deine Logik für den geschützten Endpunkt
JsonObject principal = context.get("auth");
JsonObject response = new JsonObject()
.put("sub", principal.getString("sub"))
.put("client_id", principal.getString("client_id"))
.put("organization_id", principal.getString("organization_id"))
.put("scopes", principal.getString("scope"))
.put("audience", principal.getJsonArray("aud"))
.put("message", "Geschützte Daten erfolgreich abgerufen");
context.response()
.putHeader("Content-Type", "application/json")
.end(response.encode());
}
}
Wir haben bereits die Authentifizierungs- und Autorisierungsmiddleware in den vorherigen Abschnitten eingerichtet. Jetzt können wir einen geschützten Controller erstellen, der Zugangstokens validiert und Ansprüche (Claims) aus authentifizierten Anfragen extrahiert.
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using System.Security.Claims;
namespace YourApiNamespace.Controllers
{
[ApiController]
[Route("api/[controller]")]
[Authorize] // Authentifizierung für alle Aktionen in diesem Controller erforderlich
public class ProtectedController : ControllerBase
{
[HttpGet]
public IActionResult GetProtectedData()
{
// Informationen aus dem Zugangstoken direkt aus den User-Ansprüchen (Claims) abrufen
var sub = User.FindFirst(ClaimTypes.NameIdentifier)?.Value ?? User.FindFirst("sub")?.Value;
var clientId = User.FindFirst("client_id")?.Value;
var organizationId = User.FindFirst("organization_id")?.Value;
var scopes = User.FindFirst("scope")?.Value?.Split(' ') ?? Array.Empty<string>();
var audience = User.FindAll("aud").Select(c => c.Value).ToArray();
return Ok(new {
sub,
client_id = clientId,
organization_id = organizationId,
scopes,
audience
});
}
[HttpGet("claims")]
public IActionResult GetAllClaims()
{
// Gibt alle Ansprüche (Claims) zur Fehleranalyse / Überprüfung zurück
var claims = User.Claims.Select(c => new { c.Type, c.Value }).ToList();
return Ok(new { claims });
}
}
}
- Laravel
- Symfony
- Slim
<?php
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
Route::middleware('auth.token')->group(function () {
Route::get('/api/protected', function (Request $request) {
// Zugriff auf Authentifizierungsinformationen aus den Request-Attributen
$auth = $request->attributes->get('auth');
return ['auth' => $auth->toArray()];
});
Route::get('/api/protected/detailed', function (Request $request) {
// Deine Logik für geschützte Endpunkte
$auth = $request->attributes->get('auth');
return [
'auth' => $auth->toArray(),
'message' => 'Geschützte Daten erfolgreich abgerufen'
];
});
});
Oder mit Controllern:
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
class ProtectedController extends Controller
{
public function __construct()
{
$this->middleware('auth.token');
}
public function index(Request $request)
{
// Zugriff auf Authentifizierungsinformationen aus den Request-Attributen
$auth = $request->attributes->get('auth');
return ['auth' => $auth->toArray()];
}
public function show(Request $request)
{
// Deine Logik für geschützte Endpunkte
$auth = $request->attributes->get('auth');
return [
'auth' => $auth->toArray(),
'message' => 'Geschützte Daten erfolgreich abgerufen'
];
}
}
<?php
namespace App\Controller\Api;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
#[Route('/api/protected')]
#[IsGranted('IS_AUTHENTICATED_FULLY')]
class ProtectedController extends AbstractController
{
#[Route('', methods: ['GET'])]
public function index(Request $request): JsonResponse
{
// Zugriff auf Authentifizierungsinformationen aus den Request-Attributen
$auth = $request->attributes->get('auth');
return $this->json(['auth' => $auth->toArray()]);
}
#[Route('/detailed', methods: ['GET'])]
public function detailed(Request $request): JsonResponse
{
// Deine Logik für geschützte Endpunkte
$auth = $request->attributes->get('auth');
return $this->json([
'auth' => $auth->toArray(),
'message' => 'Geschützte Daten erfolgreich abgerufen'
]);
}
}
<?php
use App\Middleware\JwtMiddleware;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Slim\Factory\AppFactory;
require __DIR__ . '/../vendor/autoload.php';
$app = AppFactory::create();
// Middleware auf geschützte Routen anwenden
$app->group('/api/protected', function ($group) {
$group->get('', function (Request $request, Response $response) {
// Zugriff auf Authentifizierungsinformationen aus den Request-Attributen
$auth = $request->getAttribute('auth');
$response->getBody()->write(json_encode(['auth' => $auth->toArray()]));
return $response->withHeader('Content-Type', 'application/json');
});
$group->get('/detailed', function (Request $request, Response $response) {
// Deine Logik für geschützte Endpunkte
$auth = $request->getAttribute('auth');
$data = [
'auth' => $auth->toArray(),
'message' => 'Geschützte Daten erfolgreich abgerufen'
];
$response->getBody()->write(json_encode($data));
return $response->withHeader('Content-Type', 'application/json');
});
})->add(new JwtMiddleware());
// Öffentlicher Endpunkt (nicht geschützt)
$app->get('/', function (Request $request, Response $response) {
$response->getBody()->write(json_encode(['message' => 'Öffentlicher Endpunkt']));
return $response->withHeader('Content-Type', 'application/json');
});
$app->run();
Oder mit einer strukturierteren Herangehensweise:
<?php
namespace App\Controllers;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
class ProtectedController
{
public function index(Request $request, Response $response): Response
{
// Zugriff auf Authentifizierungsinformationen aus den Request-Attributen
$auth = $request->getAttribute('auth');
$response->getBody()->write(json_encode(['auth' => $auth->toArray()]));
return $response->withHeader('Content-Type', 'application/json');
}
public function detailed(Request $request, Response $response): Response
{
// Deine Logik für geschützte Endpunkte
$auth = $request->getAttribute('auth');
$data = [
'auth' => $auth->toArray(),
'message' => 'Geschützte Daten erfolgreich abgerufen'
];
$response->getBody()->write(json_encode($data));
return $response->withHeader('Content-Type', 'application/json');
}
}
- Rails
- Sinatra
- Grape
class ApplicationController < ActionController::API # Für API-only-Apps
# class ApplicationController < ActionController::Base # Für vollständige Rails-Apps
include JwtAuthentication
end
class Api::ProtectedController < ApplicationController
before_action :verify_access_token
def index
# Auth-Informationen aus @auth abrufen
render json: { auth: @auth.to_h }
end
def show
# Deine geschützte Endpunkt-Logik
render json: {
auth: @auth.to_h,
message: "Geschützte Daten erfolgreich abgerufen"
}
end
end
Rails.application.routes.draw do
namespace :api do
resources :protected, only: [:index, :show]
end
end
require 'sinatra'
require 'json'
require_relative 'auth_middleware'
require_relative 'auth_constants'
require_relative 'auth_info'
require_relative 'authorization_error'
require_relative 'auth_helpers'
require_relative 'jwt_validator'
# Middleware anwenden
use AuthMiddleware
get '/api/protected' do
content_type :json
# Auth-Informationen aus env abrufen
auth = env['auth']
{ auth: auth.to_h }.to_json
end
get '/api/protected/:id' do
content_type :json
# Deine geschützte Endpunkt-Logik
auth = env['auth']
{
auth: auth.to_h,
id: params[:id],
message: "Geschützte Daten erfolgreich abgerufen"
}.to_json
end
# Öffentlicher Endpunkt (nicht durch Middleware geschützt)
get '/' do
content_type :json
{ message: "Öffentlicher Endpunkt" }.to_json
end
require 'grape'
require_relative 'auth_helpers'
require_relative 'auth_constants'
require_relative 'auth_info'
require_relative 'authorization_error'
require_relative 'jwt_validator'
class API < Grape::API
format :json
helpers GrapeAuthHelpers
namespace :api do
namespace :protected do
before do
authenticate_user!
end
get do
# Auth-Informationen aus Auth-Helper abrufen
{ auth: auth.to_h }
end
get ':id' do
# Deine geschützte Endpunkt-Logik
{
auth: auth.to_h,
id: params[:id],
message: "Geschützte Daten erfolgreich abgerufen"
}
end
end
end
# Öffentlicher Endpunkt (nicht geschützt)
get :public do
{ message: "Öffentlicher Endpunkt" }
end
end
require_relative 'api'
run API
- Axum
- Actix Web
- Rocket
use axum::{
extract::Extension,
http::StatusCode,
middleware,
response::Json,
routing::get,
Router,
};
use serde_json::{json, Value};
use std::sync::Arc;
use tower_http::cors::CorsLayer;
mod lib;
mod jwt_validator;
mod middleware as jwt_middleware;
use lib::AuthInfo;
use jwt_validator::JwtValidator;
#[tokio::main]
async fn main() {
let validator = Arc::new(JwtValidator::new().await.expect("Failed to initialize JWT validator"));
let app = Router::new()
.route("/api/protected", get(protected_handler))
.route("/api/protected/detailed", get(detailed_handler))
.layer(middleware::from_fn(jwt_middleware::jwt_middleware))
.layer(Extension(validator))
.layer(CorsLayer::permissive());
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}
async fn protected_handler(Extension(auth): Extension<AuthInfo>) -> Json<Value> {
// Zugriff auf Auth-Informationen direkt aus Extension
Json(json!({ "auth": auth }))
}
async fn detailed_handler(Extension(auth): Extension<AuthInfo>) -> Json<Value> {
// Deine geschützte Endpunkt-Logik
Json(json!({
"auth": auth,
"message": "Geschützte Daten erfolgreich abgerufen"
}))
}
use actix_web::{middleware::Logger, web, App, HttpRequest, HttpServer, Result};
use serde_json::{json, Value};
use std::sync::Arc;
mod lib;
mod jwt_validator;
mod middleware as jwt_middleware;
use lib::AuthInfo;
use jwt_validator::JwtValidator;
use jwt_middleware::JwtMiddleware;
#[actix_web::main]
async fn main() -> std::io::Result<()> {
let validator = Arc::new(JwtValidator::new().await.expect("Failed to initialize JWT validator"));
HttpServer::new(move || {
App::new()
.app_data(web::Data::new(validator.clone()))
.wrap(Logger::default())
.service(
web::scope("/api/protected")
.wrap(JwtMiddleware::new(validator.clone()))
.route("", web::get().to(protected_handler))
.route("/detailed", web::get().to(detailed_handler))
)
})
.bind("127.0.0.1:8080")?
.run()
.await
}
async fn protected_handler(req: HttpRequest) -> Result<web::Json<Value>> {
// Zugriff auf Auth-Informationen aus den Request-Extensions
let auth = req.extensions().get::<AuthInfo>().unwrap();
Ok(web::Json(json!({ "auth": auth })))
}
async fn detailed_handler(req: HttpRequest) -> Result<web::Json<Value>> {
// Deine geschützte Endpunkt-Logik
let auth = req.extensions().get::<AuthInfo>().unwrap();
Ok(web::Json(json!({
"auth": auth,
"message": "Geschützte Daten erfolgreich abgerufen"
})))
}
use rocket::{get, launch, routes, serde::json::Json, State};
use serde_json::{json, Value};
mod lib;
mod jwt_validator;
mod guards;
use lib::AuthInfo;
use jwt_validator::JwtValidator;
#[get("/api/protected")]
fn protected_handler(auth: AuthInfo) -> Json<Value> {
// Zugriff auf Auth-Informationen direkt aus dem Request Guard
Json(json!({ "auth": auth }))
}
#[get("/api/protected/detailed")]
fn detailed_handler(auth: AuthInfo) -> Json<Value> {
// Deine geschützte Endpunkt-Logik
Json(json!({
"auth": auth,
"message": "Geschützte Daten erfolgreich abgerufen"
}))
}
#[launch]
async fn rocket() -> _ {
let validator = JwtValidator::new().await.expect("Failed to initialize JWT validator");
rocket::build()
.manage(validator)
.mount("/", routes![protected_handler, detailed_handler])
}
Schritt 5: Deine Implementierung testen
Teste deine API mit gültigen und ungültigen Tokens, um sicherzustellen:
- Gültige Tokens werden akzeptiert und gewähren Zugriff.
- Gib
401 Unauthorized
für ungültige / fehlende Tokens zurück. Gib403 Forbidden
für gültige Tokens zurück, denen die erforderlichen Berechtigungen oder der Kontext fehlen.
Verwandte Ressourcen
Token-Ansprüche anpassen JSON Web Token (JWT)OpenID Connect Discovery
RFC 8707: Ressourcenindikatoren