Enhance authentication and authorization features with Logto integration
- Enrich context with user roles, permissions, and resources from Logto. - Update setAuth function to handle JWT decoding and user data retrieval. - Refactor initGuard to utilize new auth structure. - Modify resource and role services to support new data types and error handling.
This commit is contained in:
parent
b59fa5e267
commit
726a9adf1f
@ -1,9 +1,130 @@
|
|||||||
import { GuardFunction } from "@/graphql/context/types";
|
import { AuthTypes } from "@/graphql/context";
|
||||||
|
import { patContextCache } from "@/graphql/context/features/patCache";
|
||||||
|
import { listResources, Resource } from "@/graphql/features/Resource/resourceService";
|
||||||
|
import { getUserRoles, Role } from "@/graphql/features/Role/roleService";
|
||||||
|
|
||||||
const initGuard: GuardFunction = auth => {
|
// Fonction pour décoder un JWT (basique, sans vérification de signature)
|
||||||
|
function decodeJWT(token: string): { sub?: string; [key: string]: unknown } | null {
|
||||||
|
try {
|
||||||
|
const parts = token.split(".");
|
||||||
|
if (parts.length !== 3) return null;
|
||||||
|
|
||||||
|
const payload = parts[1];
|
||||||
|
const decoded = Buffer.from(payload, "base64url").toString("utf8");
|
||||||
|
return JSON.parse(decoded) as { sub?: string; [key: string]: unknown };
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("Impossible de décoder le JWT:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const setAuth = async (
|
||||||
|
req:
|
||||||
|
| Request
|
||||||
|
| {
|
||||||
|
headers?: Headers | Record<string, string>;
|
||||||
|
get?: (header: string) => string | undefined;
|
||||||
|
header?: (header: string) => string | undefined;
|
||||||
|
}
|
||||||
|
): Promise<AuthTypes & { roles?: Role[]; permissions?: string[]; resources?: Resource[] }> => {
|
||||||
|
// Extraire le token d'autorisation des headers
|
||||||
|
let authHeader: string | null = null;
|
||||||
|
|
||||||
|
// GraphQL Yoga utilise la structure Request Web API
|
||||||
|
if (req.headers) {
|
||||||
|
// Si c'est un objet Headers (Web API)
|
||||||
|
if (typeof req.headers.get === "function") {
|
||||||
|
authHeader = (req.headers as Headers).get("Authorization");
|
||||||
|
}
|
||||||
|
// Si c'est un objet record (GraphQL Yoga parfois)
|
||||||
|
else if (typeof req.headers === "object") {
|
||||||
|
authHeader =
|
||||||
|
(req.headers as Record<string, string>)["authorization"] ||
|
||||||
|
(req.headers as Record<string, string>)["Authorization"];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Essayer d'accéder aux headers via une autre méthode
|
||||||
|
try {
|
||||||
|
// Tenter d'utiliser req comme un objet Request ou custom
|
||||||
|
if (typeof (req as { get?: (header: string) => string | undefined }).get === "function") {
|
||||||
|
const val = (req as { get: (header: string) => string | undefined }).get("Authorization");
|
||||||
|
authHeader = val ?? null;
|
||||||
|
} else if (typeof (req as { header?: (header: string) => string | undefined }).header === "function") {
|
||||||
|
const val = (req as { header: (header: string) => string | undefined }).header("Authorization");
|
||||||
|
authHeader = val ?? null;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Ignorer les erreurs d'accès aux headers
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (authHeader && authHeader.startsWith("Bearer ")) {
|
||||||
|
const token = authHeader.substring(7); // Retirer "Bearer "
|
||||||
|
|
||||||
|
// Décoder le JWT pour récupérer l'userId (seulement si c'est un JWT)
|
||||||
|
let userId: string | undefined = undefined;
|
||||||
|
let user: { id: string; name?: string; email?: string; lastName?: string; isActive?: boolean } | undefined =
|
||||||
|
undefined;
|
||||||
|
|
||||||
|
if (token.includes(".") && token.split(".").length === 3) {
|
||||||
|
// C'est un JWT
|
||||||
|
const decoded = decodeJWT(token);
|
||||||
|
userId = decoded?.sub as string | undefined;
|
||||||
|
if (userId) {
|
||||||
|
user = {
|
||||||
|
id: userId,
|
||||||
|
name: typeof decoded?.name === "string" ? decoded.name : undefined,
|
||||||
|
email: typeof decoded?.email === "string" ? decoded.email : undefined,
|
||||||
|
lastName: typeof decoded?.family_name === "string" ? decoded.family_name : undefined,
|
||||||
|
isActive: typeof decoded?.isActive === "boolean" ? decoded.isActive : undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// PAT : tenter de récupérer l'userId via le cache
|
||||||
|
userId = patContextCache.getUserId(token) || undefined;
|
||||||
|
if (userId) {
|
||||||
|
user = { id: userId };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let roles: Role[] = [];
|
||||||
|
let permissions: string[] = [];
|
||||||
|
let resources: Resource[] = [];
|
||||||
|
if (userId) {
|
||||||
|
try {
|
||||||
|
const minimalContext = {
|
||||||
|
accessToken: token,
|
||||||
|
request: req as Request,
|
||||||
|
db: {},
|
||||||
|
patCache: patContextCache,
|
||||||
|
};
|
||||||
|
roles = await getUserRoles(userId, minimalContext);
|
||||||
|
permissions = roles.flatMap(r =>
|
||||||
|
Array.isArray(r.permissions) ? r.permissions.map((p: { name: string }) => p.name) : []
|
||||||
|
);
|
||||||
|
resources = await listResources(minimalContext);
|
||||||
|
} catch {
|
||||||
|
// ignore fetch errors, fallback to empty
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
accessToken: token,
|
||||||
|
personalAccessToken: token,
|
||||||
|
userId: userId,
|
||||||
|
user: user,
|
||||||
|
roles,
|
||||||
|
permissions,
|
||||||
|
resources,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pas d'authentification
|
||||||
return {
|
return {
|
||||||
isAuthenticated: !!auth?.accessToken,
|
accessToken: undefined,
|
||||||
|
personalAccessToken: undefined,
|
||||||
|
userId: undefined,
|
||||||
|
user: undefined,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export default initGuard;
|
export default setAuth;
|
||||||
|
|||||||
@ -1,105 +1,18 @@
|
|||||||
import { AuthTypes } from "@/graphql/context";
|
import { GuardFunction } from "@/graphql/context/types";
|
||||||
import { patContextCache } from "@/graphql/context/features/patCache";
|
|
||||||
|
|
||||||
// Fonction pour décoder un JWT (basique, sans vérification de signature)
|
const initGuard: GuardFunction = auth => {
|
||||||
function decodeJWT(token: string): { sub?: string; [key: string]: unknown } | null {
|
|
||||||
try {
|
|
||||||
const parts = token.split(".");
|
|
||||||
if (parts.length !== 3) return null;
|
|
||||||
|
|
||||||
const payload = parts[1];
|
|
||||||
const decoded = Buffer.from(payload, "base64url").toString("utf8");
|
|
||||||
return JSON.parse(decoded) as { sub?: string; [key: string]: unknown };
|
|
||||||
} catch (error) {
|
|
||||||
console.warn("Impossible de décoder le JWT:", error);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const setAuth = async (
|
|
||||||
req:
|
|
||||||
| Request
|
|
||||||
| {
|
|
||||||
headers?: Headers | Record<string, string>;
|
|
||||||
get?: (header: string) => string | undefined;
|
|
||||||
header?: (header: string) => string | undefined;
|
|
||||||
}
|
|
||||||
): Promise<AuthTypes> => {
|
|
||||||
// Extraire le token d'autorisation des headers
|
|
||||||
let authHeader: string | null = null;
|
|
||||||
|
|
||||||
// GraphQL Yoga utilise la structure Request Web API
|
|
||||||
if (req.headers) {
|
|
||||||
// Si c'est un objet Headers (Web API)
|
|
||||||
if (typeof req.headers.get === "function") {
|
|
||||||
authHeader = (req.headers as Headers).get("Authorization");
|
|
||||||
}
|
|
||||||
// Si c'est un objet record (GraphQL Yoga parfois)
|
|
||||||
else if (typeof req.headers === "object") {
|
|
||||||
authHeader =
|
|
||||||
(req.headers as Record<string, string>)["authorization"] ||
|
|
||||||
(req.headers as Record<string, string>)["Authorization"];
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Essayer d'accéder aux headers via une autre méthode
|
|
||||||
try {
|
|
||||||
// Tenter d'utiliser req comme un objet Request ou custom
|
|
||||||
if (typeof (req as { get?: (header: string) => string | undefined }).get === "function") {
|
|
||||||
const val = (req as { get: (header: string) => string | undefined }).get("Authorization");
|
|
||||||
authHeader = val ?? null;
|
|
||||||
} else if (typeof (req as { header?: (header: string) => string | undefined }).header === "function") {
|
|
||||||
const val = (req as { header: (header: string) => string | undefined }).header("Authorization");
|
|
||||||
authHeader = val ?? null;
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// Ignorer les erreurs d'accès aux headers
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (authHeader && authHeader.startsWith("Bearer ")) {
|
|
||||||
const token = authHeader.substring(7); // Retirer "Bearer "
|
|
||||||
|
|
||||||
// Décoder le JWT pour récupérer l'userId (seulement si c'est un JWT)
|
|
||||||
let userId: string | undefined = undefined;
|
|
||||||
let user: { id: string; name?: string; email?: string; lastName?: string; isActive?: boolean } | undefined =
|
|
||||||
undefined;
|
|
||||||
|
|
||||||
if (token.includes(".") && token.split(".").length === 3) {
|
|
||||||
// C'est un JWT
|
|
||||||
const decoded = decodeJWT(token);
|
|
||||||
userId = decoded?.sub as string | undefined;
|
|
||||||
if (userId) {
|
|
||||||
user = {
|
|
||||||
id: userId,
|
|
||||||
name: typeof decoded?.name === "string" ? decoded.name : undefined,
|
|
||||||
email: typeof decoded?.email === "string" ? decoded.email : undefined,
|
|
||||||
lastName: typeof decoded?.family_name === "string" ? decoded.family_name : undefined,
|
|
||||||
isActive: typeof decoded?.isActive === "boolean" ? decoded.isActive : undefined,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// PAT : tenter de récupérer l'userId via le cache
|
|
||||||
userId = patContextCache.getUserId(token) || undefined;
|
|
||||||
if (userId) {
|
|
||||||
user = { id: userId };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
accessToken: token,
|
|
||||||
personalAccessToken: token,
|
|
||||||
userId: userId,
|
|
||||||
user: user, // <-- Correction ici
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pas d'authentification
|
|
||||||
return {
|
return {
|
||||||
accessToken: undefined,
|
isAuthenticated: !!auth?.accessToken,
|
||||||
personalAccessToken: undefined,
|
hasRole: (roleName: string) => {
|
||||||
userId: undefined,
|
return Array.isArray(auth?.roles) && auth.roles.some(r => r.name === roleName);
|
||||||
user: undefined,
|
},
|
||||||
|
hasPermission: (permissionName: string) => {
|
||||||
|
return Array.isArray(auth?.permissions) && auth.permissions.includes(permissionName);
|
||||||
|
},
|
||||||
|
hasResource: (resourceName: string) => {
|
||||||
|
return Array.isArray(auth?.resources) && auth.resources.some(r => r.name === resourceName);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export default setAuth;
|
export default initGuard;
|
||||||
|
|||||||
@ -1,3 +1,6 @@
|
|||||||
|
import type { Resource } from "@/graphql/features/Resource/resourceService";
|
||||||
|
import type { Role } from "@/graphql/features/Role/roleService";
|
||||||
|
|
||||||
export interface DB {
|
export interface DB {
|
||||||
// Define your database types here
|
// Define your database types here
|
||||||
}
|
}
|
||||||
@ -19,6 +22,9 @@ export interface AuthTypes {
|
|||||||
accessToken?: string;
|
accessToken?: string;
|
||||||
personalAccessToken?: string; // Token d'accès personnel Logto
|
personalAccessToken?: string; // Token d'accès personnel Logto
|
||||||
userId?: string; // ID utilisateur pour les requêtes authentifiées
|
userId?: string; // ID utilisateur pour les requêtes authentifiées
|
||||||
|
roles?: Role[];
|
||||||
|
permissions?: string[];
|
||||||
|
resources?: Resource[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PATCache {
|
export interface PATCache {
|
||||||
|
|||||||
@ -42,10 +42,10 @@ export const resourceResolvers = {
|
|||||||
},
|
},
|
||||||
assignScopeToResource: async (
|
assignScopeToResource: async (
|
||||||
_parent: unknown,
|
_parent: unknown,
|
||||||
{ resourceId, scope }: { resourceId: string; scope: string },
|
{ resourceId, name, description }: { resourceId: string; name: string; description?: string },
|
||||||
context: GraphQLContext
|
context: GraphQLContext
|
||||||
) => {
|
) => {
|
||||||
return assignScopeToResource(resourceId, scope, context);
|
return assignScopeToResource(resourceId, name, description ?? null, context);
|
||||||
},
|
},
|
||||||
removeScopeFromResource: async (
|
removeScopeFromResource: async (
|
||||||
_parent: unknown,
|
_parent: unknown,
|
||||||
|
|||||||
@ -101,3 +101,5 @@ export async function deleteResource(id: string, context: GraphQLContext): Promi
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type { Resource };
|
||||||
|
|||||||
@ -27,7 +27,7 @@ export async function createResourceScope(
|
|||||||
name: string,
|
name: string,
|
||||||
description: string | null,
|
description: string | null,
|
||||||
context: GraphQLContext
|
context: GraphQLContext
|
||||||
): Promise<any> {
|
): Promise<ResourceScope> {
|
||||||
const token = getAccessTokenFromContext(context) || (await getLogtoAccessToken());
|
const token = getAccessTokenFromContext(context) || (await getLogtoAccessToken());
|
||||||
try {
|
try {
|
||||||
// LOG AVANT REQUETE
|
// LOG AVANT REQUETE
|
||||||
@ -40,8 +40,13 @@ export async function createResourceScope(
|
|||||||
// LOG APRES REQUETE
|
// LOG APRES REQUETE
|
||||||
console.error("[DEBUG createResourceScope] Logto response:", response.data);
|
console.error("[DEBUG createResourceScope] Logto response:", response.data);
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (e: any) {
|
} catch (e: unknown) {
|
||||||
console.error("[ERROR createResourceScope]", e?.response?.data || e);
|
if (typeof e === "object" && e !== null && "response" in e) {
|
||||||
|
// @ts-expect-error: response might exist on error object
|
||||||
|
console.error("[ERROR createResourceScope]", e.response?.data || e);
|
||||||
|
} else {
|
||||||
|
console.error("[ERROR createResourceScope]", e);
|
||||||
|
}
|
||||||
throw new GraphQLError("Erreur lors de la création de la permission de ressource", {
|
throw new GraphQLError("Erreur lors de la création de la permission de ressource", {
|
||||||
extensions: { code: "CREATE_RESOURCE_SCOPE_FAILED" },
|
extensions: { code: "CREATE_RESOURCE_SCOPE_FAILED" },
|
||||||
});
|
});
|
||||||
|
|||||||
@ -3,6 +3,17 @@ import { GraphQLError } from "graphql";
|
|||||||
import { GraphQLContext } from "../../context/types";
|
import { GraphQLContext } from "../../context/types";
|
||||||
import { getLogtoAccessToken } from "../User/resolver";
|
import { getLogtoAccessToken } from "../User/resolver";
|
||||||
|
|
||||||
|
interface AxiosErrorResponse {
|
||||||
|
response?: {
|
||||||
|
data?: {
|
||||||
|
code?: string;
|
||||||
|
[key: string]: unknown;
|
||||||
|
};
|
||||||
|
[key: string]: unknown;
|
||||||
|
};
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
function getAccessTokenFromContext(context: GraphQLContext): string | null {
|
function getAccessTokenFromContext(context: GraphQLContext): string | null {
|
||||||
return context.accessToken || null;
|
return context.accessToken || null;
|
||||||
}
|
}
|
||||||
@ -14,7 +25,7 @@ export async function listPermissions(context: GraphQLContext) {
|
|||||||
headers: { Authorization: `Bearer ${token}` },
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
});
|
});
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (e) {
|
} catch {
|
||||||
throw new GraphQLError("Erreur lors de la récupération des permissions", {
|
throw new GraphQLError("Erreur lors de la récupération des permissions", {
|
||||||
extensions: { code: "PERMISSIONS_FETCH_FAILED" },
|
extensions: { code: "PERMISSIONS_FETCH_FAILED" },
|
||||||
});
|
});
|
||||||
@ -30,7 +41,7 @@ export async function createPermission(name: string, description: string | null,
|
|||||||
{ headers: { Authorization: `Bearer ${token}` } }
|
{ headers: { Authorization: `Bearer ${token}` } }
|
||||||
);
|
);
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (e) {
|
} catch {
|
||||||
throw new GraphQLError("Erreur lors de la création de la permission", {
|
throw new GraphQLError("Erreur lors de la création de la permission", {
|
||||||
extensions: { code: "CREATE_PERMISSION_FAILED" },
|
extensions: { code: "CREATE_PERMISSION_FAILED" },
|
||||||
});
|
});
|
||||||
@ -44,7 +55,7 @@ export async function deletePermission(id: string, context: GraphQLContext) {
|
|||||||
headers: { Authorization: `Bearer ${token}` },
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
});
|
});
|
||||||
return true;
|
return true;
|
||||||
} catch (e) {
|
} catch {
|
||||||
throw new GraphQLError("Erreur lors de la suppression de la permission", {
|
throw new GraphQLError("Erreur lors de la suppression de la permission", {
|
||||||
extensions: { code: "DELETE_PERMISSION_FAILED" },
|
extensions: { code: "DELETE_PERMISSION_FAILED" },
|
||||||
});
|
});
|
||||||
@ -64,12 +75,23 @@ export async function assignPermissionToRole(roleId: string, permissionId: strin
|
|||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.log("[DEBUG assignPermissionToRole] Logto response:", res.data);
|
console.log("[DEBUG assignPermissionToRole] Logto response:", res.data);
|
||||||
return true;
|
return true;
|
||||||
} catch (e: any) {
|
} catch (e: unknown) {
|
||||||
// Si l'erreur est que la permission existe déjà, on considère comme succès
|
const error = e as AxiosErrorResponse;
|
||||||
if (e?.response?.data?.code === "role.scope_exists") {
|
if (
|
||||||
|
typeof e === "object" &&
|
||||||
|
e !== null &&
|
||||||
|
"response" in error &&
|
||||||
|
typeof error.response === "object" &&
|
||||||
|
error.response !== null &&
|
||||||
|
"data" in error.response &&
|
||||||
|
typeof error.response.data === "object" &&
|
||||||
|
error.response.data !== null &&
|
||||||
|
"code" in error.response.data &&
|
||||||
|
error.response.data.code === "role.scope_exists"
|
||||||
|
) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
console.error("[ERROR assignPermissionToRole]", e?.response?.data || e);
|
console.error("[ERROR assignPermissionToRole]", error?.response?.data || e);
|
||||||
throw new GraphQLError("Erreur lors de l'assignation du scope au rôle", {
|
throw new GraphQLError("Erreur lors de l'assignation du scope au rôle", {
|
||||||
extensions: { code: "ASSIGN_SCOPE_TO_ROLE_FAILED" },
|
extensions: { code: "ASSIGN_SCOPE_TO_ROLE_FAILED" },
|
||||||
});
|
});
|
||||||
|
|||||||
@ -18,7 +18,7 @@ async function fetchRolePermissions(
|
|||||||
});
|
});
|
||||||
// Les scopes sont des permissions
|
// Les scopes sont des permissions
|
||||||
return Array.isArray(response.data)
|
return Array.isArray(response.data)
|
||||||
? response.data.map((scope: any) => ({
|
? response.data.map((scope: { id: string; name: string; description?: string }) => ({
|
||||||
id: scope.id,
|
id: scope.id,
|
||||||
name: scope.name,
|
name: scope.name,
|
||||||
description: scope.description,
|
description: scope.description,
|
||||||
@ -79,18 +79,25 @@ export async function assignRoleToUser(userId: string, roleId: string, context:
|
|||||||
{ headers: { Authorization: `Bearer ${token}` } }
|
{ headers: { Authorization: `Bearer ${token}` } }
|
||||||
);
|
);
|
||||||
return true;
|
return true;
|
||||||
} catch (err: any) {
|
} catch (err: unknown) {
|
||||||
// Log détaillé côté serveur
|
// Log détaillé côté serveur
|
||||||
console.error("Erreur assignRoleToUser:", err?.response?.data || err);
|
if (axios.isAxiosError(err)) {
|
||||||
let message = "Erreur lors de l'assignation du rôle";
|
console.error("Erreur assignRoleToUser:", err.response?.data || err);
|
||||||
if (err?.response?.data?.message) {
|
let message = "Erreur lors de l'assignation du rôle";
|
||||||
message += `: ${err.response.data.message}`;
|
if (err.response?.data?.message) {
|
||||||
} else if (err?.message) {
|
message += `: ${err.response.data.message}`;
|
||||||
message += `: ${err.message}`;
|
} else if (err.message) {
|
||||||
|
message += `: ${err.message}`;
|
||||||
|
}
|
||||||
|
throw new GraphQLError(message, {
|
||||||
|
extensions: { code: "ASSIGN_ROLE_FAILED", logto: err.response?.data },
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.error("Erreur assignRoleToUser:", err);
|
||||||
|
throw new GraphQLError("Erreur lors de l'assignation du rôle", {
|
||||||
|
extensions: { code: "ASSIGN_ROLE_FAILED" },
|
||||||
|
});
|
||||||
}
|
}
|
||||||
throw new GraphQLError(message, {
|
|
||||||
extensions: { code: "ASSIGN_ROLE_FAILED", logto: err?.response?.data },
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -166,3 +173,5 @@ export async function deleteRole(id: string, context: GraphQLContext) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type { Role };
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user