Wie du Zugangstokens in deinem API-Dienst oder Backend validierst
Die Validierung von Zugangstokens 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 / 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 durchgearbeitet hast.
- Wenn du In-App-Features oder Workflows (nicht-API-Berechtigungen) schützt, wird vorausgesetzt, dass du die Anleitung Organisations-(Nicht-API-)Berechtigungen schützen durchgearbeitet hast.
- Wenn du organisationsbezogene API-Ressourcen schützt, wird vorausgesetzt, dass du die Anleitung Organisationsbezogene API-Ressourcen schützen durchgearbeitet hast.
Schritt 1: Konstanten und Hilfsfunktionen initialisieren
Definiere die notwendigen Konstanten und Hilfsfunktionen in deinem Code, um die Extraktion und Validierung von Tokens zu handhaben. Eine gültige Anfrage muss einen Authorization
-Header in der Form Bearer <Zugangstoken (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 class AuthorizationException extends RuntimeException {
private final int statusCode;
public AuthorizationException(String message) {
this(message, 403); // Standardmäßig 403 Verboten (Forbidden)
}
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 über deinen 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 von JWT-Signaturen verwendet wird.
- Aussteller (Issuer): Der erwartete Ausstellerwert (die OIDC-URL von Logto).
Zuerst finde 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.
Abrufen vom OpenID Connect Discovery-Endpunkt
Diese Werte können vom OpenID Connect Discovery-Endpunkt von Logto abgerufen werden:
https://<your-logto-endpoint>/oidc/.well-known/openid-configuration
Hier ist ein Beispiel für eine Antwort (andere Felder wurden zur Übersichtlichkeit weggelassen):
{
"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 Ausstellers (Issuer) erlaubt, kannst du diese Werte fest in deinem Code hinterlegen. Dies wird jedoch für Produktionsanwendungen nicht empfohlen, da dies den Wartungsaufwand erhöhen kann, falls sich zukünftig Konfigurationen ändern.
- JWKS URI:
https://<your-logto-endpoint>/oidc/jwks
- Aussteller (Issuer):
https://<your-logto-endpoint>/oidc
Schritt 3: Das Token und die Berechtigungen validieren
Nach dem Extrahieren des Tokens und dem Abrufen der OIDC-Konfiguration überprüfe Folgendes:
- Signatur: JWT muss gültig und von Logto (über JWKS) signiert sein.
- Aussteller (Issuer): Muss mit dem Aussteller deines Logto-Tenants übereinstimmen.
- Zielgruppe (Audience): Muss mit dem in Logto registrierten Ressourcenindikator der API oder dem Organisationskontext (falls zutreffend) übereinstimmen.
- Ablauf (Expiration): Token darf nicht abgelaufen sein.
- Berechtigungen (Scopes): Token muss die erforderlichen Berechtigungen für deine API / Aktion enthalten. Berechtigungen sind durch Leerzeichen getrennte Zeichenfolgen im
scope
-Anspruch. - Organisationskontext: Wenn du API-Ressourcen auf Organisationsebene schützt, überprüfe den
organization_id
-Anspruch.
Siehe JSON Web Token, um mehr über die Struktur und Ansprüche von JWT zu erfahren.
Was bei jedem Berechtigungsmodell zu prüfen ist
- Globale API-Ressourcen
- Organisation (nicht-API) Berechtigungen
- API-Ressourcen auf Organisationsebene
- Audience-Anspruch (
aud
): API-Ressourcenindikator - Organisations-Anspruch (
organization_id
): Nicht vorhanden - Zu prüfende Berechtigungen (
scope
): API-Ressourcen-Berechtigungen
- Audience-Anspruch (
aud
):urn:logto:organization:<id>
(Organisationskontext ist imaud
-Anspruch) - Organisations-Anspruch (
organization_id
): Nicht vorhanden - Zu prüfende Berechtigungen (
scope
): Organisationsberechtigungen
- Audience-Anspruch (
aud
): API-Ressourcenindikator - Organisations-Anspruch (
organization_id
): Organisations-ID (muss mit Anfrage übereinstimmen) - Zu prüfende Berechtigungen (
scope
): API-Ressourcen-Berechtigungen
Für nicht-API-Organisationsberechtigungen wird der Organisationskontext durch den aud
-Anspruch
dargestellt (z. B. urn:logto:organization:abc123
). Der organization_id
-Anspruch ist nur für
Tokens von API-Ressourcen auf Organisationsebene vorhanden.
Validiere immer sowohl Berechtigungen (Scopes) als auch 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 validateJwt(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
- Fastify
- Hapi.js
- Koa.js
- NestJS
import { Request, Response, NextFunction } from 'express';
import { validateJwt, createAuthInfo } from './jwt-validator.js';
// Erweiterung der Express Request-Schnittstelle 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 validateJwt(token);
// Auth-Informationen 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 { FastifyRequest, FastifyReply } from 'fastify';
import { validateJwt, createAuthInfo } from './jwt-validator.js';
// Fastify Request-Interface erweitern, um auth einzuschließen
declare module 'fastify' {
interface FastifyRequest {
auth?: AuthInfo;
}
}
export async function fastifyVerifyAccessToken(request: FastifyRequest, reply: FastifyReply) {
try {
const token = extractBearerTokenFromHeaders(request.headers);
const payload = await validateJwt(token);
// Auth-Informationen 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 { validateJwt, createAuthInfo } from './jwt-validator.js';
export async function hapiVerifyAccessToken(request: Request, h: ResponseToolkit) {
try {
const token = extractBearerTokenFromHeaders(request.headers);
const payload = await validateJwt(token);
// Auth-Informationen im 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 { Context, Next } from 'koa';
import { validateJwt, createAuthInfo } from './jwt-validator.js';
export async function koaVerifyAccessToken(ctx: Context, next: Next) {
try {
const token = extractBearerTokenFromHeaders(ctx.request.headers);
const payload = await validateJwt(token);
// Auth-Informationen 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 {
Injectable,
CanActivate,
ExecutionContext,
UnauthorizedException,
ForbiddenException,
} from '@nestjs/common';
import { validateJwt, 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 validateJwt(token);
// Auth-Informationen 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);
}
}
}
Entsprechend deinem Berechtigungsmodell implementiere die passende Verifizierungslogik in jwt-validator.ts
:
- Globale API-Ressourcen
- Organisation (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 (du musst dies ggf. 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('Organisations-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 (du musst dies ggf. 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('Organisations-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: str) -> Dict[str, Any]:
"""JWT 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 Berechtigungsmodell überprüfen"""
# Implementiere hier deine Überprüfungslogik basierend auf dem Berechtigungsmodell
# Dies wird im Abschnitt zu den Berechtigungsmodellen unten gezeigt
pass
Implementiere anschließend 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, create_auth_info
security = HTTPBearer()
async def verify_access_token(credentials: HTTPAuthorizationCredentials = Depends(security)) -> AuthInfo:
try:
token = credentials.credentials
payload = validate_jwt(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, 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)
# Auth-Informationen 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, 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)
# Auth-Informationen an die Anfrage anhängen, um sie allgemein zu verwenden
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, 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(key)
auth_info = create_auth_info(payload)
# Erstelle ein benutzerähnliches Objekt, das Auth-Informationen 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
- Organisation (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 benötigten 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 (ggf. aus dem Request-Kontext extrahieren)
expected_org_id = 'your-organization-id' # Aus dem Request-Kontext 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 benötigten 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 (ggf. aus dem Request-Kontext extrahieren)
expected_org_id = 'your-organization-id' # Aus dem Request-Kontext 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 benötigten 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("Fehler beim Abrufen von JWKS: " + err.Error())
}
}
// validateJWT validiert das JWT und gibt das geparste Token zurück
func validateJWT(tokenString string) (jwt.Token, error) {
token, err := jwt.Parse([]byte(tokenString), jwt.WithKeySet(jwkSet))
if err != nil {
return nil, NewAuthorizationError("Ungültiges Token: "+err.Error(), http.StatusUnauthorized)
}
// Aussteller überprüfen
if token.Issuer() != ISSUER {
return nil, NewAuthorizationError("Ungültiger Aussteller", 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 zu überprüfen:
- Gin
- Fiber
- Echo
- 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 := validateJWT(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 (
"net/http"
"github.com/gofiber/fiber/v2"
)
func VerifyAccessToken(c *fiber.Ctx) error {
// Konvertiere die Fiber-Anfrage in eine http.Request 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 := validateJWT(tokenString)
if err != nil {
authErr := err.(*AuthorizationError)
return c.Status(authErr.Status).JSON(fiber.Map{"error": authErr.Message})
}
// Speichere das Token in Locals für generische Nutzung
c.Locals("auth", token)
return 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 := validateJWT(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 (
"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 := validateJWT(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 möglicherweise unterschiedliche verifyPayload
-Logik anwenden:
- Globale API-Ressourcen
- Organisation (nicht-API) Berechtigungen
- Organisationsbezogene API-Ressourcen
func verifyPayload(token jwt.Token) error {
// Überprüfe, ob der Audience-Anspruch mit deinem API-Ressourcenindikator übereinstimmt
if !hasAudience(token, "https://your-api-resource-indicator") {
return NewAuthorizationError("Ungültige Zielgruppe")
}
// Überprüfe erforderliche Berechtigungen für globale API-Ressourcen
requiredScopes := []string{"api:read", "api:write"} // Ersetze durch deine tatsächlichen erforderlichen Berechtigungen
if !hasRequiredScopes(token, requiredScopes) {
return NewAuthorizationError("Unzureichende Berechtigung")
}
return nil
}
func verifyPayload(token jwt.Token) error {
// Überprüfe, ob der Audience-Anspruch dem Organisationsformat entspricht
if !hasOrganizationAudience(token) {
return NewAuthorizationError("Ungültige Zielgruppe für Organisationsberechtigungen")
}
// Überprüfe, ob die Organisations-ID mit dem Kontext übereinstimmt (du musst dies ggf. aus dem Request-Kontext extrahieren)
expectedOrgID := "your-organization-id" // Aus dem Request-Kontext extrahieren
if !hasMatchingOrganization(token, expectedOrgID) {
return NewAuthorizationError("Organisation-ID stimmt nicht überein")
}
// Überprüfe erforderliche Organisationsberechtigungen
requiredScopes := []string{"invite:users", "manage:settings"} // Ersetze durch deine tatsächlichen erforderlichen Berechtigungen
if !hasRequiredScopes(token, requiredScopes) {
return NewAuthorizationError("Unzureichende Organisationsberechtigung")
}
return nil
}
func verifyPayload(token jwt.Token) error {
// Überprüfe, ob der Audience-Anspruch mit deinem API-Ressourcenindikator übereinstimmt
if !hasAudience(token, "https://your-api-resource-indicator") {
return NewAuthorizationError("Ungültige Zielgruppe für organisationsbezogene API-Ressourcen")
}
// Überprüfe, ob die Organisations-ID mit dem Kontext übereinstimmt (du musst dies ggf. aus dem Request-Kontext extrahieren)
expectedOrgID := "your-organization-id" // Aus dem Request-Kontext extrahieren
if !hasMatchingOrganizationID(token, expectedOrgID) {
return NewAuthorizationError("Organisation-ID stimmt nicht überein")
}
// Überprüfe erforderliche Berechtigungen für organisationsbezogene API-Ressourcen
requiredScopes := []string{"api:read", "api:write"} // Ersetze durch deine tatsächlichen erforderlichen Berechtigungen
if !hasRequiredScopes(token, requiredScopes) {
return NewAuthorizationError("Unzureichende organisationsbezogene API-Berechtigungen")
}
return nil
}
Füge diese Hilfsfunktionen zur Payload-Überprüfung hinzu:
// hasAudience prüft, ob das Token die angegebene Zielgruppe 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 Zielgruppe im Organisationsformat enthält
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 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 Token-Zielgruppe 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 dies 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>
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() {
// Denke daran, diese Umgebungsvariablen in deiner Bereitstellung zu setzen
String jwksUri = System.getenv("JWKS_URI");
String issuer = System.getenv("JWT_ISSUER");
return NimbusJwtDecoder.withJwkSetUri(jwksUri)
.issuer(issuer)
.build();
}
}
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.stereotype.Component;
import java.util.List;
@Component
public class JwtValidator {
public void verifyPayload(Jwt jwt) {
// Die Aussteller-Validierung (Issuer validation) wird automatisch vom Spring Security JWT Decoder übernommen
// Implementiere hier deine zusätzliche Verifizierungslogik basierend auf dem Berechtigungsmodell
// Verwende die untenstehenden Hilfsmethoden zur Anspruchsextraktion (Claim extraction)
// Beispiel: throw new AuthorizationException("Unzureichende Berechtigungen");
// Der Statuscode wird durch das Exception-Handling von Spring Security behandelt
}
// Hilfsmethoden für Spring Boot JWT
private List<String> extractAudiences(Jwt jwt) {
return jwt.getAudience();
}
private String extractScopes(Jwt jwt) {
return jwt.getClaimAsString("scope");
}
private String extractOrganizationId(Jwt jwt) {
return jwt.getClaimAsString("organization_id");
}
}
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>
# JWT-Konfiguration
mp.jwt.verify.publickey.location=${JWKS_URI:https://your-tenant.logto.app/oidc/jwks}
mp.jwt.verify.issuer=${JWT_ISSUER:https://your-tenant.logto.app/oidc}
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.core.Response;
import jakarta.ws.rs.ext.Provider;
import java.util ArrayList;
import java.util.List;
@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);
requestContext.setProperty("auth", jwt);
} catch (AuthorizationException e) {
requestContext.abortWith(
Response.status(e.getStatusCode())
.entity("{\"error\": \"" + e.getMessage() + "\"}")
.build()
);
} catch (Exception e) {
requestContext.abortWith(
Response.status(401)
.entity("{\"error\": \"Ungültiges Token\"}")
.build()
);
}
}
}
private void verifyPayload(JsonWebToken jwt) {
// Die Aussteller (Issuer)-Validierung wird automatisch von der Quarkus JWT-Erweiterung übernommen
// Implementiere hier deine zusätzliche Verifizierungslogik basierend auf dem Berechtigungsmodell
// Verwende die untenstehenden Hilfsmethoden zur Anspruchsextraktion
}
// Hilfsmethoden für Quarkus JWT
private List<String> extractAudiences(JsonWebToken jwt) {
return new ArrayList<>(jwt.getAudience());
}
private String extractScopes(JsonWebToken jwt) {
return jwt.getClaim("scope");
}
private String extractOrganizationId(JsonWebToken jwt) {
return jwt.getClaim("organization_id");
}
}
Füge dies 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>
micronaut:
security:
authentication: bearer
token:
jwt:
signatures:
jwks:
logto:
url: ${JWKS_URI:https://your-tenant.logto.app/oidc/jwks}
claims-validators:
issuer: ${JWT_ISSUER:https://your-tenant.logto.app/oidc}
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;
import java.util.Arrays;
import java.util.List;
@Singleton
public class JwtClaimsValidator implements TokenValidator {
@Override
public Publisher<Boolean> validateToken(String token, Claims claims) {
try {
verifyPayload(claims);
return Mono.just(true);
} catch (AuthorizationException e) {
// Micronaut wird den Statuscode entsprechend behandeln
return Mono.just(false);
}
}
private void verifyPayload(Claims claims) {
// Die Aussteller (Issuer)-Validierung wird automatisch durch die Micronaut JWT-Konfiguration übernommen
// Implementiere hier deine zusätzliche Verifizierungslogik basierend auf dem Berechtigungsmodell
// Verwende die untenstehenden Hilfsmethoden zur Anspruchsextraktion
// Beispiel: throw new AuthorizationException("Unzureichende Berechtigungen");
}
// Hilfsmethoden für Micronaut JWT
@SuppressWarnings("unchecked")
private List<String> extractAudiences(Claims claims) {
Object aud = claims.get("aud");
if (aud instanceof List) {
return (List<String>) aud;
} else if (aud instanceof String) {
return Arrays.asList((String) aud);
}
return List.of();
}
private String extractScopes(Claims claims) {
return (String) claims.get("scope");
}
private String extractOrganizationId(Claims claims) {
return (String) claims.get("organization_id");
}
}
Füge dies 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>
import io.vertx.core.Future;
import io.vertx.core.Handler;
import io.vertx.core.Vertx;
import io.vertx.core.json.JsonArray;
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;
import java.util.List;
import java.util.ArrayList;
public class JwtAuthHandler implements Handler<RoutingContext> {
private final JWTAuth jwtAuth;
private final WebClient webClient;
private final String expectedIssuer;
private final String jwksUri;
public JwtAuthHandler(Vertx vertx) {
this.webClient = WebClient.create(vertx);
this.jwtAuth = JWTAuth.create(vertx, new JWTAuthOptions());
// Denke daran, diese Umgebungsvariablen in deiner Bereitstellung zu setzen
this.expectedIssuer = System.getenv("JWT_ISSUER");
this.jwksUri = System.getenv("JWKS_URI");
// JWKS abrufen und JWT-Authentifizierung konfigurieren
fetchJWKS().onSuccess(jwks -> {
// JWKS konfigurieren (vereinfacht – du benötigst möglicherweise einen richtigen JWKS-Parser)
});
}
@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("jwt", token))
.onSuccess(user -> {
try {
JsonObject principal = user.principal();
verifyPayload(principal);
context.put("auth", principal);
context.next();
} catch (AuthorizationException e) {
context.response()
.setStatusCode(e.getStatusCode()) // Verwende den Statuscode der Exception
.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: " + err.getMessage() + "\"}");
});
}
private Future<JsonObject> fetchJWKS() {
return webClient.getAbs(this.jwksUri)
.send()
.map(response -> response.bodyAsJsonObject());
}
private void verifyPayload(JsonObject principal) {
// Aussteller (Issuer) manuell für Vert.x überprüfen
String issuer = principal.getString("iss");
if (issuer == null || !expectedIssuer.equals(issuer)) {
throw new AuthorizationException("Invalid issuer: " + issuer);
}
// Implementiere hier deine zusätzliche Überprüfungslogik basierend auf dem Berechtigungsmodell
// Verwende die untenstehenden Hilfsmethoden für das Extrahieren von Ansprüchen (Claims)
}
// Hilfsmethoden für Vert.x JWT
private List<String> extractAudiences(JsonObject principal) {
JsonArray audiences = principal.getJsonArray("aud");
if (audiences != null) {
List<String> result = new ArrayList<>();
for (Object aud : audiences) {
result.add(aud.toString());
}
return result;
}
return List.of();
}
private String extractScopes(JsonObject principal) {
return principal.getString("scope");
}
private String extractOrganizationId(JsonObject principal) {
return principal.getString("organization_id");
}
}
Implementiere entsprechend deinem Berechtigungsmodell die passende Prüf-Logik:
- Globale API-Ressourcen
- Organisations-(Nicht-API)-Berechtigungen
- Organisationsbezogene API-Ressourcen
// Überprüfe, ob der Audience-Anspruch mit deinem API-Ressourcenindikator übereinstimmt
List<String> audiences = extractAudiences(token); // Framework-spezifische Extraktion
if (!audiences.contains("https://your-api-resource-indicator")) {
throw new AuthorizationException("Ungültige Zielgruppe");
}
// Überprüfe die erforderlichen Berechtigungen für globale API-Ressourcen
List<String> requiredScopes = Arrays.asList("api:read", "api:write"); // Ersetze durch deine tatsächlichen erforderlichen Berechtigungen
String scopes = extractScopes(token); // Framework-spezifische Extraktion
List<String> tokenScopes = scopes != null ? Arrays.asList(scopes.split(" ")) : List.of();
if (!tokenScopes.containsAll(requiredScopes)) {
throw new AuthorizationException("Unzureichende Berechtigung");
}
// Überprüfe, ob der Audience-Anspruch dem Organisationsformat entspricht
List<String> audiences = extractAudiences(token); // Framework-spezifische Extraktion
boolean hasOrgAudience = audiences.stream()
.anyMatch(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 (du musst sie 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("Organisations-ID stimmt nicht überein");
}
// Überprüfe die erforderlichen Organisationsberechtigungen
List<String> requiredScopes = Arrays.asList("invite:users", "manage:settings"); // Ersetze durch deine tatsächlichen erforderlichen Berechtigungen
String scopes = extractScopes(token); // Framework-spezifische Extraktion
List<String> tokenScopes = scopes != null ? Arrays.asList(scopes.split(" ")) : List.of();
if (!tokenScopes.containsAll(requiredScopes)) {
throw new AuthorizationException("Unzureichende Organisationsberechtigung");
}
// Überprüfe, ob der Audience-Anspruch mit deinem API-Ressourcenindikator übereinstimmt
List<String> audiences = extractAudiences(token); // Framework-spezifische Extraktion
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 (du musst sie ggf. aus dem Request-Kontext extrahieren)
String expectedOrgId = "your-organization-id"; // Aus dem Request-Kontext extrahieren
String orgId = extractOrganizationId(token); // Framework-spezifische Extraktion
if (!expectedOrgId.equals(orgId)) {
throw new AuthorizationException("Organisations-ID stimmt nicht überein");
}
// Überprüfe die erforderlichen Berechtigungen für organisationsbezogene API-Ressourcen
List<String> requiredScopes = Arrays.asList("api:read", "api:write"); // Ersetze durch deine tatsächlichen erforderlichen Berechtigungen
String scopes = extractScopes(token); // Framework-spezifische Extraktion
List<String> tokenScopes = scopes != null ? Arrays.asList(scopes.split(" ")) : List.of();
if (!tokenScopes.containsAll(requiredScopes)) {
throw new AuthorizationException("Unzureichende organisationsbezogene API-Berechtigungen");
}
Die Hilfsmethoden zum Extrahieren von Ansprüchen sind framework-spezifisch. Siehe die Implementierungsdetails in den oben genannten framework-spezifischen Validierungsdateien.
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:
composer require firebase/php-jwt
Füge zunächst diese gemeinsamen Hilfsfunktionen hinzu, um die JWT-Validierung zu behandeln:
<?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 validateJwt(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 dann das Middleware, um das Zugangstoken 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::validateJwt($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 die 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::validateJwt($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::validateJwt($token);
// Authentifizierungsinformationen 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);
}
}
}
Entsprechend deinem Berechtigungsmodell implementiere 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 (audience claim) 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');
}
// Überprüfe erforderliche Berechtigungen (Scopes) für globale API-Ressourcen
$requiredScopes = ['api:read', 'api:write']; // Ersetze dies durch deine tatsächlich erforderlichen Berechtigungen
$scopes = !empty($payload['scope']) ? explode(' ', $payload['scope']) : [];
foreach ($requiredScopes as $scope) {
if (!in_array($scope, $scopes)) {
throw new AuthorizationException('Unzureichende Berechtigung');
}
}
}
private static function verifyPayload(array $payload): void
{
// Überprüfe, ob der Audience-Anspruch 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 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 Organisations-Berechtigungen (Scopes)
$requiredScopes = ['invite:users', 'manage:settings']; // Ersetze dies durch deine tatsächlich erforderlichen Berechtigungen
$scopes = !empty($payload['scope']) ? explode(' ', $payload['scope']) : [];
foreach ($requiredScopes as $scope) {
if (!in_array($scope, $scopes)) {
throw new AuthorizationException('Unzureichende Organisationsberechtigung');
}
}
}
private static function verifyPayload(array $payload): void
{
// Überprüfe, ob der Audience-Anspruch (audience claim) 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 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 dies durch deine tatsächlich erforderlichen Berechtigungen
$scopes = !empty($payload['scope']) ? explode(' ', $payload['scope']) : [];
foreach ($requiredScopes as $scope) {
if (!in_array($scope, $scopes)) {
throw new AuthorizationException('Unzureichende Berechtigungen für organisationsbezogene API-Ressourcen');
}
}
}
Wir verwenden das jwt Gem, um JWTs zu validieren. Füge es zu 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)
jwks = fetch_jwks
# Die JWT-Bibliothek erkennt den Algorithmus automatisch aus dem 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]
verify_payload(decoded_token)
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 anschließend die Middleware, um das Zugangstoken zu überprüfen:
- Ruby on 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)
# Auth-Informationen 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: 'Ungültiges 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 bestimmte Routen
if request.path.start_with?('/api/protected')
begin
token = extract_bearer_token(request)
decoded_token = JwtValidator.validate_jwt(token)
# Auth-Informationen 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: 'Ungültiges 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)
# Auth-Informationen für die allgemeine Verwendung 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: 'Ungültiges Token' }, 401)
end
end
def auth
@auth
end
end
Entsprechend deinem Berechtigungsmodell implementiere die passende Überprüfungslogik in JwtValidator
:
- Globale API-Ressourcen
- Organisations-(Nicht-API)-Berechtigungen
- Organisationsbezogene API-Ressourcen
def self.verify_payload(payload)
# Überprüfe, ob der Audience-Claim 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-Claim 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-Claim 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:
[dependencies]
jsonwebtoken = "9.0"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
reqwest = { version = "0.11", features = ["json"] }
tokio = { version = "1.0", features = ["full"] }
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(&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 den 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 das 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;
// JWT-Middleware zur Überprüfung und Speicherung von Authentifizierungsinformationen
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)?;
// Authentifizierungsinformationen in den Request-Extensions für generische Nutzung speichern
request.extensions_mut().insert(auth_info);
Ok(next.run(request).await)
}
// Fehlerbehandlung für Autorisierungsfehler
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))
{
Ok(auth_info) => {
// Auth-Informationen 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))
{
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))
}
}
}
}
Entsprechend deinem Berechtigungsmodell implementiere die passende 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ächlich benötigten 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ächlich benötigten 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ächlich benötigten 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();
server.get('/api/protected', { preHandler: fastifyVerifyAccessToken }, (request, reply) => {
// Greife direkt über request.auth auf Authentifizierungsinformationen zu
reply.send({ auth: request.auth });
});
server.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()}
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():
# Zugriff auf Authentifizierungsinformationen aus g.auth
return jsonify({"auth": g.auth.to_dict()})
from django.http import JsonResponse
from auth_middleware import require_access_token
@require_access_token
def protected_view(request):
# Zugriff auf Authentifizierungsinformationen über request.auth
return JsonResponse({"auth": request.auth.to_dict()})
from django.urls import path
from . import views
urlpatterns = [
path('api/protected/', views.protected_view, name='protected'),
]
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):
# Zugriff auf Authentifizierungsinformationen über request.user.auth
return Response({"auth": request.user.auth.to_dict()})
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):
# Zugriff auf Authentifizierungsinformationen über request.user.auth
return Response({"auth": request.user.auth.to_dict()})
from django.urls import path
from . import views
urlpatterns = [
path('api/protected/', views.protected_view, name='protected'),
# Oder für klassenbasierte Views:
# path('api/protected/', views.ProtectedView.as_view(), name='protected'),
]
- 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 (Access token) Informationen direkt aus dem Kontext abrufen
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 abrufen
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 abrufen
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 abrufen
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 abrufen
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) {
// Zugangstoken-Informationen direkt aus dem JWT abrufen
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()
);
}
}
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 das JWT direkt über Injection oder Kontext
JsonWebToken token = (JsonWebToken) requestContext.getProperty("auth");
if (token == null) {
token = jwt; // Rückgriff 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()
);
}
}
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) {
// Zugangstoken-Informationen direkt aus Authentication abrufen
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")
);
}
}
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);
vertx.createHttpServer()
.requestHandler(router)
.listen(8080, result -> {
if (result.succeeded()) {
startPromise.complete();
} else {
startPromise.fail(result.cause());
}
});
}
private void protectedEndpoint(RoutingContext context) {
// Zugriff auf das JWT-Prinzipal 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 not found\"}");
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());
}
}
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()];
});
});
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 den geschützten Endpunkt
$auth = $request->attributes->get('auth');
return [
'auth' => $auth->toArray(),
'message' => 'Geschützte Daten wurden 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()]);
}
}
<?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 den geschützten Endpunkt
$auth = $request->getAttribute('auth');
$data = [
'auth' => $auth->toArray(),
'message' => 'Geschützte Daten wurden erfolgreich abgerufen'
];
$response->getBody()->write(json_encode($data));
return $response->withHeader('Content-Type', 'application/json');
}
}
- Ruby on 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
# Zugriff auf Auth-Informationen über @auth
render json: { auth: @auth.to_h }
end
end
Rails.application.routes.draw do
namespace :api do
resources :protected, only: [:index]
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
# Authentifizierungsinformationen aus env abrufen
auth = env['auth']
{ auth: auth.to_h }.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
# Zugriff auf Authentifizierungsinformationen aus dem Auth-Helper
{ auth: auth.to_h }
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("Initialisierung des JWT-Validators fehlgeschlagen"));
let app = Router::new()
.route("/api/protected", get(protected_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> {
// Greife direkt über Extension auf Auth-Informationen zu
Json(json!({ "auth": auth }))
}
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("Fehler beim Initialisieren des JWT-Validators"));
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))
)
})
.bind("127.0.0.1:8080")?
.run()
.await
}
async fn protected_handler(req: HttpRequest) -> Result<web::Json<Value>> {
// Zugriff auf Authentifizierungsinformationen aus den Erweiterungen der Anfrage
let auth = req.extensions().get::<AuthInfo>().unwrap();
Ok(web::Json(json!({ "auth": auth })))
}
use rocket::{get, launch, routes, serde::json::Json};
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> {
// Greife direkt über den Request Guard auf Authentifizierungsinformationen zu
Json(json!({ "auth": auth }))
}
#[launch]
async fn rocket() -> _ {
let validator = JwtValidator::new().await.expect("Initialisierung des JWT-Validators fehlgeschlagen");
rocket::build()
.manage(validator)
.mount("/", routes![protected_handler])
}
Schritt 5: Deine Implementierung testen
Zugangstokens erhalten
Von deiner Client-Anwendung: Wenn du eine Client-Integration eingerichtet hast, kann deine App Tokens automatisch erhalten. Extrahiere das Zugangstoken und verwende es in API-Anfragen.
Zum Testen mit curl / Postman:
-
Benutzertokens: Verwende die Entwicklertools deiner Client-App, um das Zugangstoken aus dem localStorage oder dem Netzwerk-Tab zu kopieren.
-
Maschine-zu-Maschine-Tokens: Verwende den Client-Credentials-Flow. Hier ein nicht-normatives Beispiel mit curl:
curl -X POST https://your-tenant.logto.app/oidc/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=client_credentials" \
-d "client_id=your-m2m-client-id" \
-d "client_secret=your-m2m-client-secret" \
-d "resource=https://your-api-resource-indicator" \
-d "scope=api:read api:write"Möglicherweise musst du die Parameter
resource
undscope
entsprechend deiner API-Ressource und Berechtigungen anpassen; einorganization_id
-Parameter kann ebenfalls erforderlich sein, wenn deine API organisationsgebunden ist.
Möchtest du den Inhalt des Tokens inspizieren? Verwende unseren JWT Decoder, um deine JWTs zu dekodieren und zu überprüfen.
Geschützte Endpunkte testen
Gültige Token-Anfrage
curl -H "Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..." \
http://localhost:3000/api/protected
Erwartete Antwort:
{
"auth": {
"sub": "user123",
"clientId": "app456",
"organizationId": "org789",
"scopes": ["api:read", "api:write"],
"audience": ["https://your-api-resource-indicator"]
}
}
Fehlendes Token
curl http://localhost:3000/api/protected
Erwartete Antwort (401):
{
"error": "Authorization header is missing"
}
Ungültiges Token
curl -H "Authorization: Bearer invalid-token" \
http://localhost:3000/api/protected
Erwartete Antwort (401):
{
"error": "Invalid token"
}
Berechtigungsmodell-spezifisches Testen
- Globale API-Ressourcen
- Organisations-(Nicht-API)-Berechtigungen
- Organisationsgebundene API-Ressourcen
Testszenarien für APIs, die mit globalen Berechtigungen geschützt sind:
- Gültige Berechtigungen: Teste mit Tokens, die deine erforderlichen API-Berechtigungen enthalten (z. B.
api:read
,api:write
) - Fehlende Berechtigungen: Erwarte 403 Verboten, wenn das Token die erforderlichen Berechtigungen nicht enthält
- Falsche Zielgruppe: Erwarte 403 Verboten, wenn die Zielgruppe nicht mit der API-Ressource übereinstimmt
# Token mit fehlenden Berechtigungen - erwarte 403
curl -H "Authorization: Bearer token-without-required-scopes" \
http://localhost:3000/api/protected
Testszenarien für organisationsspezifische Zugangskontrolle:
- Gültiges Organisationstoken: Teste mit Tokens, die den korrekten Organisationskontext enthalten (Organisations-ID und Berechtigungen)
- Fehlende Berechtigungen: Erwarte 403 Verboten, wenn der Benutzer keine Berechtigungen für die angeforderte Aktion hat
- Falsche Organisation: Erwarte 403 Verboten, wenn die Zielgruppe nicht mit dem Organisationskontext übereinstimmt (
urn:logto:organization:<organization_id>
)
# Token für falsche Organisation - erwarte 403
curl -H "Authorization: Bearer token-for-different-organization" \
http://localhost:3000/api/protected
Testszenarien, die API-Ressourcen-Validierung mit Organisationskontext kombinieren:
- Gültige Organisation + API-Berechtigungen: Teste mit Tokens, die sowohl den Organisationskontext als auch die erforderlichen API-Berechtigungen enthalten
- Fehlende API-Berechtigungen: Erwarte 403 Verboten, wenn das Organisationstoken die erforderlichen API-Berechtigungen nicht enthält
- Falsche Organisation: Erwarte 403 Verboten, wenn auf die API mit einem Token aus einer anderen Organisation zugegriffen wird
- Falsche Zielgruppe: Erwarte 403 Verboten, wenn die Zielgruppe nicht mit der organisationsgebundenen API-Ressource übereinstimmt
# Organisationstoken ohne API-Berechtigungen - erwarte 403
curl -H "Authorization: Bearer organization-token-without-api-scopes" \
http://localhost:3000/api/protected
Verwandte Ressourcen
Token-Ansprüche anpassen JSON Web Token (JWT)OpenID Connect Discovery
RFC 8707: Ressourcenindikatoren