Comment valider les jetons d’accès dans votre service API ou backend
La validation des jetons d’accès (Access tokens) est une étape essentielle pour appliquer le contrôle d’accès basé sur les rôles (RBAC) dans Logto. Ce guide vous explique comment vérifier les JWT émis par Logto dans votre backend / API, en contrôlant la signature, l’émetteur (Issuer), l’audience (Audience), l’expiration, les permissions (Portées / Scopes) et le contexte d’organisation.
Avant de commencer
- Ce guide suppose que vous êtes familier avec les concepts RBAC de Logto.
- Si vous protégez des ressources API, ce guide suppose que vous avez suivi le guide Protéger les ressources API globales.
- Si vous protégez des fonctionnalités ou des workflows internes à l’application (permissions non-API), ce guide suppose que vous avez suivi le guide Protéger les permissions d’organisation (non-API).
- Si vous protégez des ressources API au niveau organisation, ce guide suppose que vous avez suivi le guide Protéger les ressources API au niveau organisation.
Étape 1 : Initialiser les constantes et utilitaires
Définissez les constantes et utilitaires nécessaires dans votre code pour gérer l’extraction et la validation du jeton. Une requête valide doit inclure un en-tête Authorization
sous la forme Bearer <access_token>
.
- 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('L’en-tête Authorization est manquant', 401);
}
if (!authorization.startsWith(bearerPrefix)) {
throw new AuthorizationError(
`L’en-tête Authorization doit commencer par "${bearerPrefix}"`,
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:
"""
Extraire le jeton bearer des en-têtes HTTP.
Remarque : FastAPI et Django REST Framework disposent d'une extraction de jeton intégrée,
donc cette fonction est principalement destinée à Flask et à d'autres frameworks.
"""
authorization = headers.get('authorization') or headers.get('Authorization')
if not authorization:
raise AuthorizationError('L’en-tête Authorization est manquant', 401)
if not authorization.startswith('Bearer '):
raise AuthorizationError('L’en-tête Authorization doit commencer par "Bearer "', 401)
return authorization[7:] # Supprimer le préfixe '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 // Par défaut à 403 Interdit
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("L'en-tête Authorization est manquant", http.StatusUnauthorized)
}
if !strings.HasPrefix(authorization, bearerPrefix) {
return "", NewAuthorizationError(fmt.Sprintf("L'en-tête Authorization doit commencer par \"%s\"", bearerPrefix), http.StatusUnauthorized)
}
return strings.TrimPrefix(authorization, bearerPrefix), nil
}
public final class AuthConstants {
public static final String JWKS_URI = "https://your-tenant.logto.app/oidc/jwks";
public static final String ISSUER = "https://your-tenant.logto.app/oidc";
private AuthConstants() {
// Empêcher l'instanciation (Prevent instantiation)
}
}
public class AuthorizationException extends RuntimeException {
private final int statusCode;
public AuthorizationException(String message) {
this(message, 403); // Par défaut à 403 Interdit (Default to 403 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('L’en-tête Authorization est manquant', 401);
}
if (!str_starts_with($authorization, 'Bearer ')) {
throw new AuthorizationException('L’en-tête Authorization doit commencer par "Bearer "', 401);
}
return substr($authorization, 7); // Supprimer le préfixe 'Bearer '
}
}
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 is missing', 401) unless authorization
raise AuthorizationError.new('Authorization header must start with "Bearer "', 401) unless authorization.start_with?('Bearer ')
authorization[7..-1] # Supprimer le préfixe '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("Le header Authorization est manquant (Authorization header is missing)", 401)
})?;
if !auth_header.starts_with("Bearer ") {
return Err(AuthorizationError::with_status(
"Le header Authorization doit commencer par \"Bearer \" (Authorization header must start with \"Bearer \")",
401,
));
}
Ok(&auth_header[7..]) // Supprime le préfixe 'Bearer ' (Remove 'Bearer ' prefix)
}
Étape 2 : Récupérer les informations sur votre tenant Logto
Vous aurez besoin des valeurs suivantes pour valider les jetons émis par Logto :
- URI JSON Web Key Set (JWKS) : L’URL vers les clés publiques de Logto, utilisée pour vérifier la signature des JWT.
- Émetteur (Issuer) : La valeur attendue de l’émetteur (URL OIDC de Logto).
Commencez par trouver l’endpoint de votre tenant Logto. Vous pouvez le trouver à différents endroits :
- Dans la Console Logto, sous Paramètres → Domaines.
- Dans les paramètres de toute application configurée dans Logto, Paramètres → Endpoints & Credentials.
Récupérer depuis l’endpoint de découverte OpenID Connect
Ces valeurs peuvent être récupérées depuis l’endpoint de découverte OpenID Connect de Logto :
https://<your-logto-endpoint>/oidc/.well-known/openid-configuration
Voici un exemple de réponse (autres champs omis pour la clarté) :
{
"jwks_uri": "https://your-tenant.logto.app/oidc/jwks",
"issuer": "https://your-tenant.logto.app/oidc"
}
Codage en dur dans votre code (non recommandé)
Puisque Logto ne permet pas de personnaliser l’URI JWKS ou l’émetteur, vous pouvez coder ces valeurs en dur dans votre code. Cependant, cela n’est pas recommandé pour les applications en production car cela peut augmenter la maintenance si la configuration change à l’avenir.
- JWKS URI :
https://<your-logto-endpoint>/oidc/jwks
- Émetteur :
https://<your-logto-endpoint>/oidc
Étape 3 : Valider le jeton et les permissions
Après avoir extrait le jeton et récupéré la configuration OIDC, validez les éléments suivants :
- Signature : Le JWT doit être valide et signé par Logto (via JWKS).
- Émetteur (Issuer) : Doit correspondre à l’émetteur de votre tenant Logto.
- Audience (Audience) : Doit correspondre à l’indicateur de ressource API enregistré dans Logto, ou au contexte d’organisation si applicable.
- Expiration : Le jeton ne doit pas être expiré.
- Permissions (Portées / Scopes) : Le jeton doit inclure les portées requises pour votre API / action. Les portées sont des chaînes séparées par des espaces dans la revendication
scope
. - Contexte d’organisation : Si vous protégez des ressources API au niveau organisation, validez la revendication
organization_id
.
Voir JSON Web Token pour en savoir plus sur la structure et les revendications des JWT.
À vérifier selon chaque modèle de permission
Les revendications et règles de validation diffèrent selon le modèle de permission :
Modèle de permission | Revendication Audience (aud ) | Revendication Organisation (organization_id ) | Portées (permissions) à vérifier (scope ) |
---|---|---|---|
Ressources API globales | Indicateur de ressource API | Non présent | Permissions de ressource API |
Permissions d’organisation (non-API) | urn:logto:organization:<id> (le contexte d’organisation est dans aud ) | Non présent | Permissions d’organisation |
Ressources API au niveau organisation | Indicateur de ressource API | ID de l’organisation (doit correspondre à la requête) | Permissions de ressource API |
Pour les permissions d’organisation non-API, le contexte d’organisation est représenté par la
revendication aud
(par exemple, urn:logto:organization:abc123
). La revendication
organization_id
n’est présente que pour les jetons de ressource API au niveau organisation.
Validez toujours à la fois les permissions (portées) et le contexte (audience, organisation) pour des APIs multi-tenant sécurisées.
Ajouter la logique de validation
- Node.js
- Python
- Go
- Java
- .NET
- PHP
- Ruby
- Rust
Nous utilisons jose dans cet exemple pour valider le JWT. Installez-le si ce n'est pas déjà fait :
npm install jose
Ou utilisez votre gestionnaire de paquets préféré (par exemple, pnpm
ou yarn
).
Commencez par ajouter ces utilitaires partagés pour gérer la validation du JWT :
import { createRemoteJWKSet, jwtVerify, JWTPayload } from 'jose';
import { AuthInfo, AuthorizationError } from './auth-middleware.js';
const jwks = createRemoteJWKSet(new URL(JWKS_URI));
export async function validateJwtToken(token: string): Promise<JWTPayload> {
const { payload } = await jwtVerify(token, jwks, {
issuer: ISSUER,
});
verifyPayload(payload);
return payload;
}
export function createAuthInfo(payload: JWTPayload): AuthInfo {
const scopes = (payload.scope as string)?.split(' ') ?? [];
const audience = Array.isArray(payload.aud) ? payload.aud : payload.aud ? [payload.aud] : [];
return new AuthInfo(
payload.sub!,
payload.client_id as string,
payload.organization_id as string,
scopes,
audience
);
}
function verifyPayload(payload: JWTPayload): void {
// Implémentez ici votre logique de vérification basée sur le modèle de permission
// Ceci sera montré dans la section sur les modèles de permission ci-dessous
}
Ensuite, implémentez le middleware pour vérifier le jeton d’accès (Access token) :
- Express.js
- Koa.js
- Fastify
- Hapi.js
- NestJS
import { Request, Response, NextFunction } from 'express';
import { extractBearerTokenFromHeaders, AuthorizationError } from './auth-middleware.js';
import { validateJwtToken, createAuthInfo } from './jwt-validator.js';
// Étendre l’interface Request d’Express pour inclure auth
declare global {
namespace Express {
interface Request {
auth?: AuthInfo;
}
}
}
export async function verifyAccessToken(req: Request, res: Response, next: NextFunction) {
try {
const token = extractBearerTokenFromHeaders(req.headers);
const payload = await validateJwtToken(token);
// Stocker les infos d’auth dans la requête pour un usage générique
req.auth = createAuthInfo(payload);
next();
} catch (err: any) {
return res.status(err.status ?? 401).json({ error: err.message });
}
}
import { Context, Next } from 'koa';
import { extractBearerTokenFromHeaders, AuthorizationError } from './auth-middleware.js';
import { validateJwtToken, createAuthInfo } from './jwt-validator.js';
export async function koaVerifyAccessToken(ctx: Context, next: Next) {
try {
const token = extractBearerTokenFromHeaders(ctx.request.headers);
const payload = await validateJwtToken(token);
// Stocker les infos d’auth dans state pour un usage générique
ctx.state.auth = createAuthInfo(payload);
await next();
} catch (err: any) {
ctx.status = err.status ?? 401;
ctx.body = { error: err.message };
}
}
import { FastifyRequest, FastifyReply } from 'fastify';
import { extractBearerTokenFromHeaders, AuthorizationError } from './auth-middleware.js';
import { validateJwtToken, createAuthInfo } from './jwt-validator.js';
// Étendre l’interface Request de Fastify pour inclure auth
declare module 'fastify' {
interface FastifyRequest {
auth?: AuthInfo;
}
}
export async function fastifyVerifyAccessToken(request: FastifyRequest, reply: FastifyReply) {
try {
const token = extractBearerTokenFromHeaders(request.headers);
const payload = await validateJwtToken(token);
// Stocker les infos d’auth dans la requête pour un usage générique
request.auth = createAuthInfo(payload);
} catch (err: any) {
reply.code(err.status ?? 401).send({ error: err.message });
}
}
import { Request, ResponseToolkit } from '@hapi/hapi';
import { extractBearerTokenFromHeaders, AuthorizationError } from './auth-middleware.js';
import { validateJwtToken, createAuthInfo } from './jwt-validator.js';
export async function hapiVerifyAccessToken(request: Request, h: ResponseToolkit) {
try {
const token = extractBearerTokenFromHeaders(request.headers);
const payload = await validateJwtToken(token);
// Stocker les infos d’auth dans request.app pour un usage générique
request.app.auth = createAuthInfo(payload);
return h.continue;
} catch (err: any) {
return h
.response({ error: err.message })
.code(err.status ?? 401)
.takeover();
}
}
import {
Injectable,
CanActivate,
ExecutionContext,
UnauthorizedException,
ForbiddenException,
} from '@nestjs/common';
import { extractBearerTokenFromHeaders, AuthorizationError } from './auth-middleware.js';
import { validateJwtToken, createAuthInfo } from './jwt-validator.js';
@Injectable()
export class AccessTokenGuard implements CanActivate {
async canActivate(context: ExecutionContext): Promise<boolean> {
const req = context.switchToHttp().getRequest();
try {
const token = extractBearerTokenFromHeaders(req.headers);
const payload = await validateJwtToken(token);
// Stocker les infos d’auth dans la requête pour un usage générique
req.auth = createAuthInfo(payload);
return true;
} catch (err: any) {
if (err.status === 401) throw new UnauthorizedException(err.message);
throw new ForbiddenException(err.message);
}
}
}
Selon votre modèle de permission, implémentez la logique de vérification appropriée dans jwt-validator.ts
:
- Ressources API globales
- Permissions d’organisation (hors API)
- Ressources API au niveau organisation
function verifyPayload(payload: JWTPayload): void {
// Vérifier que la revendication audience correspond à votre indicateur de ressource API
const audiences = Array.isArray(payload.aud) ? payload.aud : payload.aud ? [payload.aud] : [];
if (!audiences.includes('https://your-api-resource-indicator')) {
throw new AuthorizationError('Audience invalide');
}
// Vérifier les portées requises pour les ressources API globales
const requiredScopes = ['api:read', 'api:write']; // Remplacez par vos portées requises
const scopes = (payload.scope as string)?.split(' ') ?? [];
if (!requiredScopes.every((scope) => scopes.includes(scope))) {
throw new AuthorizationError('Portée insuffisante');
}
}
function verifyPayload(payload: JWTPayload): void {
// Vérifier que la revendication audience correspond au format d’organisation
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('Audience invalide pour les permissions d’organisation');
}
// Vérifier que l’ID d’organisation correspond au contexte (vous devrez peut-être l’extraire du contexte de la requête)
const expectedOrgId = 'your-organization-id'; // À extraire du contexte de la requête
const expectedAud = `urn:logto:organization:${expectedOrgId}`;
if (!audiences.includes(expectedAud)) {
throw new AuthorizationError('Incohérence de l’ID d’organisation');
}
// Vérifier les portées requises pour l’organisation
const requiredScopes = ['invite:users', 'manage:settings']; // Remplacez par vos portées requises
const scopes = (payload.scope as string)?.split(' ') ?? [];
if (!requiredScopes.every((scope) => scopes.includes(scope))) {
throw new AuthorizationError('Portée d’organisation insuffisante');
}
}
function verifyPayload(payload: JWTPayload): void {
// Vérifier que la revendication audience correspond à votre indicateur de ressource API
const audiences = Array.isArray(payload.aud) ? payload.aud : payload.aud ? [payload.aud] : [];
if (!audiences.includes('https://your-api-resource-indicator')) {
throw new AuthorizationError(
'Audience invalide pour les ressources API au niveau organisation'
);
}
// Vérifier que l’ID d’organisation correspond au contexte (vous devrez peut-être l’extraire du contexte de la requête)
const expectedOrgId = 'your-organization-id'; // À extraire du contexte de la requête
const orgId = payload.organization_id as string;
if (expectedOrgId !== orgId) {
throw new AuthorizationError('Incohérence de l’ID d’organisation');
}
// Vérifier les portées requises pour les ressources API au niveau organisation
const requiredScopes = ['api:read', 'api:write']; // Remplacez par vos portées requises
const scopes = (payload.scope as string)?.split(' ') ?? [];
if (!requiredScopes.every((scope) => scopes.includes(scope))) {
throw new AuthorizationError('Portées API au niveau organisation insuffisantes');
}
}
Nous utilisons PyJWT pour valider les JWT. Installez-le si ce n'est pas déjà fait :
pip install pyjwt[crypto]
Commencez par ajouter ces utilitaires partagés pour gérer la validation des JWT :
import jwt
from jwt import PyJWKClient
from typing import Dict, Any
from auth_middleware import AuthInfo, AuthorizationError, JWKS_URI, ISSUER
jwks_client = PyJWKClient(JWKS_URI)
def validate_jwt_token(token: str) -> Dict[str, Any]:
"""Valider le jeton JWT et retourner le payload"""
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} # Nous vérifierons l'audience manuellement
)
verify_payload(payload)
return payload
except jwt.InvalidTokenError as e:
raise AuthorizationError(f'Jeton invalide : {str(e)}', 401)
except Exception as e:
raise AuthorizationError(f'Échec de la validation du jeton : {str(e)}', 401)
def create_auth_info(payload: Dict[str, Any]) -> AuthInfo:
"""Créer AuthInfo à partir du payload JWT"""
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:
"""Vérifier le payload selon le modèle de permission"""
# Implémentez ici votre logique de vérification selon le modèle de permission
# Cela sera montré dans la section des modèles de permission ci-dessous
pass
Ensuite, implémentez le middleware pour vérifier le jeton d’accès (Access token) :
- FastAPI
- Flask
- Django
- Django REST Framework
from fastapi import HTTPException, Depends
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from jwt_validator import validate_jwt_token, create_auth_info
security = HTTPBearer()
async def verify_access_token(credentials: HTTPAuthorizationCredentials = Depends(security)) -> AuthInfo:
try:
token = credentials.credentials
payload = validate_jwt_token(token)
return create_auth_info(payload)
except AuthorizationError as e:
raise HTTPException(status_code=e.status, detail=str(e))
from functools import wraps
from flask import request, jsonify, g
from jwt_validator import validate_jwt_token, create_auth_info
def verify_access_token(f):
@wraps(f)
def decorated_function(*args, **kwargs):
try:
token = extract_bearer_token_from_headers(dict(request.headers))
payload = validate_jwt_token(token)
# Stocker les informations d'authentification dans l'objet g de Flask pour un usage générique
g.auth = create_auth_info(payload)
return f(*args, **kwargs)
except AuthorizationError as e:
return jsonify({'error': str(e)}), e.status
return decorated_function
from django.http import JsonResponse
from jwt_validator import validate_jwt_token, create_auth_info
def require_access_token(view_func):
def wrapper(request, *args, **kwargs):
try:
headers = {key.replace('HTTP_', '').replace('_', '-').lower(): value
for key, value in request.META.items() if key.startswith('HTTP_')}
token = extract_bearer_token_from_headers(headers)
payload = validate_jwt_token(token)
# Attacher les informations d'authentification à la requête pour un usage générique
request.auth = create_auth_info(payload)
return view_func(request, *args, **kwargs)
except AuthorizationError as e:
return JsonResponse({'error': str(e)}, status=e.status)
return wrapper
from rest_framework.authentication import TokenAuthentication
from rest_framework import exceptions
from jwt_validator import validate_jwt_token, create_auth_info
class AccessTokenAuthentication(TokenAuthentication):
keyword = 'Bearer' # Utiliser 'Bearer' au lieu de 'Token'
def authenticate_credentials(self, key):
"""
Authentifier le jeton en le validant comme JWT.
"""
try:
payload = validate_jwt_token(key)
auth_info = create_auth_info(payload)
# Créer un objet de type utilisateur qui contient les infos d'authentification pour un usage générique
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))
Selon votre modèle de permission, implémentez la logique de vérification appropriée dans jwt_validator.py
:
- Ressources API globales
- Permissions d’organisation (hors API)
- Ressources API au niveau organisation
def verify_payload(payload: Dict[str, Any]) -> None:
"""Vérifier le payload pour les ressources API globales"""
# Vérifier que la revendication d'audience correspond à votre indicateur de ressource API
audiences = payload.get('aud', [])
if isinstance(audiences, str):
audiences = [audiences]
if 'https://your-api-resource-indicator' not in audiences:
raise AuthorizationError('Audience invalide')
# Vérifier les portées requises pour les ressources API globales
required_scopes = ['api:read', 'api:write'] # Remplacez par vos portées requises réelles
scopes = payload.get('scope', '').split(' ') if payload.get('scope') else []
if not all(scope in scopes for scope in required_scopes):
raise AuthorizationError('Portée insuffisante')
def verify_payload(payload: Dict[str, Any]) -> None:
"""Vérifier le payload pour les permissions d’organisation"""
# Vérifier que la revendication d'audience correspond au format d'organisation
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('Audience invalide pour les permissions d’organisation')
# Vérifier que l’ID d’organisation correspond au contexte (vous devrez peut-être l’extraire du contexte de la requête)
expected_org_id = 'your-organization-id' # À extraire du contexte de la requête
expected_aud = f'urn:logto:organization:{expected_org_id}'
if expected_aud not in audiences:
raise AuthorizationError('Incohérence de l’ID d’organisation')
# Vérifier les portées requises pour l’organisation
required_scopes = ['invite:users', 'manage:settings'] # Remplacez par vos portées requises réelles
scopes = payload.get('scope', '').split(' ') if payload.get('scope') else []
if not all(scope in scopes for scope in required_scopes):
raise AuthorizationError('Portée d’organisation insuffisante')
def verify_payload(payload: Dict[str, Any]) -> None:
"""Vérifier le payload pour les ressources API au niveau organisation"""
# Vérifier que la revendication d'audience correspond à votre indicateur de ressource API
audiences = payload.get('aud', [])
if isinstance(audiences, str):
audiences = [audiences]
if 'https://your-api-resource-indicator' not in audiences:
raise AuthorizationError('Audience invalide pour les ressources API au niveau organisation')
# Vérifier que l’ID d’organisation correspond au contexte (vous devrez peut-être l’extraire du contexte de la requête)
expected_org_id = 'your-organization-id' # À extraire du contexte de la requête
org_id = payload.get('organization_id')
if expected_org_id != org_id:
raise AuthorizationError('Incohérence de l’ID d’organisation')
# Vérifier les portées requises pour les ressources API au niveau organisation
required_scopes = ['api:read', 'api:write'] # Remplacez par vos portées requises réelles
scopes = payload.get('scope', '').split(' ') if payload.get('scope') else []
if not all(scope in scopes for scope in required_scopes):
raise AuthorizationError('Portées API au niveau organisation insuffisantes')
Nous utilisons github.com/lestrrat-go/jwx pour valider les JWT. Installez-le si ce n'est pas déjà fait :
go mod init your-project
go get github.com/lestrrat-go/jwx/v3
Tout d'abord, ajoutez ces composants partagés à votre fichier auth_middleware.go
:
import (
"context"
"strings"
"time"
"github.com/lestrrat-go/jwx/v3/jwk"
"github.com/lestrrat-go/jwx/v3/jwt"
)
var jwkSet jwk.Set
func init() {
// Initialiser le cache JWKS
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("Échec de la récupération du JWKS : " + err.Error())
}
}
// validateJWTToken valide le jeton JWT et retourne le jeton analysé
func validateJWTToken(tokenString string) (jwt.Token, error) {
token, err := jwt.Parse([]byte(tokenString), jwt.WithKeySet(jwkSet))
if err != nil {
return nil, NewAuthorizationError("Jeton invalide : "+err.Error(), http.StatusUnauthorized)
}
// Vérifier l'émetteur
if token.Issuer() != ISSUER {
return nil, NewAuthorizationError("Émetteur invalide", http.StatusUnauthorized)
}
if err := verifyPayload(token); err != nil {
return nil, err
}
return token, nil
}
// Fonctions utilitaires pour extraire les données du jeton
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()
}
Ensuite, implémentez le middleware pour vérifier le jeton d’accès :
- Gin
- Echo
- Fiber
- Chi
import "github.com/gin-gonic/gin"
func VerifyAccessToken() gin.HandlerFunc {
return func(c *gin.Context) {
tokenString, err := extractBearerTokenFromHeaders(c.Request)
if err != nil {
authErr := err.(*AuthorizationError)
c.JSON(authErr.Status, gin.H{"error": authErr.Message})
c.Abort()
return
}
token, err := validateJWTToken(tokenString)
if err != nil {
authErr := err.(*AuthorizationError)
c.JSON(authErr.Status, gin.H{"error": authErr.Message})
c.Abort()
return
}
// Stocker le jeton dans le contexte pour un usage générique
c.Set("auth", token)
c.Next()
}
}
import "github.com/labstack/echo/v4"
func VerifyAccessToken(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
tokenString, err := extractBearerTokenFromHeaders(c.Request())
if err != nil {
authErr := err.(*AuthorizationError)
return c.JSON(authErr.Status, echo.Map{"error": authErr.Message})
}
token, err := validateJWTToken(tokenString)
if err != nil {
authErr := err.(*AuthorizationError)
return c.JSON(authErr.Status, echo.Map{"error": authErr.Message})
}
// Stocker le jeton dans le contexte pour un usage générique
c.Set("auth", token)
return next(c)
}
}
import (
"net/http"
"github.com/gofiber/fiber/v2"
)
func VerifyAccessToken(c *fiber.Ctx) error {
// Convertir la requête fiber en http.Request pour la compatibilité
req := &http.Request{
Header: make(http.Header),
}
req.Header.Set("Authorization", c.Get("Authorization"))
tokenString, err := extractBearerTokenFromHeaders(req)
if err != nil {
authErr := err.(*AuthorizationError)
return c.Status(authErr.Status).JSON(fiber.Map{"error": authErr.Message})
}
token, err := validateJWTToken(tokenString)
if err != nil {
authErr := err.(*AuthorizationError)
return c.Status(authErr.Status).JSON(fiber.Map{"error": authErr.Message})
}
// Stocker le jeton dans les locals pour un usage générique
c.Locals("auth", token)
return c.Next()
}
import (
"context"
"encoding/json"
"net/http"
)
type contextKey string
const AuthContextKey contextKey = "auth"
func VerifyAccessToken(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
tokenString, err := extractBearerTokenFromHeaders(r)
if err != nil {
authErr := err.(*AuthorizationError)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(authErr.Status)
json.NewEncoder(w).Encode(map[string]string{"error": authErr.Message})
return
}
token, err := validateJWTToken(tokenString)
if err != nil {
authErr := err.(*AuthorizationError)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(authErr.Status)
json.NewEncoder(w).Encode(map[string]string{"error": authErr.Message})
return
}
// Stocker le jeton dans le contexte pour un usage générique
ctx := context.WithValue(r.Context(), AuthContextKey, token)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
Selon votre modèle de permission, vous devrez peut-être adopter une logique différente pour verifyPayload
:
- Ressources API globales
- Permissions d'organisation (hors API)
- Ressources API au niveau de l'organisation
func verifyPayload(token jwt.Token) error {
// Vérifier que la revendication d'audience correspond à votre indicateur de ressource API
if !hasAudience(token, "https://your-api-resource-indicator") {
return NewAuthorizationError("Audience invalide")
}
// Vérifier les portées requises pour les ressources API globales
requiredScopes := []string{"api:read", "api:write"} // Remplacez par vos portées requises
if !hasRequiredScopes(token, requiredScopes) {
return NewAuthorizationError("Portée insuffisante")
}
return nil
}
func verifyPayload(token jwt.Token) error {
// Vérifier que la revendication d'audience correspond au format d'organisation
if !hasOrganizationAudience(token) {
return NewAuthorizationError("Audience invalide pour les permissions d'organisation")
}
// Vérifier que l'ID d'organisation correspond au contexte (vous devrez peut-être l'extraire du contexte de la requête)
expectedOrgID := "your-organization-id" // À extraire du contexte de la requête
if !hasMatchingOrganization(token, expectedOrgID) {
return NewAuthorizationError("Incohérence de l'ID d'organisation")
}
// Vérifier les portées requises pour l'organisation
requiredScopes := []string{"invite:users", "manage:settings"} // Remplacez par vos portées requises
if !hasRequiredScopes(token, requiredScopes) {
return NewAuthorizationError("Portée d'organisation insuffisante")
}
return nil
}
func verifyPayload(token jwt.Token) error {
// Vérifier que la revendication d'audience correspond à votre indicateur de ressource API
if !hasAudience(token, "https://your-api-resource-indicator") {
return NewAuthorizationError("Audience invalide pour les ressources API au niveau de l'organisation")
}
// Vérifier que l'ID d'organisation correspond au contexte (vous devrez peut-être l'extraire du contexte de la requête)
expectedOrgID := "your-organization-id" // À extraire du contexte de la requête
if !hasMatchingOrganizationID(token, expectedOrgID) {
return NewAuthorizationError("Incohérence de l'ID d'organisation")
}
// Vérifier les portées requises pour les ressources API au niveau de l'organisation
requiredScopes := []string{"api:read", "api:write"} // Remplacez par vos portées requises
if !hasRequiredScopes(token, requiredScopes) {
return NewAuthorizationError("Portées API au niveau de l'organisation insuffisantes")
}
return nil
}
Ajoutez ces fonctions utilitaires pour la vérification du payload :
// hasAudience vérifie si le jeton possède l'audience spécifiée
func hasAudience(token jwt.Token, expectedAud string) bool {
audiences := token.Audience()
for _, aud := range audiences {
if aud == expectedAud {
return true
}
}
return false
}
// hasOrganizationAudience vérifie si le jeton possède une audience au format organisation
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 vérifie si le jeton possède toutes les portées requises
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 vérifie si l'audience du jeton correspond à l'organisation attendue
func hasMatchingOrganization(token jwt.Token, expectedOrgID string) bool {
expectedAud := fmt.Sprintf("urn:logto:organization:%s", expectedOrgID)
return hasAudience(token, expectedAud)
}
// hasMatchingOrganizationID vérifie si le champ organization_id du jeton correspond à celui attendu
func hasMatchingOrganizationID(token jwt.Token, expectedOrgID string) bool {
orgID := getStringClaim(token, "organization_id")
return orgID == expectedOrgID
}
Nous utilisons différentes bibliothèques JWT selon le framework. Installez les dépendances requises :
- Spring Boot
- Quarkus
- Micronaut
- Vert.x Web
Ajoutez à votre pom.xml
:
<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>
Ajoutez à votre pom.xml
:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-smallrye-jwt</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-resteasy-reactive</artifactId>
</dependency>
Ajoutez à votre pom.xml
:
<dependency>
<groupId>io.micronaut.security</groupId>
<artifactId>micronaut-security-jwt</artifactId>
</dependency>
<dependency>
<groupId>io.micronaut</groupId>
<artifactId>micronaut-http-server-netty</artifactId>
</dependency>
Ajoutez à votre pom.xml
:
<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>
Ensuite, implémentez le middleware pour vérifier le jeton d’accès (Access token) :
- Spring Boot
- Quarkus
- Micronaut
- Vert.x Web
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
public class JwtSecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authz -> authz
.requestMatchers("/api/protected/**").authenticated()
.anyRequest().permitAll()
)
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt.decoder(jwtDecoder()))
);
return http.build();
}
@Bean
public JwtDecoder jwtDecoder() {
return NimbusJwtDecoder.withJwkSetUri(AuthConstants.JWKS_URI)
.build();
}
}
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.stereotype.Component;
import java.util.Arrays;
import java.util.List;
@Component
public class JwtValidator {
public void verifyPayload(Jwt jwt) {
// Implémentez ici votre logique de vérification selon le modèle de permission
// Cela sera montré dans la section des modèles de permission ci-dessous
}
}
import org.eclipse.microprofile.jwt.JsonWebToken;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.ws.rs.container.ContainerRequestContext;
import jakarta.ws.rs.container.ContainerRequestFilter;
import jakarta.ws.rs.ext.Provider;
@Provider
@ApplicationScoped
public class JwtVerificationFilter implements ContainerRequestFilter {
@Inject
JsonWebToken jwt;
@Override
public void filter(ContainerRequestContext requestContext) {
if (requestContext.getUriInfo().getPath().startsWith("/api/protected")) {
try {
verifyPayload(jwt);
// Stocker le JWT dans le contexte pour un accès dans les contrôleurs
requestContext.setProperty("auth", jwt);
} catch (AuthorizationException e) {
requestContext.abortWith(
jakarta.ws.rs.core.Response.status(e.getStatusCode())
.entity("{\"error\": \"" + e.getMessage() + "\"}")
.build()
);
} catch (Exception e) {
requestContext.abortWith(
jakarta.ws.rs.core.Response.status(401)
.entity("{\"error\": \"Invalid token\"}")
.build()
);
}
}
}
private void verifyPayload(JsonWebToken jwt) {
// Implémentez ici votre logique de vérification selon le modèle de permission
// Cela sera montré dans la section des modèles de permission ci-dessous
}
}
micronaut:
security:
authentication: bearer
token:
jwt:
signatures:
jwks:
logto:
url: ${JWKS_URI:https://your-tenant.logto.app/oidc/jwks}
import io.micronaut.security.token.Claims;
import io.micronaut.security.token.validator.TokenValidator;
import jakarta.inject.Singleton;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Mono;
@Singleton
public class JwtClaimsValidator implements TokenValidator {
@Override
public Publisher<Boolean> validateToken(String token, Claims claims) {
return Mono.fromCallable(() -> {
try {
verifyPayload(claims);
return true;
} catch (AuthorizationException e) {
throw new RuntimeException(e.getMessage());
}
});
}
private void verifyPayload(Claims claims) {
// Implémentez ici votre logique de vérification selon le modèle de permission
// Cela sera montré dans la section des modèles de permission ci-dessous
}
}
import io.vertx.core.Future;
import io.vertx.core.Handler;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.auth.jwt.JWTAuth;
import io.vertx.ext.auth.jwt.JWTAuthOptions;
import io.vertx.ext.web.RoutingContext;
import io.vertx.ext.web.client.WebClient;
public class JwtAuthHandler implements Handler<RoutingContext> {
private final JWTAuth jwtAuth;
private final WebClient webClient;
public JwtAuthHandler(io.vertx.core.Vertx vertx) {
this.webClient = WebClient.create(vertx);
this.jwtAuth = JWTAuth.create(vertx, new JWTAuthOptions());
// Récupérer le JWKS et configurer l’authentification JWT
fetchJWKS().onSuccess(jwks -> {
// Configurer le JWKS (simplifié - vous pouvez avoir besoin d’un parseur JWKS approprié)
});
}
@Override
public void handle(RoutingContext context) {
String authHeader = context.request().getHeader("Authorization");
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
context.response()
.setStatusCode(401)
.putHeader("Content-Type", "application/json")
.end("{\"error\": \"Authorization header missing or invalid\"}");
return;
}
String token = authHeader.substring(7);
jwtAuth.authenticate(new JsonObject().put("token", token))
.onSuccess(user -> {
try {
JsonObject principal = user.principal();
verifyPayload(principal);
// Stocker le principal JWT dans le contexte pour un accès dans les handlers
context.put("auth", principal);
context.next();
} catch (AuthorizationException e) {
context.response()
.setStatusCode(e.getStatusCode())
.putHeader("Content-Type", "application/json")
.end("{\"error\": \"" + e.getMessage() + "\"}");
} catch (Exception e) {
context.response()
.setStatusCode(401)
.putHeader("Content-Type", "application/json")
.end("{\"error\": \"Invalid token\"}");
}
})
.onFailure(err -> {
context.response()
.setStatusCode(401)
.putHeader("Content-Type", "application/json")
.end("{\"error\": \"Invalid token\"}");
});
}
private Future<JsonObject> fetchJWKS() {
return webClient.getAbs(AuthConstants.JWKS_URI)
.send()
.map(response -> response.bodyAsJsonObject());
}
private void verifyPayload(JsonObject principal) {
// Implémentez ici votre logique de vérification selon le modèle de permission
// Cela sera montré dans la section des modèles de permission ci-dessous
}
}
Selon votre modèle de permission, vous devrez peut-être adopter une logique de vérification différente :
- Ressources API globales
- Permissions d’organisation (hors API)
- Ressources API au niveau de l’organisation
- Spring Boot
- Quarkus
- Micronaut
- Vert.x Web
private void verifyPayload(Jwt jwt) {
// Vérifier que la revendication d’audience correspond à votre indicateur de ressource API
List<String> audiences = jwt.getAudience();
if (!audiences.contains("https://your-api-resource-indicator")) {
throw new AuthorizationException("Invalid audience");
}
// Vérifier les portées requises pour les ressources API globales
List<String> requiredScopes = Arrays.asList("api:read", "api:write"); // Remplacez par vos portées requises
String scopes = jwt.getClaimAsString("scope");
List<String> tokenScopes = scopes != null ? Arrays.asList(scopes.split(" ")) : List.of();
if (!tokenScopes.containsAll(requiredScopes)) {
throw new AuthorizationException("Insufficient scope");
}
}
private void verifyPayload(JsonWebToken jwt) {
// Vérifier que la revendication d’audience correspond à votre indicateur de ressource API
if (!jwt.getAudience().contains("https://your-api-resource-indicator")) {
throw new AuthorizationException("Invalid audience");
}
// Vérifier les portées requises pour les ressources API globales
List<String> requiredScopes = Arrays.asList("api:read", "api:write"); // Remplacez par vos portées requises
String scopes = jwt.getClaim("scope");
List<String> tokenScopes = scopes != null ? Arrays.asList(scopes.split(" ")) : List.of();
if (!tokenScopes.containsAll(requiredScopes)) {
throw new AuthorizationException("Insufficient scope");
}
}
private void verifyPayload(Claims claims) {
// Vérifier que la revendication d’audience correspond à votre indicateur de ressource API
List<String> audiences = (List<String>) claims.get("aud");
if (audiences == null || !audiences.contains("https://your-api-resource-indicator")) {
throw new AuthorizationException("Invalid audience");
}
// Vérifier les portées requises pour les ressources API globales
List<String> requiredScopes = Arrays.asList("api:read", "api:write"); // Remplacez par vos portées requises
String scopes = (String) claims.get("scope");
List<String> tokenScopes = scopes != null ? Arrays.asList(scopes.split(" ")) : List.of();
if (!tokenScopes.containsAll(requiredScopes)) {
throw new AuthorizationException("Insufficient scope");
}
}
private void verifyPayload(JsonObject principal) {
// Vérifier que la revendication d’audience correspond à votre indicateur de ressource API
JsonArray audiences = principal.getJsonArray("aud");
if (audiences == null || !audiences.contains("https://your-api-resource-indicator")) {
throw new AuthorizationException("Invalid audience");
}
// Vérifier les portées requises pour les ressources API globales
List<String> requiredScopes = Arrays.asList("api:read", "api:write"); // Remplacez par vos portées requises
String scopes = principal.getString("scope");
List<String> tokenScopes = scopes != null ? Arrays.asList(scopes.split(" ")) : List.of();
if (!tokenScopes.containsAll(requiredScopes)) {
throw new AuthorizationException("Insufficient scope");
}
}
- Spring Boot
- Quarkus
- Micronaut
- Vert.x Web
private void verifyPayload(Jwt jwt) {
// Vérifier que la revendication d’audience correspond au format d’organisation
List<String> audiences = jwt.getAudience();
boolean hasOrgAudience = audiences.stream()
.anyMatch(aud -> aud.startsWith("urn:logto:organization:"));
if (!hasOrgAudience) {
throw new AuthorizationException("Invalid audience for organization permissions");
}
// Vérifier que l’ID d’organisation correspond au contexte (à extraire du contexte de la requête)
String expectedOrgId = "your-organization-id"; // À extraire du contexte de la requête
String expectedAud = "urn:logto:organization:" + expectedOrgId;
if (!audiences.contains(expectedAud)) {
throw new AuthorizationException("Organization ID mismatch");
}
// Vérifier les portées requises pour l’organisation
List<String> requiredScopes = Arrays.asList("invite:users", "manage:settings"); // Remplacez par vos portées requises
String scopes = jwt.getClaimAsString("scope");
List<String> tokenScopes = scopes != null ? Arrays.asList(scopes.split(" ")) : List.of();
if (!tokenScopes.containsAll(requiredScopes)) {
throw new AuthorizationException("Insufficient organization scope");
}
}
private void verifyPayload(JsonWebToken jwt) {
// Vérifier que la revendication d’audience correspond au format d’organisation
boolean hasOrgAudience = jwt.getAudience().stream()
.anyMatch(aud -> aud.startsWith("urn:logto:organization:"));
if (!hasOrgAudience) {
throw new AuthorizationException("Invalid audience for organization permissions");
}
// Vérifier que l’ID d’organisation correspond au contexte (à extraire du contexte de la requête)
String expectedOrgId = "your-organization-id"; // À extraire du contexte de la requête
String expectedAud = "urn:logto:organization:" + expectedOrgId;
if (!jwt.getAudience().contains(expectedAud)) {
throw new AuthorizationException("Organization ID mismatch");
}
// Vérifier les portées requises pour l’organisation
List<String> requiredScopes = Arrays.asList("invite:users", "manage:settings"); // Remplacez par vos portées requises
String scopes = jwt.getClaim("scope");
List<String> tokenScopes = scopes != null ? Arrays.asList(scopes.split(" ")) : List.of();
if (!tokenScopes.containsAll(requiredScopes)) {
throw new AuthorizationException("Insufficient organization scope");
}
}
private void verifyPayload(Claims claims) {
// Vérifier que la revendication d’audience correspond au format d’organisation
List<String> audiences = (List<String>) claims.get("aud");
boolean hasOrgAudience = audiences != null && audiences.stream()
.anyMatch(aud -> aud.startsWith("urn:logto:organization:"));
if (!hasOrgAudience) {
throw new AuthorizationException("Invalid audience for organization permissions");
}
// Vérifier que l’ID d’organisation correspond au contexte (à extraire du contexte de la requête)
String expectedOrgId = "your-organization-id"; // À extraire du contexte de la requête
String expectedAud = "urn:logto:organization:" + expectedOrgId;
if (!audiences.contains(expectedAud)) {
throw new AuthorizationException("Organization ID mismatch");
}
// Vérifier les portées requises pour l’organisation
List<String> requiredScopes = Arrays.asList("invite:users", "manage:settings"); // Remplacez par vos portées requises
String scopes = (String) claims.get("scope");
List<String> tokenScopes = scopes != null ? Arrays.asList(scopes.split(" ")) : List.of();
if (!tokenScopes.containsAll(requiredScopes)) {
throw new AuthorizationException("Insufficient organization scope");
}
}
private void verifyPayload(JsonObject principal) {
// Vérifier que la revendication d’audience correspond au format d’organisation
JsonArray audiences = principal.getJsonArray("aud");
boolean hasOrgAudience = false;
if (audiences != null) {
for (Object aud : audiences) {
if (aud.toString().startsWith("urn:logto:organization:")) {
hasOrgAudience = true;
break;
}
}
}
if (!hasOrgAudience) {
throw new AuthorizationException("Invalid audience for organization permissions");
}
// Vérifier que l’ID d’organisation correspond au contexte (à extraire du contexte de la requête)
String expectedOrgId = "your-organization-id"; // À extraire du contexte de la requête
String expectedAud = "urn:logto:organization:" + expectedOrgId;
if (!audiences.contains(expectedAud)) {
throw new AuthorizationException("Organization ID mismatch");
}
// Vérifier les portées requises pour l’organisation
List<String> requiredScopes = Arrays.asList("invite:users", "manage:settings"); // Remplacez par vos portées requises
String scopes = principal.getString("scope");
List<String> tokenScopes = scopes != null ? Arrays.asList(scopes.split(" ")) : List.of();
if (!tokenScopes.containsAll(requiredScopes)) {
throw new AuthorizationException("Insufficient organization scope");
}
}
- Spring Boot
- Quarkus
- Micronaut
- Vert.x Web
private void verifyPayload(Jwt jwt) {
// Vérifier que la revendication d’audience correspond à votre indicateur de ressource API
List<String> audiences = jwt.getAudience();
if (!audiences.contains("https://your-api-resource-indicator")) {
throw new AuthorizationException("Invalid audience for organization-level API resources");
}
// Vérifier que l’ID d’organisation correspond au contexte (à extraire du contexte de la requête)
String expectedOrgId = "your-organization-id"; // À extraire du contexte de la requête
String orgId = jwt.getClaimAsString("organization_id");
if (!expectedOrgId.equals(orgId)) {
throw new AuthorizationException("Organization ID mismatch");
}
// Vérifier les portées requises pour les ressources API au niveau de l’organisation
List<String> requiredScopes = Arrays.asList("api:read", "api:write"); // Remplacez par vos portées requises
String scopes = jwt.getClaimAsString("scope");
List<String> tokenScopes = scopes != null ? Arrays.asList(scopes.split(" ")) : List.of();
if (!tokenScopes.containsAll(requiredScopes)) {
throw new AuthorizationException("Insufficient organization-level API scopes");
}
}
private void verifyPayload(JsonWebToken jwt) {
// Vérifier que la revendication d’audience correspond à votre indicateur de ressource API
if (!jwt.getAudience().contains("https://your-api-resource-indicator")) {
throw new AuthorizationException("Invalid audience for organization-level API resources");
}
// Vérifier que l’ID d’organisation correspond au contexte (à extraire du contexte de la requête)
String expectedOrgId = "your-organization-id"; // À extraire du contexte de la requête
String orgId = jwt.getClaim("organization_id");
if (!expectedOrgId.equals(orgId)) {
throw new AuthorizationException("Organization ID mismatch");
}
// Vérifier les portées requises pour les ressources API au niveau de l’organisation
List<String> requiredScopes = Arrays.asList("api:read", "api:write"); // Remplacez par vos portées requises
String scopes = jwt.getClaim("scope");
List<String> tokenScopes = scopes != null ? Arrays.asList(scopes.split(" ")) : List.of();
if (!tokenScopes.containsAll(requiredScopes)) {
throw new AuthorizationException("Insufficient organization-level API scopes");
}
}
private void verifyPayload(Claims claims) {
// Vérifier que la revendication d’audience correspond à votre indicateur de ressource API
List<String> audiences = (List<String>) claims.get("aud");
if (audiences == null || !audiences.contains("https://your-api-resource-indicator")) {
throw new AuthorizationException("Invalid audience for organization-level API resources");
}
// Vérifier que l’ID d’organisation correspond au contexte (à extraire du contexte de la requête)
String expectedOrgId = "your-organization-id"; // À extraire du contexte de la requête
String orgId = (String) claims.get("organization_id");
if (!expectedOrgId.equals(orgId)) {
throw new AuthorizationException("Organization ID mismatch");
}
// Vérifier les portées requises pour les ressources API au niveau de l’organisation
List<String> requiredScopes = Arrays.asList("api:read", "api:write"); // Remplacez par vos portées requises
String scopes = (String) claims.get("scope");
List<String> tokenScopes = scopes != null ? Arrays.asList(scopes.split(" ")) : List.of();
if (!tokenScopes.containsAll(requiredScopes)) {
throw new AuthorizationException("Insufficient organization-level API scopes");
}
}
private void verifyPayload(JsonObject principal) {
// Vérifier que la revendication d’audience correspond à votre indicateur de ressource API
JsonArray audiences = principal.getJsonArray("aud");
if (audiences == null || !audiences.contains("https://your-api-resource-indicator")) {
throw new AuthorizationException("Invalid audience for organization-level API resources");
}
// Vérifier que l’ID d’organisation correspond au contexte (à extraire du contexte de la requête)
String expectedOrgId = "your-organization-id"; // À extraire du contexte de la requête
String orgId = principal.getString("organization_id");
if (!expectedOrgId.equals(orgId)) {
throw new AuthorizationException("Organization ID mismatch");
}
// Vérifier les portées requises pour les ressources API au niveau de l’organisation
List<String> requiredScopes = Arrays.asList("api:read", "api:write"); // Remplacez par vos portées requises
String scopes = principal.getString("scope");
List<String> tokenScopes = scopes != null ? Arrays.asList(scopes.split(" ")) : List.of();
if (!tokenScopes.containsAll(requiredScopes)) {
throw new AuthorizationException("Insufficient organization-level API scopes");
}
}
Ajoutez le package NuGet requis pour l’authentification JWT :
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.0" />
Créez un service de validation pour gérer la validation du jeton :
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
{
// Ajoutez ici votre logique de validation basée sur le modèle de permission
ValidatePayload(principal);
}
catch (AuthorizationException)
{
throw; // Relancer les exceptions d'autorisation
}
catch (Exception ex)
{
throw new AuthorizationException($"Échec de la validation du jeton : {ex.Message}", 401);
}
}
private void ValidatePayload(ClaimsPrincipal principal)
{
// Implémentez ici votre logique de vérification basée sur le modèle de permission
// Ceci sera détaillé dans la section sur les modèles de permission ci-dessous
}
}
}
Configurez l’authentification JWT dans votre Program.cs
:
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using YourApiNamespace.Services;
using YourApiNamespace.Exceptions;
var builder = WebApplication.CreateBuilder(args);
// Ajoutez les services au conteneur
builder.Services.AddControllers();
builder.Services.AddScoped<IJwtValidationService, JwtValidationService>();
// Configurez l’authentification JWT
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, // Nous validerons l’audience manuellement selon le modèle de permission
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 =>
{
// Gérer les erreurs de la bibliothèque JWT comme 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();
// Gestion globale des erreurs pour les échecs d’authentification / autorisation
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}\"}}");
}
});
// Configurez le pipeline de requêtes HTTP
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();
Selon votre modèle de permission, implémentez la logique de validation appropriée dans JwtValidationService
:
- Ressources API globales
- Permissions d’organisation (hors API)
- Ressources API au niveau organisation
private void ValidatePayload(ClaimsPrincipal principal)
{
// Vérifiez que la revendication d’audience correspond à votre indicateur de ressource API
var audiences = principal.FindAll("aud").Select(c => c.Value).ToList();
if (!audiences.Contains("https://your-api-resource-indicator"))
{
throw new AuthorizationException("Audience invalide");
}
// Vérifiez les portées requises pour les ressources API globales
var requiredScopes = new[] { "api:read", "api:write" }; // Remplacez par vos portées requises
var tokenScopes = principal.FindFirst("scope")?.Value?.Split(' ') ?? Array.Empty<string>();
if (!requiredScopes.All(scope => tokenScopes.Contains(scope)))
{
throw new AuthorizationException("Portée insuffisante");
}
}
private void ValidatePayload(ClaimsPrincipal principal)
{
// Vérifiez que la revendication d’audience correspond au format d’organisation
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("Audience invalide pour les permissions d’organisation");
}
// Vérifiez que l’ID d’organisation correspond au contexte (vous devrez peut-être l’extraire du contexte de la requête)
var expectedOrgId = "your-organization-id"; // À extraire du contexte de la requête
var expectedAud = $"urn:logto:organization:{expectedOrgId}";
if (!audiences.Contains(expectedAud))
{
throw new AuthorizationException("Incohérence de l’ID d’organisation");
}
// Vérifiez les portées requises pour l’organisation
var requiredScopes = new[] { "invite:users", "manage:settings" }; // Remplacez par vos portées requises
var tokenScopes = principal.FindFirst("scope")?.Value?.Split(' ') ?? Array.Empty<string>();
if (!requiredScopes.All(scope => tokenScopes.Contains(scope)))
{
throw new AuthorizationException("Portée d’organisation insuffisante");
}
}
private void ValidatePayload(ClaimsPrincipal principal)
{
// Vérifiez que la revendication d’audience correspond à votre indicateur de ressource API
var audiences = principal.FindAll("aud").Select(c => c.Value).ToList();
if (!audiences.Contains("https://your-api-resource-indicator"))
{
throw new AuthorizationException("Audience invalide pour les ressources API au niveau organisation");
}
// Vérifiez que l’ID d’organisation correspond au contexte (vous devrez peut-être l’extraire du contexte de la requête)
var expectedOrgId = "your-organization-id"; // À extraire du contexte de la requête
var orgId = principal.FindFirst("organization_id")?.Value;
if (!expectedOrgId.Equals(orgId))
{
throw new AuthorizationException("Incohérence de l’ID d’organisation");
}
// Vérifiez les portées requises pour les ressources API au niveau organisation
var requiredScopes = new[] { "api:read", "api:write" }; // Remplacez par vos portées requises
var tokenScopes = principal.FindFirst("scope")?.Value?.Split(' ') ?? Array.Empty<string>();
if (!requiredScopes.All(scope => tokenScopes.Contains(scope)))
{
throw new AuthorizationException("Portées API au niveau organisation insuffisantes");
}
}
Nous utilisons firebase/php-jwt pour valider les JWT. Installez-le avec Composer :
- Laravel
- Symfony
- Slim
composer require firebase/php-jwt
composer require firebase/php-jwt
composer require firebase/php-jwt slim/slim:"4.*" slim/psr7
Commencez par ajouter ces utilitaires partagés pour gérer la validation des JWT :
<?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('Échec de la récupération du JWKS', 401);
}
self::$jwks = json_decode($jwksData, true);
}
return self::$jwks;
}
public static function validateJwtToken(string $token): array
{
try {
$jwks = self::fetchJwks();
$keys = JWK::parseKeySet($jwks);
$decoded = JWT::decode($token, $keys);
$payload = (array) $decoded;
// Vérifier l’émetteur
if (($payload['iss'] ?? '') !== AuthConstants::ISSUER) {
throw new AuthorizationException('Émetteur invalide', 401);
}
self::verifyPayload($payload);
return $payload;
} catch (AuthorizationException $e) {
throw $e;
} catch (Exception $e) {
throw new AuthorizationException('Jeton invalide : ' . $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
{
// Implémentez ici votre logique de vérification selon le modèle de permission
// Cela sera montré dans la section des modèles de permission ci-dessous
}
}
Ensuite, implémentez le middleware pour vérifier le jeton d’accès :
- Laravel
- Symfony
- Slim
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
class VerifyAccessToken
{
use AuthHelpers;
public function handle(Request $request, Closure $next): Response
{
try {
$token = $this->extractBearerToken($request->headers->all());
$payload = JwtValidator::validateJwtToken($token);
// Stocker les infos d’authentification dans les attributs de la requête pour un usage générique
$request->attributes->set('auth', JwtValidator::createAuthInfo($payload));
return $next($request);
} catch (AuthorizationException $e) {
return response()->json(['error' => $e->getMessage()], $e->statusCode);
}
}
}
Enregistrez le middleware dans app/Http/Kernel.php
:
protected $middlewareAliases = [
// ... autres middlewares
'auth.token' => \App\Http\Middleware\VerifyAccessToken::class,
];
<?php
namespace App\Security;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Http\Authenticator\AbstractAuthenticator;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport;
class JwtAuthenticator extends AbstractAuthenticator
{
use AuthHelpers;
public function supports(Request $request): ?bool
{
return $request->headers->has('authorization');
}
public function authenticate(Request $request): Passport
{
try {
$token = $this->extractBearerToken($request->headers->all());
$payload = JwtValidator::validateJwtToken($token);
$authInfo = JwtValidator::createAuthInfo($payload);
// Stocker les infos d’authentification dans les attributs de la requête pour un usage générique
$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; // Continuer vers le contrôleur
}
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
{
return new JsonResponse(['error' => $exception->getMessage()], Response::HTTP_UNAUTHORIZED);
}
}
Configurez la sécurité dans config/packages/security.yaml
:
security:
firewalls:
api:
pattern: ^/api/protected
stateless: true
custom_authenticators:
- App\Security\JwtAuthenticator
<?php
namespace App\Middleware;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Slim\Psr7\Response;
class JwtMiddleware implements MiddlewareInterface
{
use AuthHelpers;
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
try {
$headers = $request->getHeaders();
$token = $this->extractBearerToken($headers);
$payload = JwtValidator::validateJwtToken($token);
// Stocker les infos d’authentification dans les attributs de la requête pour un usage générique
$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);
}
}
}
Selon votre modèle de permission, implémentez la logique de vérification appropriée dans JwtValidator
:
- Ressources API globales
- Permissions d’organisation (hors API)
- Ressources API au niveau organisation
private static function verifyPayload(array $payload): void
{
// Vérifier que la revendication d’audience correspond à votre indicateur de ressource API
$audiences = $payload['aud'] ?? [];
if (is_string($audiences)) {
$audiences = [$audiences];
}
if (!in_array('https://your-api-resource-indicator', $audiences)) {
throw new AuthorizationException('Audience invalide');
}
// Vérifier les portées requises pour les ressources API globales
$requiredScopes = ['api:read', 'api:write']; // Remplacez par vos portées requises
$scopes = !empty($payload['scope']) ? explode(' ', $payload['scope']) : [];
foreach ($requiredScopes as $scope) {
if (!in_array($scope, $scopes)) {
throw new AuthorizationException('Portée insuffisante');
}
}
}
private static function verifyPayload(array $payload): void
{
// Vérifier que la revendication d’audience correspond au format d’organisation
$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('Audience invalide pour les permissions d’organisation');
}
// Vérifier que l’ID d’organisation correspond au contexte (vous devrez peut-être l’extraire du contexte de la requête)
$expectedOrgId = 'your-organization-id'; // À extraire du contexte de la requête
$expectedAud = "urn:logto:organization:{$expectedOrgId}";
if (!in_array($expectedAud, $audiences)) {
throw new AuthorizationException('Incohérence de l’ID d’organisation');
}
// Vérifier les portées requises pour l’organisation
$requiredScopes = ['invite:users', 'manage:settings']; // Remplacez par vos portées requises
$scopes = !empty($payload['scope']) ? explode(' ', $payload['scope']) : [];
foreach ($requiredScopes as $scope) {
if (!in_array($scope, $scopes)) {
throw new AuthorizationException('Portée d’organisation insuffisante');
}
}
}
private static function verifyPayload(array $payload): void
{
// Vérifier que la revendication d’audience correspond à votre indicateur de ressource API
$audiences = $payload['aud'] ?? [];
if (is_string($audiences)) {
$audiences = [$audiences];
}
if (!in_array('https://your-api-resource-indicator', $audiences)) {
throw new AuthorizationException('Audience invalide pour les ressources API au niveau organisation');
}
// Vérifier que l’ID d’organisation correspond au contexte (vous devrez peut-être l’extraire du contexte de la requête)
$expectedOrgId = 'your-organization-id'; // À extraire du contexte de la requête
$orgId = $payload['organization_id'] ?? null;
if ($expectedOrgId !== $orgId) {
throw new AuthorizationException('Incohérence de l’ID d’organisation');
}
// Vérifier les portées requises pour les ressources API au niveau organisation
$requiredScopes = ['api:read', 'api:write']; // Remplacez par vos portées requises
$scopes = !empty($payload['scope']) ? explode(' ', $payload['scope']) : [];
foreach ($requiredScopes as $scope) {
if (!in_array($scope, $scopes)) {
throw new AuthorizationException('Portées API au niveau organisation insuffisantes');
}
}
}
Nous utilisons la gem jwt pour valider les JWT. Ajoutez-la à votre Gemfile :
gem 'jwt'
# net-http fait partie de la bibliothèque standard Ruby depuis Ruby 2.7, inutile de l’ajouter explicitement
Puis exécutez :
bundle install
Ajoutez d'abord ces utilitaires partagés pour gérer les JWKS et la validation des jetons :
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('Échec de la récupération des JWKS', 401) unless response.is_a?(Net::HTTPSuccess)
jwks_data = JSON.parse(response.body)
JWT::JWK::Set.new(jwks_data)
end
end
def self.validate_jwt_token(token)
jwks = fetch_jwks
# Laissez la bibliothèque JWT gérer la détection de l’algorithme à partir des JWKS
decoded_token = JWT.decode(token, nil, true, {
iss: AuthConstants::ISSUER,
verify_iss: true,
verify_aud: false, # Nous vérifierons l’audience manuellement selon le modèle de permission
jwks: jwks
})[0]
decoded_token
end
def self.create_auth_info(payload)
scopes = payload['scope']&.split(' ') || []
audience = payload['aud'] || []
AuthInfo.new(
payload['sub'],
payload['client_id'],
payload['organization_id'],
scopes,
audience
)
end
def self.verify_payload(payload)
# Implémentez ici votre logique de vérification selon le modèle de permission
# Cela sera montré dans la section des modèles de permission ci-dessous
end
end
Ensuite, implémentez le middleware pour vérifier le jeton d’accès :
- Rails
- Sinatra
- Grape
module JwtAuthentication
extend ActiveSupport::Concern
include AuthHelpers
included do
before_action :verify_access_token, only: [:protected_action] # Ajoutez les actions spécifiques
end
private
def verify_access_token
begin
token = extract_bearer_token(request)
decoded_token = JwtValidator.validate_jwt_token(token)
JwtValidator.verify_payload(decoded_token)
# Stocker les infos d’authentification pour un usage générique
@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: 'Jeton invalide' }, status: 401
end
end
end
class AuthMiddleware
include AuthHelpers
def initialize(app)
@app = app
end
def call(env)
request = Rack::Request.new(env)
# Protéger uniquement certaines routes
if request.path.start_with?('/api/protected')
begin
token = extract_bearer_token(request)
decoded_token = JwtValidator.validate_jwt_token(token)
JwtValidator.verify_payload(decoded_token)
# Stocker les infos d’authentification dans env pour un usage générique
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: 'Jeton invalide' }.to_json]]
end
end
@app.call(env)
end
end
module GrapeAuthHelpers
include AuthHelpers
def authenticate_user!
begin
token = extract_bearer_token(request)
decoded_token = JwtValidator.validate_jwt_token(token)
JwtValidator.verify_payload(decoded_token)
# Stocker les infos d’authentification pour un usage générique
@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: 'Jeton invalide' }, 401)
end
end
def auth
@auth
end
end
Selon votre modèle de permission, implémentez la logique de vérification appropriée dans JwtValidator
:
- Ressources API globales
- Permissions d’organisation (hors API)
- Ressources API au niveau organisation
def self.verify_payload(payload)
# Vérifier que la revendication d’audience correspond à votre indicateur de ressource API
audiences = payload['aud'] || []
unless audiences.include?('https://your-api-resource-indicator')
raise AuthorizationError.new('Audience invalide')
end
# Vérifier les portées requises pour les ressources API globales
required_scopes = ['api:read', 'api:write'] # Remplacez par vos portées requises
token_scopes = payload['scope']&.split(' ') || []
unless required_scopes.all? { |scope| token_scopes.include?(scope) }
raise AuthorizationError.new('Portée insuffisante')
end
end
def self.verify_payload(payload)
# Vérifier que la revendication d’audience correspond au format d’organisation
audiences = payload['aud'] || []
has_org_audience = audiences.any? { |aud| aud.start_with?('urn:logto:organization:') }
unless has_org_audience
raise AuthorizationError.new('Audience invalide pour les permissions d’organisation')
end
# Vérifier que l’ID d’organisation correspond au contexte (vous devrez peut-être l’extraire du contexte de la requête)
expected_org_id = 'your-organization-id' # À extraire du contexte de la requête
expected_aud = "urn:logto:organization:#{expected_org_id}"
unless audiences.include?(expected_aud)
raise AuthorizationError.new('ID d’organisation non correspondant')
end
# Vérifier les portées requises pour l’organisation
required_scopes = ['invite:users', 'manage:settings'] # Remplacez par vos portées requises
token_scopes = payload['scope']&.split(' ') || []
unless required_scopes.all? { |scope| token_scopes.include?(scope) }
raise AuthorizationError.new('Portée d’organisation insuffisante')
end
end
def self.verify_payload(payload)
# Vérifier que la revendication d’audience correspond à votre indicateur de ressource API
audiences = payload['aud'] || []
unless audiences.include?('https://your-api-resource-indicator')
raise AuthorizationError.new('Audience invalide pour les ressources API au niveau organisation')
end
# Vérifier que l’ID d’organisation correspond au contexte (vous devrez peut-être l’extraire du contexte de la requête)
expected_org_id = 'your-organization-id' # À extraire du contexte de la requête
org_id = payload['organization_id']
unless expected_org_id == org_id
raise AuthorizationError.new('ID d’organisation non correspondant')
end
# Vérifier les portées requises pour les ressources API au niveau organisation
required_scopes = ['api:read', 'api:write'] # Remplacez par vos portées requises
token_scopes = payload['scope']&.split(' ') || []
unless required_scopes.all? { |scope| token_scopes.include?(scope) }
raise AuthorizationError.new('Portées API au niveau organisation insuffisantes')
end
end
Nous utilisons jsonwebtoken pour valider les JWT. Ajoutez les dépendances requises à votre Cargo.toml
:
- Axum
- Actix Web
- Rocket
[dependencies]
axum = "0.7"
tokio = { version = "1.0", features = ["full"] }
tower = "0.4"
tower-http = { version = "0.5", features = ["cors"] }
jsonwebtoken = "9.0"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
reqwest = { version = "0.11", features = ["json"] }
[dependencies]
actix-web = "4.0"
tokio = { version = "1.0", features = ["full"] }
jsonwebtoken = "9.0"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
reqwest = { version = "0.11", features = ["json"] }
[dependencies]
rocket = { version = "0.5", features = ["json"] }
tokio = { version = "1.0", features = ["full"] }
jsonwebtoken = "9.0"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
reqwest = { version = "0.11", features = ["json"] }
Commencez par ajouter ces utilitaires partagés pour gérer la validation des JWT :
use crate::{AuthInfo, AuthorizationError, ISSUER, JWKS_URI};
use jsonwebtoken::{decode, decode_header, Algorithm, DecodingKey, Validation};
use serde_json::Value;
use std::collections::HashMap;
pub struct JwtValidator {
jwks: HashMap<String, DecodingKey>,
}
impl JwtValidator {
pub async fn new() -> Result<Self, AuthorizationError> {
let jwks = Self::fetch_jwks().await?;
Ok(Self { jwks })
}
async fn fetch_jwks() -> Result<HashMap<String, DecodingKey>, AuthorizationError> {
let response = reqwest::get(JWKS_URI).await.map_err(|e| {
AuthorizationError::with_status(format!("Failed to fetch JWKS: {}", e), 401)
})?;
let jwks: Value = response.json().await.map_err(|e| {
AuthorizationError::with_status(format!("Failed to parse JWKS: {}", e), 401)
})?;
let mut keys = HashMap::new();
if let Some(keys_array) = jwks["keys"].as_array() {
for key in keys_array {
if let (Some(kid), Some(kty), Some(n), Some(e)) = (
key["kid"].as_str(),
key["kty"].as_str(),
key["n"].as_str(),
key["e"].as_str(),
) {
if kty == "RSA" {
if let Ok(decoding_key) = DecodingKey::from_rsa_components(n, e) {
keys.insert(kid.to_string(), decoding_key);
}
}
}
}
}
if keys.is_empty() {
return Err(AuthorizationError::with_status("No valid keys found in JWKS", 401));
}
Ok(keys)
}
pub fn validate_jwt_token(&self, token: &str) -> Result<AuthInfo, AuthorizationError> {
let header = decode_header(token).map_err(|e| {
AuthorizationError::with_status(format!("Invalid token header: {}", e), 401)
})?;
let kid = header.kid.ok_or_else(|| {
AuthorizationError::with_status("Token missing kid claim", 401)
})?;
let key = self.jwks.get(&kid).ok_or_else(|| {
AuthorizationError::with_status("Unknown key ID", 401)
})?;
let mut validation = Validation::new(Algorithm::RS256);
validation.set_issuer(&[ISSUER]);
validation.validate_aud = false; // Nous vérifierons l'audience manuellement
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> {
// Implémentez ici votre logique de vérification basée sur le modèle de permission
// Ceci sera détaillé dans la section sur les modèles de permission ci-dessous
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,
)
}
}
Ensuite, implémentez le middleware pour vérifier le jeton d’accès (access token) :
- Axum
- Actix Web
- Rocket
use crate::{AuthInfo, AuthorizationError, extract_bearer_token};
use crate::jwt_validator::JwtValidator;
use axum::{
extract::Request,
http::{HeaderMap, StatusCode},
middleware::Next,
response::{IntoResponse, Response},
Extension, Json,
};
use serde_json::json;
use std::sync::Arc;
pub async fn jwt_middleware(
Extension(validator): Extension<Arc<JwtValidator>>,
headers: HeaderMap,
mut request: Request,
next: Next,
) -> Result<Response, AuthorizationError> {
let authorization = headers
.get("authorization")
.and_then(|h| h.to_str().ok());
let token = extract_bearer_token(authorization)?;
let auth_info = validator.validate_jwt_token(token)?;
// Stocker les informations d'authentification dans les extensions de la requête pour un usage générique
request.extensions_mut().insert(auth_info);
Ok(next.run(request).await)
}
impl IntoResponse for AuthorizationError {
fn into_response(self) -> Response {
let status = StatusCode::from_u16(self.status_code).unwrap_or(StatusCode::FORBIDDEN);
(status, Json(json!({ "error": self.message }))).into_response()
}
}
use crate::{AuthInfo, AuthorizationError, extract_bearer_token};
use crate::jwt_validator::JwtValidator;
use actix_web::{
dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform},
web, Error, HttpMessage, HttpResponse,
};
use futures::future::{ok, Ready};
use std::sync::Arc;
pub struct JwtMiddleware {
validator: Arc<JwtValidator>,
}
impl JwtMiddleware {
pub fn new(validator: Arc<JwtValidator>) -> Self {
Self { validator }
}
}
impl<S, B> Transform<S, ServiceRequest> for JwtMiddleware
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
S::Future: 'static,
B: 'static,
{
type Response = ServiceResponse<B>;
type Error = Error;
type InitError = ();
type Transform = JwtMiddlewareService<S>;
type Future = Ready<Result<Self::Transform, Self::InitError>>;
fn new_transform(&self, service: S) -> Self::Future {
ok(JwtMiddlewareService {
service,
validator: self.validator.clone(),
})
}
}
pub struct JwtMiddlewareService<S> {
service: S,
validator: Arc<JwtValidator>,
}
impl<S, B> Service<ServiceRequest> for JwtMiddlewareService<S>
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
S::Future: 'static,
B: 'static,
{
type Response = ServiceResponse<B>;
type Error = Error;
type Future = futures::future::LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
forward_ready!(service);
fn call(&self, req: ServiceRequest) -> Self::Future {
let validator = self.validator.clone();
Box::pin(async move {
let authorization = req
.headers()
.get("authorization")
.and_then(|h| h.to_str().ok());
match extract_bearer_token(authorization)
.and_then(|token| validator.validate_jwt_token(token))
{
Ok(auth_info) => {
// Stocker les informations d'authentification dans les extensions de la requête pour un usage générique
req.extensions_mut().insert(auth_info);
let fut = self.service.call(req);
fut.await
}
Err(e) => {
let response = HttpResponse::build(
actix_web::http::StatusCode::from_u16(e.status_code)
.unwrap_or(actix_web::http::StatusCode::FORBIDDEN),
)
.json(serde_json::json!({ "error": e.message }));
Ok(req.into_response(response))
}
}
})
}
}
use crate::{AuthInfo, AuthorizationError, extract_bearer_token};
use crate::jwt_validator::JwtValidator;
use rocket::{
http::Status,
outcome::Outcome,
request::{self, FromRequest, Request},
State,
};
#[rocket::async_trait]
impl<'r> FromRequest<'r> for AuthInfo {
type Error = AuthorizationError;
async fn from_request(req: &'r Request<'_>) -> request::Outcome<Self, Self::Error> {
let validator = match req.guard::<&State<JwtValidator>>().await {
Outcome::Success(validator) => validator,
Outcome::Failure((status, _)) => {
return Outcome::Failure((
status,
AuthorizationError::with_status("JWT validator not found", 500),
))
}
Outcome::Forward(()) => {
return Outcome::Forward(())
}
};
let authorization = req.headers().get_one("authorization");
match extract_bearer_token(authorization)
.and_then(|token| validator.validate_jwt_token(token))
{
Ok(auth_info) => Outcome::Success(auth_info),
Err(e) => {
let status = Status::from_code(e.status_code).unwrap_or(Status::Forbidden);
Outcome::Failure((status, e))
}
}
}
}
Selon votre modèle de permission, implémentez la logique de vérification appropriée dans JwtValidator
:
- Ressources API globales
- Permissions d’organisation (hors API)
- Ressources API au niveau de l’organisation
fn verify_payload(&self, claims: &Value) -> Result<(), AuthorizationError> {
// Vérifier que la revendication d'audience correspond à votre indicateur de ressource API
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"));
}
// Vérifier les portées requises pour les ressources API globales
let required_scopes = vec!["api:read", "api:write"]; // Remplacez par vos portées requises
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> {
// Vérifier que la revendication d'audience correspond au format d'organisation
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"));
}
// Vérifier que l’ID d’organisation correspond au contexte (vous devrez peut-être l’extraire du contexte de la requête)
let expected_org_id = "your-organization-id"; // À extraire du contexte de la requête
let expected_aud = format!("urn:logto:organization:{}", expected_org_id);
if !audiences.contains(&expected_aud.as_str()) {
return Err(AuthorizationError::new("Organization ID mismatch"));
}
// Vérifier les portées requises pour l’organisation
let required_scopes = vec!["invite:users", "manage:settings"]; // Remplacez par vos portées requises
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> {
// Vérifier que la revendication d'audience correspond à votre indicateur de ressource API
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"));
}
// Vérifier que l’ID d’organisation correspond au contexte (vous devrez peut-être l’extraire du contexte de la requête)
let expected_org_id = "your-organization-id"; // À extraire du contexte de la requête
let org_id = claims["organization_id"].as_str().unwrap_or_default();
if expected_org_id != org_id {
return Err(AuthorizationError::new("Organization ID mismatch"));
}
// Vérifier les portées requises pour les ressources API au niveau de l’organisation
let required_scopes = vec!["api:read", "api:write"]; // Remplacez par vos portées requises
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(())
}
Étape 4 : Appliquer le middleware à votre API
Appliquez le middleware à vos routes API protégées.
- 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) => {
// Accédez directement aux informations d'authentification depuis req.auth
res.json({ auth: req.auth });
});
app.get('/api/protected/detailed', verifyAccessToken, (req, res) => {
// Votre logique de point de terminaison protégé
res.json({
auth: req.auth,
message: 'Données protégées accessibles avec succès',
});
});
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) => {
// Accédez directement aux informations d'authentification depuis ctx.state.auth
ctx.body = { auth: ctx.state.auth };
});
router.get('/api/protected/detailed', koaVerifyAccessToken, (ctx) => {
// Votre logique de point de terminaison protégé
ctx.body = {
auth: ctx.state.auth,
message: 'Données protégées accessibles avec succès',
};
});
app.use(router.routes());
app.listen(3000);
import Fastify from 'fastify';
import { fastifyVerifyAccessToken } from './auth-middleware.js';
const fastify = Fastify();
fastify.get('/api/protected', { preHandler: fastifyVerifyAccessToken }, (request, reply) => {
// Accédez directement aux informations d'authentification depuis request.auth
reply.send({ auth: request.auth });
});
fastify.get(
'/api/protected/detailed',
{ preHandler: fastifyVerifyAccessToken },
(request, reply) => {
// Votre logique de point de terminaison protégé
reply.send({
auth: request.auth,
message: 'Données protégées accessibles avec succès',
});
}
);
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) => {
// Accédez aux informations d'authentification depuis request.app.auth
return { auth: request.app.auth };
},
},
});
server.route({
method: 'GET',
path: '/api/protected/detailed',
options: {
pre: [{ method: hapiVerifyAccessToken }],
handler: (request, h) => {
// Votre logique de point de terminaison protégé
return {
auth: request.app.auth,
message: 'Données protégées accessibles avec succès',
};
},
},
});
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) {
// Accédez aux informations d'authentification depuis req.auth
return { auth: req.auth };
}
@Get('protected/detailed')
@UseGuards(AccessTokenGuard)
getDetailedProtected(@Req() req: any) {
// Votre logique de point de terminaison protégé
return {
auth: req.auth,
message: 'Données protégées accessibles avec succès',
};
}
}
- 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)):
# Accédez aux informations d'authentification directement depuis le paramètre auth
return {"auth": auth.to_dict()}
@app.get("/api/protected/detailed")
async def detailed_endpoint(auth: AuthInfo = Depends(verify_access_token)):
# Votre logique de point de terminaison protégé
return {
"auth": auth.to_dict(),
"message": "Données protégées accessibles avec succès"
}
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():
# Accédez aux informations d'authentification depuis g.auth
return jsonify({"auth": g.auth.to_dict()})
@app.route('/api/protected/detailed', methods=['GET'])
@verify_access_token
def detailed_endpoint():
# Votre logique de point de terminaison protégé
return jsonify({
"auth": g.auth.to_dict(),
"message": "Données protégées accessibles avec succès"
})
from django.http import JsonResponse
from auth_middleware import require_access_token
@require_access_token
def protected_view(request):
# Accédez aux informations d'authentification depuis request.auth
return JsonResponse({"auth": request.auth.to_dict()})
@require_access_token
def detailed_view(request):
# Votre logique de point de terminaison protégé
return JsonResponse({
"auth": request.auth.to_dict(),
"message": "Données protégées accessibles avec succès"
})
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):
# Accédez aux informations d'authentification depuis request.user.auth
return Response({"auth": request.user.auth.to_dict()})
@api_view(['GET'])
@authentication_classes([AccessTokenAuthentication])
def detailed_view(request):
# Votre logique de point de terminaison protégé
return Response({
"auth": request.user.auth.to_dict(),
"message": "Données protégées accessibles avec succès"
})
Ou en utilisant des vues basées sur des classes :
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):
# Accédez aux informations d'authentification depuis request.user.auth
return Response({"auth": request.user.auth.to_dict()})
class DetailedView(APIView):
authentication_classes = [AccessTokenAuthentication]
def get(self, request):
# Votre logique de point de terminaison protégé
return Response({
"auth": request.user.auth.to_dict(),
"message": "Données protégées accessibles avec succès"
})
- 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()
// Appliquer le middleware aux routes protégées
r.GET("/api/protected", VerifyAccessToken(), func(c *gin.Context) {
// Informations du jeton d’accès directement depuis le contexte
tokenInterface, exists := c.Get("auth")
if !exists {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Token not found"})
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()
// Appliquer le middleware aux routes protégées
e.GET("/api/protected", func(c echo.Context) error {
// Informations du jeton d’accès directement depuis le contexte
tokenInterface := c.Get("auth")
if tokenInterface == nil {
return c.JSON(http.StatusInternalServerError, echo.Map{"error": "Token not found"})
}
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")
}
Ou en utilisant des groupes de routes :
package main
import (
"github.com/labstack/echo/v4"
"github.com/lestrrat-go/jwx/v3/jwt"
)
func main() {
e := echo.New()
// Créer un groupe de routes protégées
api := e.Group("/api", VerifyAccessToken)
api.GET("/protected", func(c echo.Context) error {
// Informations du jeton d’accès directement depuis le contexte
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": "Accès aux données protégées réussi",
})
})
e.Start(":8080")
}
package main
import (
"github.com/gofiber/fiber/v2"
"github.com/lestrrat-go/jwx/v3/jwt"
)
func main() {
app := fiber.New()
// Appliquer le middleware aux routes protégées
app.Get("/api/protected", VerifyAccessToken, func(c *fiber.Ctx) error {
// Informations du jeton d’accès directement depuis locals
tokenInterface := c.Locals("auth")
if tokenInterface == nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Token not found"})
}
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")
}
Ou en utilisant des groupes de routes :
package main
import (
"github.com/gofiber/fiber/v2"
"github.com/lestrrat-go/jwx/v3/jwt"
)
func main() {
app := fiber.New()
// Créer un groupe de routes protégées
api := app.Group("/api", VerifyAccessToken)
api.Get("/protected", func(c *fiber.Ctx) error {
// Informations du jeton d’accès directement depuis locals
token := c.Locals("auth").(jwt.Token)
return c.JSON(fiber.Map{
"sub": token.Subject(),
"client_id": getStringClaim(token, "client_id"),
"organization_id": getStringClaim(token, "organization_id"),
"scopes": getScopesFromToken(token),
"audience": getAudienceFromToken(token),
"message": "Accès aux données protégées réussi",
})
})
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()
// Appliquer le middleware aux routes protégées
r.With(VerifyAccessToken).Get("/api/protected", func(w http.ResponseWriter, r *http.Request) {
// Informations du jeton d’accès directement depuis le contexte
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 not found"})
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)
}
Ou en utilisant des groupes de routes :
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()
// Créer un groupe de routes protégées
r.Route("/api", func(r chi.Router) {
r.Use(VerifyAccessToken)
r.Get("/protected", func(w http.ResponseWriter, r *http.Request) {
// Informations du jeton d’accès directement depuis le contexte
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": "Accès aux données protégées réussi",
})
})
})
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) {
// Accéder aux informations du jeton d’accès directement depuis le JWT
String scopes = jwt.getClaimAsString("scope");
List<String> scopeList = scopes != null ? Arrays.asList(scopes.split(" ")) : List.of();
return Map.of(
"sub", jwt.getSubject(),
"client_id", jwt.getClaimAsString("client_id"),
"organization_id", jwt.getClaimAsString("organization_id"),
"scopes", scopeList,
"audience", jwt.getAudience()
);
}
@GetMapping("/api/protected/detailed")
public Map<String, Object> detailedEndpoint(@AuthenticationPrincipal Jwt jwt) {
// Votre logique de point de terminaison protégé
return Map.of(
"sub", jwt.getSubject(),
"client_id", jwt.getClaimAsString("client_id"),
"organization_id", jwt.getClaimAsString("organization_id"),
"scopes", jwt.getClaimAsString("scope"),
"audience", jwt.getAudience(),
"message", "Données protégées accessibles avec succès"
);
}
}
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) {
// Accéder au JWT directement via l’injection ou le contexte
JsonWebToken token = (JsonWebToken) requestContext.getProperty("auth");
if (token == null) {
token = jwt; // Repli sur le JWT injecté
}
String scopes = token.getClaim("scope");
List<String> scopeList = scopes != null ? Arrays.asList(scopes.split(" ")) : List.of();
return Map.of(
"sub", token.getSubject(),
"client_id", token.<String>getClaim("client_id"),
"organization_id", token.<String>getClaim("organization_id"),
"scopes", scopeList,
"audience", token.getAudience()
);
}
@GET
@Path("/protected/detailed")
@Produces(MediaType.APPLICATION_JSON)
public Map<String, Object> detailedEndpoint() {
// Votre logique de point de terminaison protégé
String scopes = jwt.getClaim("scope");
return Map.of(
"sub", jwt.getSubject(),
"client_id", jwt.<String>getClaim("client_id"),
"organization_id", jwt.<String>getClaim("organization_id"),
"scopes", scopes,
"audience", jwt.getAudience(),
"message", "Données protégées accessibles avec succès"
);
}
}
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) {
// Accéder aux informations du jeton d’accès directement depuis Authentication
String scopes = (String) authentication.getAttributes().get("scope");
List<String> scopeList = scopes != null ? Arrays.asList(scopes.split(" ")) : List.of();
return Map.of(
"sub", authentication.getName(),
"client_id", authentication.getAttributes().get("client_id"),
"organization_id", authentication.getAttributes().get("organization_id"),
"scopes", scopeList,
"audience", authentication.getAttributes().get("aud")
);
}
@Get("/protected/detailed")
public Map<String, Object> detailedEndpoint(Authentication authentication) {
// Votre logique de point de terminaison protégé
return Map.of(
"sub", authentication.getName(),
"client_id", authentication.getAttributes().get("client_id"),
"organization_id", authentication.getAttributes().get("organization_id"),
"scopes", authentication.getAttributes().get("scope"),
"audience", authentication.getAttributes().get("aud"),
"message", "Données protégées accessibles avec succès"
);
}
}
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);
// Appliquer le middleware aux routes protégées
router.route("/api/protected*").handler(new JwtAuthHandler(vertx));
router.get("/api/protected").handler(this::protectedEndpoint);
router.get("/api/protected/detailed").handler(this::detailedEndpoint);
vertx.createHttpServer()
.requestHandler(router)
.listen(8080, result -> {
if (result.succeeded()) {
startPromise.complete();
} else {
startPromise.fail(result.cause());
}
});
}
private void protectedEndpoint(RoutingContext context) {
// Accéder au principal JWT directement depuis le contexte
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());
}
private void detailedEndpoint(RoutingContext context) {
// Votre logique de point de terminaison protégé
JsonObject principal = context.get("auth");
JsonObject response = new JsonObject()
.put("sub", principal.getString("sub"))
.put("client_id", principal.getString("client_id"))
.put("organization_id", principal.getString("organization_id"))
.put("scopes", principal.getString("scope"))
.put("audience", principal.getJsonArray("aud"))
.put("message", "Données protégées accessibles avec succès");
context.response()
.putHeader("Content-Type", "application/json")
.end(response.encode());
}
}
Nous avons déjà configuré les middlewares d’authentification (Authentication) et d’autorisation (Authorization) dans les sections précédentes. Nous pouvons maintenant créer un contrôleur protégé qui valide les jetons d’accès (Access tokens) et extrait les revendications (Claims) des requêtes authentifiées.
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using System.Security.Claims;
namespace YourApiNamespace.Controllers
{
[ApiController]
[Route("api/[controller]")]
[Authorize] // Exige l’authentification pour toutes les actions de ce contrôleur
public class ProtectedController : ControllerBase
{
[HttpGet]
public IActionResult GetProtectedData()
{
// Informations du jeton d’accès (Access token) directement depuis les revendications (Claims) de l’utilisateur
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()
{
// Retourne toutes les revendications (Claims) pour le débogage / l’inspection
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) {
// Accéder aux informations d'authentification à partir des attributs de la requête
$auth = $request->attributes->get('auth');
return ['auth' => $auth->toArray()];
});
Route::get('/api/protected/detailed', function (Request $request) {
// Votre logique de point de terminaison protégé
$auth = $request->attributes->get('auth');
return [
'auth' => $auth->toArray(),
'message' => 'Données protégées accessibles avec succès'
];
});
});
Ou en utilisant des contrôleurs :
<?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)
{
// Accéder aux informations d'authentification à partir des attributs de la requête
$auth = $request->attributes->get('auth');
return ['auth' => $auth->toArray()];
}
public function show(Request $request)
{
// Votre logique de point de terminaison protégé
$auth = $request->attributes->get('auth');
return [
'auth' => $auth->toArray(),
'message' => 'Données protégées accessibles avec succès'
];
}
}
<?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
{
// Accéder aux informations d'authentification à partir des attributs de la requête
$auth = $request->attributes->get('auth');
return $this->json(['auth' => $auth->toArray()]);
}
#[Route('/detailed', methods: ['GET'])]
public function detailed(Request $request): JsonResponse
{
// Votre logique de point de terminaison protégé
$auth = $request->attributes->get('auth');
return $this->json([
'auth' => $auth->toArray(),
'message' => 'Données protégées accessibles avec succès'
]);
}
}
<?php
use App\Middleware\JwtMiddleware;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Slim\Factory\AppFactory;
require __DIR__ . '/../vendor/autoload.php';
$app = AppFactory::create();
// Appliquer le middleware aux routes protégées
$app->group('/api/protected', function ($group) {
$group->get('', function (Request $request, Response $response) {
// Accéder aux informations d'authentification à partir des attributs de la requête
$auth = $request->getAttribute('auth');
$response->getBody()->write(json_encode(['auth' => $auth->toArray()]));
return $response->withHeader('Content-Type', 'application/json');
});
$group->get('/detailed', function (Request $request, Response $response) {
// Votre logique de point de terminaison protégé
$auth = $request->getAttribute('auth');
$data = [
'auth' => $auth->toArray(),
'message' => 'Données protégées accessibles avec succès'
];
$response->getBody()->write(json_encode($data));
return $response->withHeader('Content-Type', 'application/json');
});
})->add(new JwtMiddleware());
// Point de terminaison public (non protégé)
$app->get('/', function (Request $request, Response $response) {
$response->getBody()->write(json_encode(['message' => 'Point de terminaison public']));
return $response->withHeader('Content-Type', 'application/json');
});
$app->run();
Ou en utilisant une approche plus structurée :
<?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
{
// Accéder aux informations d'authentification à partir des attributs de la requête
$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
{
// Votre logique de point de terminaison protégé
$auth = $request->getAttribute('auth');
$data = [
'auth' => $auth->toArray(),
'message' => 'Données protégées accessibles avec succès'
];
$response->getBody()->write(json_encode($data));
return $response->withHeader('Content-Type', 'application/json');
}
}
- Rails
- Sinatra
- Grape
class ApplicationController < ActionController::API # Pour les applications API uniquement
# class ApplicationController < ActionController::Base # Pour les applications Rails complètes
include JwtAuthentication
end
class Api::ProtectedController < ApplicationController
before_action :verify_access_token
def index
# Accéder aux informations d'authentification depuis @auth
render json: { auth: @auth.to_h }
end
def show
# Votre logique de point de terminaison protégé
render json: {
auth: @auth.to_h,
message: "Données protégées accessibles avec succès"
}
end
end
Rails.application.routes.draw do
namespace :api do
resources :protected, only: [:index, :show]
end
end
require 'sinatra'
require 'json'
require_relative 'auth_middleware'
require_relative 'auth_constants'
require_relative 'auth_info'
require_relative 'authorization_error'
require_relative 'auth_helpers'
require_relative 'jwt_validator'
# Appliquer le middleware
use AuthMiddleware
get '/api/protected' do
content_type :json
# Accéder aux informations d'authentification depuis env
auth = env['auth']
{ auth: auth.to_h }.to_json
end
get '/api/protected/:id' do
content_type :json
# Votre logique de point de terminaison protégé
auth = env['auth']
{
auth: auth.to_h,
id: params[:id],
message: "Données protégées accessibles avec succès"
}.to_json
end
# Point de terminaison public (non protégé par le middleware)
get '/' do
content_type :json
{ message: "Point de terminaison public" }.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
# Accéder aux informations d'authentification depuis le helper d'auth
{ auth: auth.to_h }
end
get ':id' do
# Votre logique de point de terminaison protégé
{
auth: auth.to_h,
id: params[:id],
message: "Données protégées accessibles avec succès"
}
end
end
end
# Point de terminaison public (non protégé)
get :public do
{ message: "Point de terminaison public" }
end
end
require_relative 'api'
run API
- Axum
- Actix Web
- Rocket
use axum::{
extract::Extension,
http::StatusCode,
middleware,
response::Json,
routing::get,
Router,
};
use serde_json::{json, Value};
use std::sync::Arc;
use tower_http::cors::CorsLayer;
mod lib;
mod jwt_validator;
mod middleware as jwt_middleware;
use lib::AuthInfo;
use jwt_validator::JwtValidator;
#[tokio::main]
async fn main() {
let validator = Arc::new(JwtValidator::new().await.expect("Failed to initialize JWT validator"));
let app = Router::new()
.route("/api/protected", get(protected_handler))
.route("/api/protected/detailed", get(detailed_handler))
.layer(middleware::from_fn(jwt_middleware::jwt_middleware))
.layer(Extension(validator))
.layer(CorsLayer::permissive());
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}
async fn protected_handler(Extension(auth): Extension<AuthInfo>) -> Json<Value> {
// Accédez directement aux informations d'authentification depuis Extension
Json(json!({ "auth": auth }))
}
async fn detailed_handler(Extension(auth): Extension<AuthInfo>) -> Json<Value> {
// Votre logique de point de terminaison protégé
Json(json!({
"auth": auth,
"message": "Données protégées accessibles avec succès"
}))
}
use actix_web::{middleware::Logger, web, App, HttpRequest, HttpServer, Result};
use serde_json::{json, Value};
use std::sync::Arc;
mod lib;
mod jwt_validator;
mod middleware as jwt_middleware;
use lib::AuthInfo;
use jwt_validator::JwtValidator;
use jwt_middleware::JwtMiddleware;
#[actix_web::main]
async fn main() -> std::io::Result<()> {
let validator = Arc::new(JwtValidator::new().await.expect("Failed to initialize JWT validator"));
HttpServer::new(move || {
App::new()
.app_data(web::Data::new(validator.clone()))
.wrap(Logger::default())
.service(
web::scope("/api/protected")
.wrap(JwtMiddleware::new(validator.clone()))
.route("", web::get().to(protected_handler))
.route("/detailed", web::get().to(detailed_handler))
)
})
.bind("127.0.0.1:8080")?
.run()
.await
}
async fn protected_handler(req: HttpRequest) -> Result<web::Json<Value>> {
// Accédez aux informations d'authentification depuis les extensions de la requête
let auth = req.extensions().get::<AuthInfo>().unwrap();
Ok(web::Json(json!({ "auth": auth })))
}
async fn detailed_handler(req: HttpRequest) -> Result<web::Json<Value>> {
// Votre logique de point de terminaison protégé
let auth = req.extensions().get::<AuthInfo>().unwrap();
Ok(web::Json(json!({
"auth": auth,
"message": "Données protégées accessibles avec succès"
})))
}
use rocket::{get, launch, routes, serde::json::Json, State};
use serde_json::{json, Value};
mod lib;
mod jwt_validator;
mod guards;
use lib::AuthInfo;
use jwt_validator::JwtValidator;
#[get("/api/protected")]
fn protected_handler(auth: AuthInfo) -> Json<Value> {
// Accédez directement aux informations d'authentification depuis le guard de requête
Json(json!({ "auth": auth }))
}
#[get("/api/protected/detailed")]
fn detailed_handler(auth: AuthInfo) -> Json<Value> {
// Votre logique de point de terminaison protégé
Json(json!({
"auth": auth,
"message": "Données protégées accessibles avec succès"
}))
}
#[launch]
async fn rocket() -> _ {
let validator = JwtValidator::new().await.expect("Failed to initialize JWT validator");
rocket::build()
.manage(validator)
.mount("/", routes![protected_handler, detailed_handler])
}
Étape 5 : Tester votre implémentation
Testez votre API avec des jetons valides et invalides pour vérifier :
- Les jetons valides passent et donnent accès.
- Retournez
401 Unauthorized
pour les jetons invalides / manquants. Retournez403 Forbidden
pour les jetons valides qui n’ont pas les permissions ou le contexte requis.
Ressources associées
Personnaliser les revendications de jeton JSON Web Token (JWT)OpenID Connect Discovery
RFC 8707 : Indicateurs de ressource