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 accompagne dans la vérification des 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 flux 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 <jeton d’accès (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 class AuthorizationException extends RuntimeException {
private final int statusCode;
public AuthorizationException(String message) {
this(message, 403); // Par défaut à 403 Interdit
}
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 les signatures JWT.
- Émetteur (Issuer) : La valeur attendue de l’émetteur (l’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 que vous avez 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 plus de 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 (Issuer), 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 charge de maintenance si une configuration change à l’avenir.
- URI JWKS :
https://<your-logto-endpoint>/oidc/jwks
- Émetteur (Issuer) :
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 de l’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
.
Consultez JSON Web Token pour en savoir plus sur la structure et les revendications des JWT.
À vérifier selon chaque modèle de permission
- Ressources API globales
- Permissions d'organisation (hors API)
- Ressources API au niveau organisation
- Revendication Audience (
aud
) : Indicateur de ressource API - Revendication Organisation (
organization_id
) : Non présent - Portées (permissions) à vérifier (
scope
) : Permissions de ressource API
- Revendication Audience (
aud
) :urn:logto:organization:<id>
(le contexte d'organisation est dansaud
) - Revendication Organisation (
organization_id
) : Non présent - Portées (permissions) à vérifier (
scope
) : Permissions d'organisation
- Revendication Audience (
aud
) : Indicateur de ressource API - Revendication Organisation (
organization_id
) : ID de l'organisation (doit correspondre à la requête) - Portées (permissions) à vérifier (
scope
) : Permissions de ressource API
Pour les permissions d’organisation hors 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 / scopes) et le contexte (audience, organisation) pour sécuriser les API multi-tenant.
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 validateJwt(token: string): Promise<JWTPayload> {
const { payload } = await jwtVerify(token, jwks, {
issuer: ISSUER,
});
verifyPayload(payload);
return payload;
}
export function createAuthInfo(payload: JWTPayload): AuthInfo {
const scopes = (payload.scope as string)?.split(' ') ?? [];
const audience = Array.isArray(payload.aud) ? payload.aud : payload.aud ? [payload.aud] : [];
return new AuthInfo(
payload.sub!,
payload.client_id as string,
payload.organization_id as string,
scopes,
audience
);
}
function verifyPayload(payload: JWTPayload): void {
// 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
}
Ensuite, implémentez le middleware pour vérifier le jeton d’accès (Access token) :
- Express.js
- Fastify
- Hapi.js
- Koa.js
- NestJS
import { Request, Response, NextFunction } from 'express';
import { validateJwt, 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 validateJwt(token);
// Stocker les informations d'authentification 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 { FastifyRequest, FastifyReply } from 'fastify';
import { validateJwt, createAuthInfo } from './jwt-validator.js';
// Étendre l'interface Fastify Request 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 validateJwt(token);
// Stocker les informations d'authentification 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 { validateJwt, createAuthInfo } from './jwt-validator.js';
export async function hapiVerifyAccessToken(request: Request, h: ResponseToolkit) {
try {
const token = extractBearerTokenFromHeaders(request.headers);
const payload = await validateJwt(token);
// Stocker les informations d'authentification dans request.app pour une utilisation 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 { Context, Next } from 'koa';
import { validateJwt, createAuthInfo } from './jwt-validator.js';
export async function koaVerifyAccessToken(ctx: Context, next: Next) {
try {
const token = extractBearerTokenFromHeaders(ctx.request.headers);
const payload = await validateJwt(token);
// Stocker les informations d'authentification dans l'état pour une utilisation générique
ctx.state.auth = createAuthInfo(payload);
await next();
} catch (err: any) {
ctx.status = err.status ?? 401;
ctx.body = { error: err.message };
}
}
import {
Injectable,
CanActivate,
ExecutionContext,
UnauthorizedException,
ForbiddenException,
} from '@nestjs/common';
import { validateJwt, createAuthInfo } from './jwt-validator.js';
@Injectable()
export class AccessTokenGuard implements CanActivate {
async canActivate(context: ExecutionContext): Promise<boolean> {
const req = context.switchToHttp().getRequest();
try {
const token = extractBearerTokenFromHeaders(req.headers);
const payload = await validateJwt(token);
// Stocker les informations d'authentification 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érifiez que la revendication d’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érifiez 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érifiez que la revendication d’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érifiez 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érifiez 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érifiez que la revendication d’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érifiez 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érifiez 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: str) -> Dict[str, Any]:
"""Valider le 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
# Ceci sera détaillé dans la section sur les 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, create_auth_info
security = HTTPBearer()
async def verify_access_token(credentials: HTTPAuthorizationCredentials = Depends(security)) -> AuthInfo:
try:
token = credentials.credentials
payload = validate_jwt(token)
return create_auth_info(payload)
except AuthorizationError as e:
raise HTTPException(status_code=e.status, detail=str(e))
from functools import wraps
from flask import request, jsonify, g
from jwt_validator import validate_jwt, create_auth_info
def verify_access_token(f):
@wraps(f)
def decorated_function(*args, **kwargs):
try:
token = extract_bearer_token_from_headers(dict(request.headers))
payload = validate_jwt(token)
# Stocker les informations d'authentification dans l'objet g de Flask pour une utilisation 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, 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)
# Attacher les informations d'authentification à la requête pour une utilisation 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, 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 un JWT.
"""
try:
payload = validate_jwt(key)
auth_info = create_auth_info(payload)
# Créer un objet de type utilisateur qui contient les informations 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
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
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
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
Commencez par ajouter 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())
}
}
// validateJWT valide le JWT et retourne le jeton analysé
func validateJWT(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 (access token) :
- Gin
- Fiber
- Echo
- Chi
import "github.com/gin-gonic/gin"
func VerifyAccessToken() gin.HandlerFunc {
return func(c *gin.Context) {
tokenString, err := extractBearerTokenFromHeaders(c.Request)
if err != nil {
authErr := err.(*AuthorizationError)
c.JSON(authErr.Status, gin.H{"error": authErr.Message})
c.Abort()
return
}
token, err := validateJWT(tokenString)
if err != nil {
authErr := err.(*AuthorizationError)
c.JSON(authErr.Status, gin.H{"error": authErr.Message})
c.Abort()
return
}
// Stocker le jeton dans le contexte pour une utilisation générique (Store token in context for generic use)
c.Set("auth", token)
c.Next()
}
}
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 := validateJWT(tokenString)
if err != nil {
authErr := err.(*AuthorizationError)
return c.Status(authErr.Status).JSON(fiber.Map{"error": authErr.Message})
}
// Stocker le jeton dans locals pour une utilisation générique
c.Locals("auth", token)
return c.Next()
}
import "github.com/labstack/echo/v4"
func VerifyAccessToken(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
tokenString, err := extractBearerTokenFromHeaders(c.Request())
if err != nil {
authErr := err.(*AuthorizationError)
return c.JSON(authErr.Status, echo.Map{"error": authErr.Message})
}
token, err := validateJWT(tokenString)
if err != nil {
authErr := err.(*AuthorizationError)
return c.JSON(authErr.Status, echo.Map{"error": authErr.Message})
}
// Stocker le jeton dans le contexte pour une utilisation générique (Store token in context for generic use)
c.Set("auth", token)
return next(c)
}
}
import (
"context"
"encoding/json"
"net/http"
)
type contextKey string
const AuthContextKey contextKey = "auth"
func VerifyAccessToken(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
tokenString, err := extractBearerTokenFromHeaders(r)
if err != nil {
authErr := err.(*AuthorizationError)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(authErr.Status)
json.NewEncoder(w).Encode(map[string]string{"error": authErr.Message})
return
}
token, err := validateJWT(tokenString)
if err != nil {
authErr := err.(*AuthorizationError)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(authErr.Status)
json.NewEncoder(w).Encode(map[string]string{"error": authErr.Message})
return
}
// Stocker le jeton dans le contexte pour une utilisation générique
ctx := context.WithValue(r.Context(), AuthContextKey, token)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
Selon votre modèle de permissions, 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("ID d'organisation non correspondant")
}
// 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("ID d'organisation non correspondant")
}
// 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 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>
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() {
// N'oubliez pas de définir ces variables d'environnement dans votre déploiement
String jwksUri = System.getenv("JWKS_URI");
String issuer = System.getenv("JWT_ISSUER");
return NimbusJwtDecoder.withJwkSetUri(jwksUri)
.issuer(issuer)
.build();
}
}
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.stereotype.Component;
import java.util.List;
@Component
public class JwtValidator {
public void verifyPayload(Jwt jwt) {
// La validation de l’émetteur (Issuer) est gérée automatiquement par le décodeur JWT de Spring Security
// Implémentez ici votre logique de vérification supplémentaire basée sur le modèle de permission (Permission)
// Utilisez les méthodes utilitaires ci-dessous pour extraire les revendications (Claims)
// Exemple : throw new AuthorizationException("Insufficient permissions");
// Le code de statut sera géré par la gestion des exceptions de Spring Security
}
// Méthodes utilitaires pour JWT avec Spring Boot
private List<String> extractAudiences(Jwt jwt) {
return jwt.getAudience();
}
private String extractScopes(Jwt jwt) {
return jwt.getClaimAsString("scope");
}
private String extractOrganizationId(Jwt jwt) {
return jwt.getClaimAsString("organization_id");
}
}
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>
# Configuration JWT (Jeton d’identifiant)
mp.jwt.verify.publickey.location=${JWKS_URI:https://your-tenant.logto.app/oidc/jwks}
mp.jwt.verify.issuer=${JWT_ISSUER:https://your-tenant.logto.app/oidc}
import org.eclipse.microprofile.jwt.JsonWebToken;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.ws.rs.container.ContainerRequestContext;
import jakarta.ws.rs.container.ContainerRequestFilter;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.ext.Provider;
import java.util.ArrayList;
import java.util.List;
@Provider
@ApplicationScoped
public class JwtVerificationFilter implements ContainerRequestFilter {
@Inject
JsonWebToken jwt;
@Override
public void filter(ContainerRequestContext requestContext) {
if (requestContext.getUriInfo().getPath().startsWith("/api/protected")) {
try {
verifyPayload(jwt);
requestContext.setProperty("auth", jwt);
} catch (AuthorizationException e) {
requestContext.abortWith(
Response.status(e.getStatusCode())
.entity("{\"error\": \"" + e.getMessage() + "\"}")
.build()
);
} catch (Exception e) {
requestContext.abortWith(
Response.status(401)
.entity("{\"error\": \"Invalid token\"}")
.build()
);
}
}
}
private void verifyPayload(JsonWebToken jwt) {
// La validation de l’émetteur (Issuer) est gérée automatiquement par l’extension Quarkus JWT
// Implémentez ici votre logique de vérification supplémentaire selon votre modèle de permission (Permission)
// Utilisez les méthodes utilitaires ci-dessous pour extraire les revendications (Claims)
}
// Méthodes utilitaires pour Quarkus JWT
private List<String> extractAudiences(JsonWebToken jwt) {
return new ArrayList<>(jwt.getAudience());
}
private String extractScopes(JsonWebToken jwt) {
return jwt.getClaim("scope");
}
private String extractOrganizationId(JsonWebToken jwt) {
return jwt.getClaim("organization_id");
}
}
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>
micronaut:
security:
authentication: bearer
token:
jwt:
signatures:
jwks:
logto:
url: ${JWKS_URI:https://your-tenant.logto.app/oidc/jwks}
claims-validators:
issuer: ${JWT_ISSUER:https://your-tenant.logto.app/oidc}
import io.micronaut.security.token.Claims;
import io.micronaut.security.token.validator.TokenValidator;
import jakarta.inject.Singleton;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Mono;
import java.util.Arrays;
import java.util.List;
@Singleton
public class JwtClaimsValidator implements TokenValidator {
@Override
public Publisher<Boolean> validateToken(String token, Claims claims) {
try {
verifyPayload(claims);
return Mono.just(true);
} catch (AuthorizationException e) {
// Micronaut gérera le code de statut de manière appropriée
return Mono.just(false);
}
}
private void verifyPayload(Claims claims) {
// La validation de l’émetteur (Issuer) est gérée automatiquement par la configuration JWT de Micronaut
// Implémentez ici votre logique de vérification supplémentaire basée sur le modèle de permission (Permission)
// Utilisez les méthodes utilitaires ci-dessous pour l’extraction des revendications (Claims)
// Exemple : throw new AuthorizationException("Permissions insuffisantes");
}
// Méthodes utilitaires pour Micronaut JWT
@SuppressWarnings("unchecked")
private List<String> extractAudiences(Claims claims) {
Object aud = claims.get("aud");
if (aud instanceof List) {
return (List<String>) aud;
} else if (aud instanceof String) {
return Arrays.asList((String) aud);
}
return List.of();
}
private String extractScopes(Claims claims) {
return (String) claims.get("scope");
}
private String extractOrganizationId(Claims claims) {
return (String) claims.get("organization_id");
}
}
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>
import io.vertx.core.Future;
import io.vertx.core.Handler;
import io.vertx.core.Vertx;
import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.auth.jwt.JWTAuth;
import io.vertx.ext.auth.jwt.JWTAuthOptions;
import io.vertx.ext.web.RoutingContext;
import io.vertx.ext.web.client.WebClient;
import java.util.List;
import java.util.ArrayList;
public class JwtAuthHandler implements Handler<RoutingContext> {
private final JWTAuth jwtAuth;
private final WebClient webClient;
private final String expectedIssuer;
private final String jwksUri;
public JwtAuthHandler(Vertx vertx) {
this.webClient = WebClient.create(vertx);
this.jwtAuth = JWTAuth.create(vertx, new JWTAuthOptions());
// N'oubliez pas de définir ces variables d'environnement dans votre déploiement
this.expectedIssuer = System.getenv("JWT_ISSUER");
this.jwksUri = System.getenv("JWKS_URI");
// Récupérer le JWKS et configurer l'authentification JWT
fetchJWKS().onSuccess(jwks -> {
// Configurer le JWKS (simplifié - vous pourriez 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("jwt", token))
.onSuccess(user -> {
try {
JsonObject principal = user.principal();
verifyPayload(principal);
context.put("auth", principal);
context.next();
} catch (AuthorizationException e) {
context.response()
.setStatusCode(e.getStatusCode()) // Utiliser le code d'état de l'exception
.putHeader("Content-Type", "application/json")
.end("{\"error\": \"" + e.getMessage() + "\"}");
} catch (Exception e) {
context.response()
.setStatusCode(401)
.putHeader("Content-Type", "application/json")
.end("{\"error\": \"Invalid token\"}");
}
})
.onFailure(err -> {
context.response()
.setStatusCode(401)
.putHeader("Content-Type", "application/json")
.end("{\"error\": \"Invalid token: " + err.getMessage() + "\"}");
});
}
private Future<JsonObject> fetchJWKS() {
return webClient.getAbs(this.jwksUri)
.send()
.map(response -> response.bodyAsJsonObject());
}
private void verifyPayload(JsonObject principal) {
// Vérifier l'émetteur manuellement pour Vert.x
String issuer = principal.getString("iss");
if (issuer == null || !expectedIssuer.equals(issuer)) {
throw new AuthorizationException("Invalid issuer: " + issuer);
}
// Implémentez ici votre logique de vérification supplémentaire basée sur le modèle de permission
// Utilisez les méthodes utilitaires ci-dessous pour l'extraction des revendications
}
// Méthodes utilitaires pour Vert.x JWT
private List<String> extractAudiences(JsonObject principal) {
JsonArray audiences = principal.getJsonArray("aud");
if (audiences != null) {
List<String> result = new ArrayList<>();
for (Object aud : audiences) {
result.add(aud.toString());
}
return result;
}
return List.of();
}
private String extractScopes(JsonObject principal) {
return principal.getString("scope");
}
private String extractOrganizationId(JsonObject principal) {
return principal.getString("organization_id");
}
}
Selon votre modèle de permission (Permission), implémentez la logique de vérification appropriée :
- Ressources API globales
- Permissions d'organisation (hors API)
- Ressources API au niveau de l'organisation
// Vérifiez que la revendication d'audience (Audience) correspond à votre indicateur de ressource API
List<String> audiences = extractAudiences(token); // Extraction spécifique au framework
if (!audiences.contains("https://your-api-resource-indicator")) {
throw new AuthorizationException("Audience invalide");
}
// Vérifiez les portées (Scopes) requises pour les ressources API globales
List<String> requiredScopes = Arrays.asList("api:read", "api:write"); // Remplacez par vos portées requises réelles
String scopes = extractScopes(token); // Extraction spécifique au framework
List<String> tokenScopes = scopes != null ? Arrays.asList(scopes.split(" ")) : List.of();
if (!tokenScopes.containsAll(requiredScopes)) {
throw new AuthorizationException("Portée insuffisante");
}
// Vérifiez que la revendication d'audience (Audience) correspond au format d'organisation
List<String> audiences = extractAudiences(token); // Extraction spécifique au framework
boolean hasOrgAudience = audiences.stream()
.anyMatch(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)
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("ID d'organisation non correspondant");
}
// Vérifiez les portées (Scopes) d'organisation requises
List<String> requiredScopes = Arrays.asList("invite:users", "manage:settings"); // Remplacez par vos portées requises réelles
String scopes = extractScopes(token); // Extraction spécifique au framework
List<String> tokenScopes = scopes != null ? Arrays.asList(scopes.split(" ")) : List.of();
if (!tokenScopes.containsAll(requiredScopes)) {
throw new AuthorizationException("Portée d'organisation insuffisante");
}
// Vérifiez que la revendication d'audience (Audience) correspond à votre indicateur de ressource API
List<String> audiences = extractAudiences(token); // Extraction spécifique au framework
if (!audiences.contains("https://your-api-resource-indicator")) {
throw new AuthorizationException("Audience invalide pour les ressources API au niveau de l'organisation");
}
// Vérifiez que l'ID d'organisation correspond au contexte (vous devrez peut-être l'extraire du contexte de la requête)
String expectedOrgId = "your-organization-id"; // À extraire du contexte de la requête
String orgId = extractOrganizationId(token); // Extraction spécifique au framework
if (!expectedOrgId.equals(orgId)) {
throw new AuthorizationException("ID d'organisation non correspondant");
}
// Vérifiez les portées (Scopes) 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 réelles
String scopes = extractScopes(token); // Extraction spécifique au framework
List<String> tokenScopes = scopes != null ? Arrays.asList(scopes.split(" ")) : List.of();
if (!tokenScopes.containsAll(requiredScopes)) {
throw new AuthorizationException("Portées API au niveau de l'organisation insuffisantes");
}
Les méthodes utilitaires pour extraire les revendications (Claims) sont spécifiques au framework. Consultez les détails d'implémentation dans les fichiers de validation spécifiques au framework ci-dessus.
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 :
composer require firebase/php-jwt
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 validateJwt(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
// Ceci sera détaillé dans la section sur les 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::validateJwt($token);
// Stocker les informations d'authentification dans les attributs de la requête pour une utilisation 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::validateJwt($token);
$authInfo = JwtValidator::createAuthInfo($payload);
// Stocker les informations 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::validateJwt($token);
// Stocker les informations d'authentification dans les attributs de la requête pour une utilisation 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 de l’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 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
$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 de l’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 de l’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('Failed to fetch JWKS', 401) unless response.is_a?(Net::HTTPSuccess)
jwks_data = JSON.parse(response.body)
JWT::JWK::Set.new(jwks_data)
end
end
def self.validate_jwt(token)
jwks = fetch_jwks
# Laissez la bibliothèque JWT gérer la détection de l’algorithme à partir du 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]
verify_payload(decoded_token)
decoded_token
end
def self.create_auth_info(payload)
scopes = payload['scope']&.split(' ') || []
audience = payload['aud'] || []
AuthInfo.new(
payload['sub'],
payload['client_id'],
payload['organization_id'],
scopes,
audience
)
end
def self.verify_payload(payload)
# Implémentez ici votre logique de vérification selon le modèle de permission
# Cela sera montré dans la section sur les modèles de permission ci-dessous
end
end
Ensuite, implémentez le middleware pour vérifier le jeton d’accès (access token) :
- Ruby on Rails
- Sinatra
- Grape
module JwtAuthentication
extend ActiveSupport::Concern
include AuthHelpers
included do
before_action :verify_access_token, only: [:protected_action] # Ajouter des actions spécifiques
end
private
def verify_access_token
begin
token = extract_bearer_token(request)
decoded_token = JwtValidator.validate_jwt(token)
# Stocker les informations d'authentification pour une utilisation 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 des routes spécifiques
if request.path.start_with?('/api/protected')
begin
token = extract_bearer_token(request)
decoded_token = JwtValidator.validate_jwt(token)
# Stocker les informations 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: 'Invalid token' }.to_json]]
end
end
@app.call(env)
end
end
module GrapeAuthHelpers
include AuthHelpers
def authenticate_user!
begin
token = extract_bearer_token(request)
decoded_token = JwtValidator.validate_jwt(token)
# Stocker les informations d'authentification pour une utilisation 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 de l’organisation
def self.verify_payload(payload)
# Vérifiez 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('Invalid audience')
end
# Vérifiez 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('Insufficient scope')
end
end
def self.verify_payload(payload)
# Vérifiez 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('Invalid audience for organization permissions')
end
# Vérifiez 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('Organization ID mismatch')
end
# Vérifiez 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('Insufficient organization scope')
end
end
def self.verify_payload(payload)
# Vérifiez 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('Invalid audience for organization-level API resources')
end
# Vérifiez 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('Organization ID mismatch')
end
# Vérifiez les portées requises pour les ressources API au niveau de l’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('Insufficient organization-level API scopes')
end
end
Nous utilisons jsonwebtoken pour valider les JWT. Ajoutez les dépendances requises à votre Cargo.toml
:
[dependencies]
jsonwebtoken = "9.0"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
reqwest = { version = "0.11", features = ["json"] }
tokio = { version = "1.0", features = ["full"] }
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(&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;
// Middleware JWT pour la vérification de l'autorisation (Authorization)
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)?;
// Stocker les informations d'authentification (Authentication) 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))
{
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))
{
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érifiez 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érifiez 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érifiez 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érifiez 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érifiez 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érifiez 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érifiez 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érifiez 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();
server.get('/api/protected', { preHandler: fastifyVerifyAccessToken }, (request, reply) => {
// Accédez directement aux informations d'authentification depuis request.auth
reply.send({ auth: request.auth });
});
server.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 directement aux informations d'authentification depuis le paramètre auth
return {"auth": auth.to_dict()}
from flask import Flask, g, jsonify
from auth_middleware import verify_access_token
app = Flask(__name__)
@app.route('/api/protected', methods=['GET'])
@verify_access_token
def protected_endpoint():
# Accéder aux informations d'authentification depuis g.auth
return jsonify({"auth": g.auth.to_dict()})
from django.http import JsonResponse
from auth_middleware import require_access_token
@require_access_token
def protected_view(request):
# Accéder aux informations d'authentification depuis request.auth
return JsonResponse({"auth": request.auth.to_dict()})
from django.urls import path
from . import views
urlpatterns = [
path('api/protected/', views.protected_view, name='protected'),
]
from rest_framework.decorators import api_view, authentication_classes
from rest_framework.response import Response
from auth_middleware import AccessTokenAuthentication
@api_view(['GET'])
@authentication_classes([AccessTokenAuthentication])
def protected_view(request):
# Accéder aux informations d'authentification depuis request.user.auth
return Response({"auth": request.user.auth.to_dict()})
Ou en utilisant des vues basées sur les 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éder aux informations d'authentification depuis request.user.auth
return Response({"auth": request.user.auth.to_dict()})
from django.urls import path
from . import views
urlpatterns = [
path('api/protected/', views.protected_view, name='protected'),
# Ou pour les vues basées sur les classes :
# path('api/protected/', views.ProtectedView.as_view(), name='protected'),
]
- Gin
- Echo
- Fiber
- Chi
package main
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/lestrrat-go/jwx/v3/jwt"
)
func main() {
r := gin.Default()
// Appliquer le middleware aux routes protégées
r.GET("/api/protected", VerifyAccessToken(), func(c *gin.Context) {
// Informations du jeton d’accès (Access token) 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 (Access token) accessibles 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 (Access token) accessibles 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": "Données protégées accessibles avec succès",
})
})
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": "Données protégées accessibles avec succès",
})
})
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 (Access token) 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 (Access token) 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": "Données protégées accessibles avec succès",
})
})
})
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) {
// Informations du jeton d’accès (Access token) 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()
);
}
}
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 depuis 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()
);
}
}
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) {
// Informations du jeton d’accès (Access token) 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")
);
}
}
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);
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());
}
}
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()];
});
});
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()]);
}
}
<?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');
}
}
- Ruby on 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
end
Rails.application.routes.draw do
namespace :api do
resources :protected, only: [:index]
end
end
require 'sinatra'
require 'json'
require_relative 'auth_middleware'
require_relative 'auth_constants'
require_relative 'auth_info'
require_relative 'authorization_error'
require_relative 'auth_helpers'
require_relative 'jwt_validator'
# 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
# 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 l'assistant d'authentification
{ auth: auth.to_h }
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("Échec de l'initialisation du validateur JWT"));
let app = Router::new()
.route("/api/protected", get(protected_handler))
.layer(middleware::from_fn(jwt_middleware::jwt_middleware))
.layer(Extension(validator))
.layer(CorsLayer::permissive());
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}
async fn protected_handler(Extension(auth): Extension<AuthInfo>) -> Json<Value> {
// Accédez directement aux informations d'authentification depuis Extension
Json(json!({ "auth": auth }))
}
use actix_web::{middleware::Logger, web, App, HttpRequest, HttpServer, Result};
use serde_json::{json, Value};
use std::sync::Arc;
mod lib;
mod jwt_validator;
mod middleware as jwt_middleware;
use lib::AuthInfo;
use jwt_validator::JwtValidator;
use jwt_middleware::JwtMiddleware;
#[actix_web::main]
async fn main() -> std::io::Result<()> {
let validator = Arc::new(JwtValidator::new().await.expect("Échec de l'initialisation du validateur JWT"));
HttpServer::new(move || {
App::new()
.app_data(web::Data::new(validator.clone()))
.wrap(Logger::default())
.service(
web::scope("/api/protected")
.wrap(JwtMiddleware::new(validator.clone()))
.route("", web::get().to(protected_handler))
)
})
.bind("127.0.0.1:8080")?
.run()
.await
}
async fn protected_handler(req: HttpRequest) -> Result<web::Json<Value>> {
// Accéder aux informations d'authentification à partir des extensions de la requête
let auth = req.extensions().get::<AuthInfo>().unwrap();
Ok(web::Json(json!({ "auth": auth })))
}
use rocket::{get, launch, routes, serde::json::Json};
use serde_json::{json, Value};
mod lib;
mod jwt_validator;
mod guards;
use lib::AuthInfo;
use jwt_validator::JwtValidator;
#[get("/api/protected")]
fn protected_handler(auth: AuthInfo) -> Json<Value> {
// Accédez directement aux informations d'authentification depuis le request guard (Access auth information directly from request guard)
Json(json!({ "auth": auth }))
}
#[launch]
async fn rocket() -> _ {
let validator = JwtValidator::new().await.expect("Échec de l'initialisation du validateur JWT (Failed to initialize JWT validator)");
rocket::build()
.manage(validator)
.mount("/", routes![protected_handler])
}
Étape 5 : Tester votre implémentation
Obtenir des jetons d’accès (Access tokens)
Depuis votre application cliente :
Si vous avez configuré une intégration client, votre application peut obtenir automatiquement les jetons. Extrayez le jeton d’accès (access token) et utilisez-le dans les requêtes API.
Pour tester avec curl / Postman :
-
Jetons utilisateur : Utilisez les outils développeur de votre application cliente pour copier le jeton d’accès depuis le localStorage ou l’onglet réseau.
-
Jetons machine à machine : Utilisez le flux d’identifiants client (client credentials flow). Voici un exemple non normatif utilisant curl :
curl -X POST https://your-tenant.logto.app/oidc/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=client_credentials" \
-d "client_id=your-m2m-client-id" \
-d "client_secret=your-m2m-client-secret" \
-d "resource=https://your-api-resource-indicator" \
-d "scope=api:read api:write"Vous devrez peut-être ajuster les paramètres
resource
etscope
selon votre ressource API (API resource) et vos permissions ; un paramètreorganization_id
peut également être requis si votre API est liée à une organisation.
Besoin d’inspecter le contenu du jeton ? Utilisez notre décodificateur JWT pour décoder et vérifier vos JWT.
Tester les points de terminaison protégés
Requête avec jeton valide
curl -H "Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..." \
http://localhost:3000/api/protected
Réponse attendue :
{
"auth": {
"sub": "user123",
"clientId": "app456",
"organizationId": "org789",
"scopes": ["api:read", "api:write"],
"audience": ["https://your-api-resource-indicator"]
}
}
Jeton manquant
curl http://localhost:3000/api/protected
Réponse attendue (401) :
{
"error": "Authorization header is missing"
}
Jeton invalide
curl -H "Authorization: Bearer invalid-token" \
http://localhost:3000/api/protected
Réponse attendue (401) :
{
"error": "Invalid token"
}
Tests spécifiques au modèle de permission
- Ressources API globales
- Permissions d’organisation (hors API)
- Ressources API au niveau organisation
Scénarios de test pour les API protégées par des portées globales :
- Portées valides : Testez avec des jetons qui incluent les portées API requises (par exemple,
api:read
,api:write
) - Portées manquantes : Attendez-vous à une réponse 403 Interdit si le jeton ne contient pas les portées requises
- Audience incorrecte : Attendez-vous à une réponse 403 Interdit si l’audience ne correspond pas à la ressource API
# Jeton sans les portées requises - attendre 403
curl -H "Authorization: Bearer token-without-required-scopes" \
http://localhost:3000/api/protected
Scénarios de test pour le contrôle d’accès spécifique à une organisation :
- Jeton d’organisation valide : Testez avec des jetons qui incluent le bon contexte d’organisation (ID d’organisation et portées)
- Portées manquantes : Attendez-vous à une réponse 403 Interdit si l’utilisateur n’a pas les permissions pour l’action demandée
- Mauvaise organisation : Attendez-vous à une réponse 403 Interdit si l’audience ne correspond pas au contexte d’organisation (
urn:logto:organization:<organization_id>
)
# Jeton pour une mauvaise organisation - attendre 403
curl -H "Authorization: Bearer token-for-different-organization" \
http://localhost:3000/api/protected
Scénarios de test combinant la validation de ressource API avec le contexte d’organisation :
- Organisation valide + portées API : Testez avec des jetons ayant à la fois le contexte d’organisation et les portées API requises
- Portées API manquantes : Attendez-vous à une réponse 403 Interdit si le jeton d’organisation ne possède pas les permissions API requises
- Mauvaise organisation : Attendez-vous à une réponse 403 Interdit lors de l’accès à l’API avec un jeton d’une autre organisation
- Audience incorrecte : Attendez-vous à une réponse 403 Interdit si l’audience ne correspond pas à la ressource API au niveau organisation
# Jeton d’organisation sans portées API - attendre 403
curl -H "Authorization: Bearer organization-token-without-api-scopes" \
http://localhost:3000/api/protected
Ressources associées
Personnalisation des revendications de jeton JSON Web Token (JWT)OpenID Connect Discovery
RFC 8707 : Indicateurs de ressource