From 62f0b9f56e4ee08633e92160fc79d2b62cca63f2 Mon Sep 17 00:00:00 2001 From: Benjamin Levesque <14175665+benjlevesque@users.noreply.github.com> Date: Fri, 18 Oct 2024 22:21:18 +0200 Subject: [PATCH 01/16] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Remove=20legacy=20au?= =?UTF-8?q?thentication=20flow?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../legacy/src/config/authN.config.ts | 16 +- .../legacy/src/controllers/index.ts | 1 - .../legacy/src/controllers/signout.ts | 10 -- .../keycloak/attachUserToRequestMiddleware.ts | 32 ++-- .../src/infra/keycloak/makeKeycloakAuth.ts | 167 +++++------------- .../src/modules/authN/queries/RegisterAuth.ts | 2 +- packages/applications/legacy/src/routes.ts | 5 +- packages/applications/legacy/src/server.ts | 2 +- .../legacy/src/types/express-custom.d.ts | 4 +- .../src/app/api/auth/[...nextauth]/route.ts | 27 +-- .../app/api/auth/federated-logout/route.ts | 40 +++++ .../ssr/src/app/auth/signIn/page.tsx | 26 +-- .../ssr/src/app/auth/signOut/page.tsx | 12 +- packages/applications/ssr/src/auth.ts | 42 +++++ .../molecules/UserHeaderQuickAccessItem.tsx | 2 +- .../applications/ssr/src/types/next-auth.d.ts | 7 + 16 files changed, 168 insertions(+), 227 deletions(-) delete mode 100644 packages/applications/legacy/src/controllers/signout.ts create mode 100644 packages/applications/ssr/src/app/api/auth/federated-logout/route.ts create mode 100644 packages/applications/ssr/src/auth.ts diff --git a/packages/applications/legacy/src/config/authN.config.ts b/packages/applications/legacy/src/config/authN.config.ts index 954e5c6703..9dba2021a5 100644 --- a/packages/applications/legacy/src/config/authN.config.ts +++ b/packages/applications/legacy/src/config/authN.config.ts @@ -1,25 +1,11 @@ import { makeKeycloakAuth } from '../infra/keycloak'; -import { sequelizeInstance } from '../sequelize.config'; import { getUserByEmail } from './queries.config'; import { createUser } from './useCases.config'; const getKeycloakAuth = () => { - const { - KEYCLOAK_SERVER, - KEYCLOAK_REALM, - KEYCLOAK_USER_CLIENT_ID, - KEYCLOAK_USER_CLIENT_SECRET, - NEXT_AUTH_SESSION_TOKEN_COOKIE_NAME, - } = process.env; - - console.log(`Authentication through Keycloak server ${KEYCLOAK_SERVER}`); + const { NEXT_AUTH_SESSION_TOKEN_COOKIE_NAME } = process.env; return makeKeycloakAuth({ - sequelizeInstance, - KEYCLOAK_SERVER, - KEYCLOAK_REALM, - KEYCLOAK_USER_CLIENT_ID, - KEYCLOAK_USER_CLIENT_SECRET, NEXT_AUTH_SESSION_TOKEN_COOKIE_NAME, getUserByEmail, createUser, diff --git a/packages/applications/legacy/src/controllers/index.ts b/packages/applications/legacy/src/controllers/index.ts index 75d5e9f9c3..7bc2d54d4f 100644 --- a/packages/applications/legacy/src/controllers/index.ts +++ b/packages/applications/legacy/src/controllers/index.ts @@ -8,6 +8,5 @@ export * from './tableauxDeBord'; export * from './userAccount'; export * from './getDéclarationAccessibilitéPage'; export * from './getSuccèsPage'; -export * from './signout'; export * from './upload'; export * from './v1Router'; diff --git a/packages/applications/legacy/src/controllers/signout.ts b/packages/applications/legacy/src/controllers/signout.ts deleted file mode 100644 index a7723ff01b..0000000000 --- a/packages/applications/legacy/src/controllers/signout.ts +++ /dev/null @@ -1,10 +0,0 @@ -import asyncHandler from './helpers/asyncHandler'; -import routes from '../routes'; -import { v1Router } from './v1Router'; - -v1Router.get( - routes.LOGOUT_ACTION, - asyncHandler(async (request, response) => { - response.redirect('/auth/signOut'); - }), -); diff --git a/packages/applications/legacy/src/infra/keycloak/attachUserToRequestMiddleware.ts b/packages/applications/legacy/src/infra/keycloak/attachUserToRequestMiddleware.ts index ae0ee85559..d979eccf24 100644 --- a/packages/applications/legacy/src/infra/keycloak/attachUserToRequestMiddleware.ts +++ b/packages/applications/legacy/src/infra/keycloak/attachUserToRequestMiddleware.ts @@ -2,6 +2,7 @@ import { NextFunction, Request, Response } from 'express'; import { logger, ok } from '../../core/utils'; import { CreateUser, GetUserByEmail, USER_ROLES } from '../../modules/users'; import { getPermissions } from '../../modules/authN'; +import { Utilisateur } from '@potentiel-domain/utilisateur'; type AttachUserToRequestMiddlewareDependencies = { getUserByEmail: GetUserByEmail; @@ -23,30 +24,23 @@ const makeAttachUserToRequestMiddleware = return; } - const token = request.kauth?.grant?.access_token; + const accessToken = request?.token?.accessToken as string; - const userEmail = token?.content?.email; - const kRole = USER_ROLES.find((role) => token?.hasRealmRole(role)); - - if (userEmail) { - await getUserByEmail(userEmail) + if (accessToken) { + const { + identifiantUtilisateur: { email }, + role: { nom: role }, + nom: fullName, + } = Utilisateur.convertirEnValueType(accessToken); + await getUserByEmail(email) .andThen((user) => { if (user) { - return ok({ - ...user, - role: kRole!, - }); + return ok({ ...user, role }); } + const createUserArgs = { email, role, fullName }; - const fullName = token?.content?.name; - const createUserArgs = { email: userEmail, role: kRole, fullName }; - - return createUser(createUserArgs).andThen(({ id, role }) => { - if (!kRole) { - request.session.destroy(() => {}); - } - - return ok({ ...createUserArgs, id, role }); + return createUser(createUserArgs).andThen(({ id }) => { + return ok({ ...createUserArgs, id }); }); }) .match( diff --git a/packages/applications/legacy/src/infra/keycloak/makeKeycloakAuth.ts b/packages/applications/legacy/src/infra/keycloak/makeKeycloakAuth.ts index 1f1dfc7862..1e7e83fd1c 100644 --- a/packages/applications/legacy/src/infra/keycloak/makeKeycloakAuth.ts +++ b/packages/applications/legacy/src/infra/keycloak/makeKeycloakAuth.ts @@ -1,106 +1,74 @@ -import makeSequelizeStore from 'connect-session-sequelize'; -import session from 'express-session'; -import Keycloak from 'keycloak-connect'; import QueryString from 'querystring'; -import { User } from '../../entities'; import { EnsureRole, RegisterAuth } from '../../modules/authN'; import { CreateUser, GetUserByEmail } from '../../modules/users'; import routes from '../../routes'; import { makeAttachUserToRequestMiddleware } from './attachUserToRequestMiddleware'; import { miseAJourStatistiquesUtilisation } from '../../controllers/helpers'; -import { isLocalEnv } from '../../config'; import { getLogger } from '@potentiel-libraries/monitoring'; import { Routes } from '@potentiel-applications/routes'; +import { RequestHandler } from 'express'; +import { decode } from 'next-auth/jwt'; export interface KeycloakAuthDeps { - sequelizeInstance: any; - KEYCLOAK_SERVER: string | undefined; - KEYCLOAK_REALM: string | undefined; - KEYCLOAK_USER_CLIENT_ID: string | undefined; - KEYCLOAK_USER_CLIENT_SECRET: string | undefined; NEXT_AUTH_SESSION_TOKEN_COOKIE_NAME: string | undefined; getUserByEmail: GetUserByEmail; createUser: CreateUser; } export const makeKeycloakAuth = (deps: KeycloakAuthDeps) => { - const { - sequelizeInstance, - KEYCLOAK_SERVER, - KEYCLOAK_REALM, - KEYCLOAK_USER_CLIENT_ID, - KEYCLOAK_USER_CLIENT_SECRET, - NEXT_AUTH_SESSION_TOKEN_COOKIE_NAME, - getUserByEmail, - createUser, - } = deps; - - if ( - !KEYCLOAK_SERVER || - !KEYCLOAK_REALM || - !KEYCLOAK_USER_CLIENT_ID || - !KEYCLOAK_USER_CLIENT_SECRET - ) { - console.error('Missing KEYCLOAK env vars'); - process.exit(1); - } + const { NEXT_AUTH_SESSION_TOKEN_COOKIE_NAME, getUserByEmail, createUser } = deps; if (!NEXT_AUTH_SESSION_TOKEN_COOKIE_NAME) { console.error('Missing NEXT_AUTH_SESSION_TOKEN_COOKIE_NAME env var'); process.exit(1); } - const SequelizeStore = makeSequelizeStore(session.Store); - const store = new SequelizeStore({ - db: sequelizeInstance, - tableName: 'sessions', - checkExpirationInterval: 15 * 60 * 1000, // 15 minutes - expiration: 24 * 60 * 60 * 1000, // 1 day - }); + const loadToken: RequestHandler = async (req, _, next) => { + const cookie = req.cookies[NEXT_AUTH_SESSION_TOKEN_COOKIE_NAME]; + if (cookie) { + const token = await decode({ + token: cookie, + secret: process.env.NEXTAUTH_SECRET ?? '', + }); + if (token) { + req.token = token; + } else { + console.log('could not decode token from cookie'); + } + } + next(); + }; - const keycloak = new Keycloak( - { - store, - }, - { - 'confidential-port': 0, - 'auth-server-url': KEYCLOAK_SERVER, - resource: KEYCLOAK_USER_CLIENT_ID, - 'ssl-required': 'external', - 'bearer-only': false, - realm: KEYCLOAK_REALM, - // @ts-ignore - credentials: { - secret: KEYCLOAK_USER_CLIENT_SECRET, - }, - }, - ); + const protectRoute: RequestHandler = async (req, res, next) => { + if (req.user) { + return next(); + } + const params = new URLSearchParams({ callbackUrl: req.path }); + return res.redirect(`auth/signIn?${params}`); + }; const ensureRole: EnsureRole = (roles) => { const roleList = Array.isArray(roles) ? roles : [roles]; - return keycloak.protect((token) => { - return roleList.some((role) => token.hasRealmRole(role)); - }); + const logger = getLogger('KeycloakAuthLegacy'); + return (req, res, next) => + protectRoute(req, res, () => { + if (!req.user) { + logger.warn('no user found'); + res.status(403); + res.end('Access denied'); + } + if (!roleList.includes(req.user.role)) { + logger.warn(`Role missing`, { user: req.user, roleList }); + res.status(403); + res.end('Access denied'); + } + return next(); + }); }; - const registerAuth: RegisterAuth = ({ app, sessionSecret, router }) => { - app.use( - session({ - secret: sessionSecret, - store, - resave: false, - proxy: true, - saveUninitialized: false, - ...(!isLocalEnv && { - cookie: { - secure: true, - }, - }), - }), - ); - - app.use(keycloak.middleware()); + const registerAuth: RegisterAuth = ({ app, router }) => { + app.use(loadToken); app.use( makeAttachUserToRequestMiddleware({ @@ -109,38 +77,7 @@ export const makeKeycloakAuth = (deps: KeycloakAuthDeps) => { }), ); - router.get(routes.LOGIN, keycloak.protect(), (req, res) => { - res.redirect(routes.REDIRECT_BASED_ON_ROLE); - }); - - router.get(routes.REDIRECT_BASED_ON_ROLE, keycloak.protect(), async (req, res) => { - const user = req.user as User; - - if (!user) { - // Sometimes, the user session is not immediately available in the req object - // In that case, wait a bit and redirect to the same url - - // @ts-ignore - if (req.kauth && Object.keys(req.kauth).length) { - getLogger().error(new Error(`Got a valid auth token but no user associated !`), { - token: req.kauth?.grant?.access_token?.content, - }); - res.redirect(routes.LOGOUT_ACTION); - return; - } - - // Use a retry counter to avoid infinite loop - const retryCount = Number(req.query.retry || 0); - if (retryCount > 5) { - // Too many retries - return res.redirect('/'); - } - setTimeout(() => { - res.redirect(`${routes.REDIRECT_BASED_ON_ROLE}?retry=${retryCount + 1}`); - }, 200); - return; - } - + router.get(routes.REDIRECT_BASED_ON_ROLE, protectRoute, async (req, res) => { miseAJourStatistiquesUtilisation({ type: 'connexionUtilisateur', données: { @@ -151,26 +88,12 @@ export const makeKeycloakAuth = (deps: KeycloakAuthDeps) => { }); // @ts-ignore - const queryString = QueryString.stringify(req.query); - - /** - * @todo Code à revoir quand on aura basculé toute l'app sur Next - * - * Le code ci-dessous fait les actiosn suivantes : - * - Si j'ai un cookie qui a le nom de la variable d'env NEXT_AUTH_SESSION_TOKEN_COOKIE_NAME, alors je suis déjà authentifié sur l'app next, - * alors je retourne directement la liste des projets - * - Sinon je m'authentifie déjà sur next, puis je suis redirigé sur la liste des projets - * - */ + const queryString = new QueryString.stringify(req.query); const redirectTo = req.user.role === 'grd' ? Routes.Raccordement.lister : `${routes.LISTE_PROJETS}?${queryString}`; - if (req.cookies[NEXT_AUTH_SESSION_TOKEN_COOKIE_NAME]) { - return res.redirect(redirectTo); - } - - return res.redirect(`auth/signIn?callbackUrl=${redirectTo}`); + return res.redirect(redirectTo); }); }; diff --git a/packages/applications/legacy/src/modules/authN/queries/RegisterAuth.ts b/packages/applications/legacy/src/modules/authN/queries/RegisterAuth.ts index d388e29be6..cad3080cd6 100644 --- a/packages/applications/legacy/src/modules/authN/queries/RegisterAuth.ts +++ b/packages/applications/legacy/src/modules/authN/queries/RegisterAuth.ts @@ -1,5 +1,5 @@ import type { Application, Router } from 'express'; export interface RegisterAuth { - (args: { app: Application; sessionSecret: string; router: Router }): void; + (args: { app: Application; router: Router }): void; } diff --git a/packages/applications/legacy/src/routes.ts b/packages/applications/legacy/src/routes.ts index 637db666dc..9360ea04ac 100644 --- a/packages/applications/legacy/src/routes.ts +++ b/packages/applications/legacy/src/routes.ts @@ -25,13 +25,12 @@ export { withParams }; class routes { static HOME = '/'; - static LOGIN = '/login.html'; - static LOGIN_ACTION = '/login.html'; + static LOGIN = '/auth/signIn'; static STATS = '/stats.html'; static ABONNEMENT_LETTRE_INFORMATION = '/abonnement-lettre-information.html'; static POST_SINSCRIRE_LETTRE_INFORMATION = '/s-inscrire-a-la-lettre-d-information'; static DECLARATION_ACCESSIBILITE = '/accessibilite.html'; - static LOGOUT_ACTION = '/signout'; + static LOGOUT_ACTION = '/api/auth/federated-logout'; static SIGNUP = '/signup.html'; static POST_SIGNUP = '/signup'; diff --git a/packages/applications/legacy/src/server.ts b/packages/applications/legacy/src/server.ts index 5451b4443a..c2687dc028 100644 --- a/packages/applications/legacy/src/server.ts +++ b/packages/applications/legacy/src/server.ts @@ -107,7 +107,7 @@ export async function makeServer(port: number, sessionSecret: string) { app.use(express.json({ limit: FILE_SIZE_LIMIT_IN_MB + 'mb' })); - registerAuth({ app, sessionSecret, router: v1Router }); + registerAuth({ app, router: v1Router }); app.use(v1Router); app.use(express.static(path.join(__dirname, 'public'))); diff --git a/packages/applications/legacy/src/types/express-custom.d.ts b/packages/applications/legacy/src/types/express-custom.d.ts index efb4e7d9e8..795574ec42 100644 --- a/packages/applications/legacy/src/types/express-custom.d.ts +++ b/packages/applications/legacy/src/types/express-custom.d.ts @@ -1,10 +1,10 @@ import { UtilisateurReadModel } from '../modules/utilisateur/récupérer/UtilisateurReadModel'; - +import { JWT } from 'next-auth/jwt'; declare module 'express-serve-static-core' { // eslint-disable-next-line interface Request { user: UtilisateurReadModel; - kauth: any; + token: JWT; } } diff --git a/packages/applications/ssr/src/app/api/auth/[...nextauth]/route.ts b/packages/applications/ssr/src/app/api/auth/[...nextauth]/route.ts index 95292344d4..d6726d1f31 100644 --- a/packages/applications/ssr/src/app/api/auth/[...nextauth]/route.ts +++ b/packages/applications/ssr/src/app/api/auth/[...nextauth]/route.ts @@ -1,30 +1,7 @@ import NextAuth from 'next-auth'; -import KeycloakProvider from 'next-auth/providers/keycloak'; -const FIFTEEN_MINUTES = 15 * 60; -const ONE_DAY = 24 * 60 * 60; +import { authOptions } from '@/auth'; -const handler = NextAuth({ - providers: [ - KeycloakProvider({ - clientId: process.env.KEYCLOAK_USER_CLIENT_ID ?? '', - clientSecret: process.env.KEYCLOAK_USER_CLIENT_SECRET ?? '', - issuer: `${process.env.KEYCLOAK_SERVER}/realms/${process.env.KEYCLOAK_REALM}`, - }), - ], - session: { - strategy: 'jwt', - maxAge: ONE_DAY, - updateAge: FIFTEEN_MINUTES, - }, - callbacks: { - jwt({ token, account }) { - if (account?.access_token) { - token.accessToken = account.access_token; - } - return token; - }, - }, -}); +const handler = NextAuth(authOptions); export { handler as GET, handler as POST }; diff --git a/packages/applications/ssr/src/app/api/auth/federated-logout/route.ts b/packages/applications/ssr/src/app/api/auth/federated-logout/route.ts new file mode 100644 index 0000000000..7769584fa9 --- /dev/null +++ b/packages/applications/ssr/src/app/api/auth/federated-logout/route.ts @@ -0,0 +1,40 @@ +import { NextResponse } from 'next/server'; +import { getServerSession } from 'next-auth'; + +import { authOptions, issuerUrl } from '@/auth'; + +/** + * This route manages logout from the SSO (keycloak). + * Without this, logging out of the app only removes session, but the user is still logged to SSO + * @see https://github.com/nextauthjs/next-auth/discussions/3938 + */ +export async function GET() { + const { NEXTAUTH_URL = '' } = process.env; + + // Gets the session, with idToken + const session = await getServerSession({ + ...authOptions, + callbacks: { + ...authOptions.callbacks, + session({ session, token }) { + session.idToken = token.idToken; + return session; + }, + }, + }); + if (!session) { + return NextResponse.redirect(NEXTAUTH_URL); + } + + // after keycloak logout, redirect the user to this route to remove the session + const redirectUrl = new URL('/auth/signOut', NEXTAUTH_URL); + const ssoLogoutUrl = new URL(`${issuerUrl}/protocol/openid-connect/logout`); + ssoLogoutUrl.searchParams.set('post_logout_redirect_uri', redirectUrl.toString()); + + if (session.idToken) { + // without this, Keycloak prompts the user for confirmation + ssoLogoutUrl.searchParams.set('id_token_hint', session.idToken); + } + + return NextResponse.redirect(ssoLogoutUrl); +} diff --git a/packages/applications/ssr/src/app/auth/signIn/page.tsx b/packages/applications/ssr/src/app/auth/signIn/page.tsx index 400ecfe091..14308c9f94 100644 --- a/packages/applications/ssr/src/app/auth/signIn/page.tsx +++ b/packages/applications/ssr/src/app/auth/signIn/page.tsx @@ -9,23 +9,17 @@ import { PageTemplate } from '@/components/templates/Page.template'; export default function SignIn() { const params = useSearchParams(); const { status } = useSession(); + const callbackUrl = params.get('callbackUrl') ?? '/'; useEffect(() => { - const autoSigning = async () => { - await delay(1500); - - const callbackUrl = params.get('callbackUrl') ?? '/'; - - if (status === 'unauthenticated') { - signIn('keycloak', { callbackUrl }); - } - - if (status === 'authenticated') { - redirect(callbackUrl); - } - }; - autoSigning(); - }, [status, params]); + if (status === 'loading') return; + if (status === 'authenticated') { + redirect(callbackUrl); + } + if (status === 'unauthenticated') { + signIn('keycloak', { callbackUrl }); + } + }, [status, callbackUrl]); return ( @@ -35,5 +29,3 @@ export default function SignIn() { ); } - -const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); diff --git a/packages/applications/ssr/src/app/auth/signOut/page.tsx b/packages/applications/ssr/src/app/auth/signOut/page.tsx index 795b3272e5..5740c2ccf6 100644 --- a/packages/applications/ssr/src/app/auth/signOut/page.tsx +++ b/packages/applications/ssr/src/app/auth/signOut/page.tsx @@ -7,14 +7,8 @@ import { PageTemplate } from '@/components/templates/Page.template'; export default function SignIn() { useEffect(() => { - const autoSignout = async () => { - await delay(1500); - - signOut({ callbackUrl: '/logout' }); - }; - - autoSignout(); - }); + signOut({ callbackUrl: '/' }); + }, []); return ( @@ -24,5 +18,3 @@ export default function SignIn() { ); } - -const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); diff --git a/packages/applications/ssr/src/auth.ts b/packages/applications/ssr/src/auth.ts new file mode 100644 index 0000000000..3b0a2375ac --- /dev/null +++ b/packages/applications/ssr/src/auth.ts @@ -0,0 +1,42 @@ +import { AuthOptions } from 'next-auth'; +import KeycloakProvider from 'next-auth/providers/keycloak'; + +const FIFTEEN_MINUTES = 15 * 60; +const ONE_DAY = 24 * 60 * 60; + +export const issuerUrl = `${process.env.KEYCLOAK_SERVER}/realms/${process.env.KEYCLOAK_REALM}`; + +export const authOptions: AuthOptions = { + providers: [ + KeycloakProvider({ + clientId: process.env.KEYCLOAK_USER_CLIENT_ID ?? '', + clientSecret: process.env.KEYCLOAK_USER_CLIENT_SECRET ?? '', + issuer: issuerUrl, + idToken: true, + profile(profile) { + return { + id: profile.sub, + name: profile.name ?? profile.preferred_username, + email: profile.email, + image: profile.picture, + }; + }, + }), + ], + session: { + strategy: 'jwt', + maxAge: ONE_DAY, + updateAge: FIFTEEN_MINUTES, + }, + callbacks: { + jwt({ token, account }) { + if (account?.access_token) { + token.accessToken = account.access_token; + } + if (account?.id_token) { + token.idToken = account.id_token; + } + return token; + }, + }, +}; diff --git a/packages/applications/ssr/src/components/molecules/UserHeaderQuickAccessItem.tsx b/packages/applications/ssr/src/components/molecules/UserHeaderQuickAccessItem.tsx index 0e540aa6f7..a8b61d3ba0 100644 --- a/packages/applications/ssr/src/components/molecules/UserHeaderQuickAccessItem.tsx +++ b/packages/applications/ssr/src/components/molecules/UserHeaderQuickAccessItem.tsx @@ -41,7 +41,7 @@ export async function UserHeaderQuickAccessItem() { quickAccessItem={{ iconId: 'ri-logout-box-line', linkProps: { - href: '/auth/signOut', + href: '/api/auth/federated-logout', }, text: 'Me déconnecter', }} diff --git a/packages/applications/ssr/src/types/next-auth.d.ts b/packages/applications/ssr/src/types/next-auth.d.ts index ab9fe812aa..5e50bafec5 100644 --- a/packages/applications/ssr/src/types/next-auth.d.ts +++ b/packages/applications/ssr/src/types/next-auth.d.ts @@ -2,6 +2,13 @@ import NextAuthJwt from 'next-auth/jwt'; declare module 'next-auth/jwt' { interface JWT extends NextAuthJwt.JWT { + idToken?: string; accessToken?: string; } } + +declare module 'next-auth' { + interface Session { + idToken?: string; + } +} From 72055c1a1b70d0b18bc3d5cbe26b42f4ad0d7fde Mon Sep 17 00:00:00 2001 From: Benjamin Levesque <14175665+benjlevesque@users.noreply.github.com> Date: Thu, 31 Oct 2024 09:19:03 +0100 Subject: [PATCH 02/16] =?UTF-8?q?=F0=9F=8E=A8=20Compat=20with=20legacy=20a?= =?UTF-8?q?uth=20logout?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ssr/src/app/api/auth/federated-logout/route.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/applications/ssr/src/app/api/auth/federated-logout/route.ts b/packages/applications/ssr/src/app/api/auth/federated-logout/route.ts index 7769584fa9..62e17fdf3a 100644 --- a/packages/applications/ssr/src/app/api/auth/federated-logout/route.ts +++ b/packages/applications/ssr/src/app/api/auth/federated-logout/route.ts @@ -1,5 +1,6 @@ import { NextResponse } from 'next/server'; import { getServerSession } from 'next-auth'; +import { cookies } from 'next/headers'; import { authOptions, issuerUrl } from '@/auth'; @@ -9,7 +10,8 @@ import { authOptions, issuerUrl } from '@/auth'; * @see https://github.com/nextauthjs/next-auth/discussions/3938 */ export async function GET() { - const { NEXTAUTH_URL = '' } = process.env; + const { NEXTAUTH_URL = '', NEXT_AUTH_SESSION_TOKEN_COOKIE_NAME = 'next-auth.session-token' } = + process.env; // Gets the session, with idToken const session = await getServerSession({ @@ -29,10 +31,12 @@ export async function GET() { // after keycloak logout, redirect the user to this route to remove the session const redirectUrl = new URL('/auth/signOut', NEXTAUTH_URL); const ssoLogoutUrl = new URL(`${issuerUrl}/protocol/openid-connect/logout`); - ssoLogoutUrl.searchParams.set('post_logout_redirect_uri', redirectUrl.toString()); + + cookies().delete(NEXT_AUTH_SESSION_TOKEN_COOKIE_NAME); if (session.idToken) { // without this, Keycloak prompts the user for confirmation + ssoLogoutUrl.searchParams.set('post_logout_redirect_uri', redirectUrl.toString()); ssoLogoutUrl.searchParams.set('id_token_hint', session.idToken); } From a64ba5778ac13a8b44a6a88dc011d4d1a75fc163 Mon Sep 17 00:00:00 2001 From: Benjamin Levesque <14175665+benjlevesque@users.noreply.github.com> Date: Wed, 13 Nov 2024 18:09:15 +0100 Subject: [PATCH 03/16] =?UTF-8?q?=E2=9E=96=20Retirer=20les=20d=C3=A9penden?= =?UTF-8?q?ces=20inutiles?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 499 +----------------- packages/applications/legacy/package.json | 4 - .../legacy/src/types/express-custom.d.ts | 6 - 3 files changed, 12 insertions(+), 497 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9fc723681a..a1ba61f62f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8430,11 +8430,6 @@ "node": ">=14" } }, - "node_modules/@testim/chrome-version": { - "version": "1.1.4", - "license": "MIT", - "optional": true - }, "node_modules/@testing-library/dom": { "version": "10.4.0", "dev": true, @@ -8606,11 +8601,6 @@ "@testing-library/dom": ">=7.21.4" } }, - "node_modules/@tootallnate/quickjs-emscripten": { - "version": "0.23.0", - "license": "MIT", - "optional": true - }, "node_modules/@tsconfig/node10": { "version": "1.0.9", "dev": true, @@ -8757,14 +8747,6 @@ "@types/send": "*" } }, - "node_modules/@types/express-session": { - "version": "1.18.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/express": "*" - } - }, "node_modules/@types/graceful-fs": { "version": "4.1.6", "dev": true, @@ -9040,14 +9022,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/yauzl": { - "version": "2.10.3", - "license": "MIT", - "optional": true, - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "7.18.0", "dev": true, @@ -10100,16 +10074,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/asn1.js": { - "version": "5.4.1", - "license": "MIT", - "dependencies": { - "bn.js": "^4.0.0", - "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0", - "safer-buffer": "^2.1.0" - } - }, "node_modules/assert": { "version": "2.1.0", "dev": true, @@ -10140,17 +10104,6 @@ "repeat-string": "^1.6.1" } }, - "node_modules/ast-types": { - "version": "0.13.4", - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.0.1" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/async": { "version": "3.2.4", "license": "MIT" @@ -10620,14 +10573,6 @@ "node": ">= 0.8" } }, - "node_modules/basic-ftp": { - "version": "5.0.5", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=10.0.0" - } - }, "node_modules/better-opn": { "version": "3.0.2", "dev": true, @@ -10675,6 +10620,7 @@ }, "node_modules/bn.js": { "version": "4.12.0", + "dev": true, "license": "MIT" }, "node_modules/body-parser": { @@ -10749,6 +10695,7 @@ }, "node_modules/brorand": { "version": "1.1.0", + "dev": true, "license": "MIT" }, "node_modules/brotli": { @@ -10968,14 +10915,6 @@ "ieee754": "^1.1.4" } }, - "node_modules/buffer-crc32": { - "version": "0.2.13", - "license": "MIT", - "optional": true, - "engines": { - "node": "*" - } - }, "node_modules/buffer-equal-constant-time": { "version": "1.0.1", "license": "BSD-3-Clause" @@ -11247,27 +11186,6 @@ "node": ">=6.0" } }, - "node_modules/chromedriver": { - "version": "127.0.2", - "hasInstallScript": true, - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "@testim/chrome-version": "^1.1.4", - "axios": "^1.6.7", - "compare-versions": "^6.1.0", - "extract-zip": "^2.0.1", - "proxy-agent": "^6.4.0", - "proxy-from-env": "^1.1.0", - "tcp-port-used": "^1.0.2" - }, - "bin": { - "chromedriver": "bin/chromedriver" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/ci-info": { "version": "3.8.0", "dev": true, @@ -11583,11 +11501,6 @@ "dev": true, "license": "MIT" }, - "node_modules/compare-versions": { - "version": "6.1.1", - "license": "MIT", - "optional": true - }, "node_modules/concat-map": { "version": "0.0.1", "license": "MIT" @@ -11677,19 +11590,6 @@ "proto-list": "~1.2.1" } }, - "node_modules/connect-session-sequelize": { - "version": "7.1.7", - "license": "MIT", - "dependencies": { - "debug": "^4.1.1" - }, - "engines": { - "node": ">= 10" - }, - "peerDependencies": { - "sequelize": ">= 6.1.0" - } - }, "node_modules/console-browserify": { "version": "1.2.0", "dev": true @@ -12105,14 +12005,6 @@ "type": "^1.0.1" } }, - "node_modules/data-uri-to-buffer": { - "version": "6.0.2", - "license": "MIT", - "optional": true, - "engines": { - "node": ">= 14" - } - }, "node_modules/data-urls": { "version": "5.0.0", "license": "MIT", @@ -12252,7 +12144,7 @@ }, "node_modules/deep-is": { "version": "0.1.4", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/deepmerge": { @@ -12310,19 +12202,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/degenerator": { - "version": "5.0.1", - "license": "MIT", - "optional": true, - "dependencies": { - "ast-types": "^0.13.4", - "escodegen": "^2.1.0", - "esprima": "^4.0.1" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/delayed-stream": { "version": "1.0.0", "license": "MIT", @@ -12757,6 +12636,7 @@ }, "node_modules/elliptic": { "version": "6.5.7", + "dev": true, "license": "MIT", "dependencies": { "bn.js": "^4.11.9", @@ -12802,14 +12682,6 @@ "node": ">= 0.8" } }, - "node_modules/end-of-stream": { - "version": "1.4.4", - "license": "MIT", - "optional": true, - "dependencies": { - "once": "^1.4.0" - } - }, "node_modules/endent": { "version": "2.1.0", "dev": true, @@ -13220,7 +13092,7 @@ }, "node_modules/escodegen": { "version": "2.1.0", - "devOptional": true, + "dev": true, "license": "BSD-2-Clause", "dependencies": { "esprima": "^4.0.1", @@ -13879,7 +13751,7 @@ }, "node_modules/esprima": { "version": "4.0.1", - "devOptional": true, + "dev": true, "license": "BSD-2-Clause", "bin": { "esparse": "bin/esparse.js", @@ -13913,7 +13785,7 @@ }, "node_modules/estraverse": { "version": "5.3.0", - "devOptional": true, + "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=4.0" @@ -13934,7 +13806,7 @@ }, "node_modules/esutils": { "version": "2.0.3", - "devOptional": true, + "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=0.10.0" @@ -14072,56 +13944,6 @@ "version": "1.2.0", "license": "MIT" }, - "node_modules/express-session": { - "version": "1.18.1", - "license": "MIT", - "dependencies": { - "cookie": "0.7.2", - "cookie-signature": "1.0.7", - "debug": "2.6.9", - "depd": "~2.0.0", - "on-headers": "~1.0.2", - "parseurl": "~1.3.3", - "safe-buffer": "5.2.1", - "uid-safe": "~2.1.5" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/express-session/node_modules/cookie-signature": { - "version": "1.0.7", - "license": "MIT" - }, - "node_modules/express-session/node_modules/debug": { - "version": "2.6.9", - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/express-session/node_modules/ms": { - "version": "2.0.0", - "license": "MIT" - }, - "node_modules/express-session/node_modules/safe-buffer": { - "version": "5.2.1", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, "node_modules/express/node_modules/cookie": { "version": "0.7.1", "license": "MIT", @@ -14176,39 +13998,6 @@ "version": "2.7.2", "license": "ISC" }, - "node_modules/extract-zip": { - "version": "2.0.1", - "license": "BSD-2-Clause", - "optional": true, - "dependencies": { - "debug": "^4.1.1", - "get-stream": "^5.1.0", - "yauzl": "^2.10.0" - }, - "bin": { - "extract-zip": "cli.js" - }, - "engines": { - "node": ">= 10.17.0" - }, - "optionalDependencies": { - "@types/yauzl": "^2.9.1" - } - }, - "node_modules/extract-zip/node_modules/get-stream": { - "version": "5.2.0", - "license": "MIT", - "optional": true, - "dependencies": { - "pump": "^3.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/fast-deep-equal": { "version": "3.1.3", "license": "MIT" @@ -14305,14 +14094,6 @@ "bser": "2.1.1" } }, - "node_modules/fd-slicer": { - "version": "1.1.0", - "license": "MIT", - "optional": true, - "dependencies": { - "pend": "~1.2.0" - } - }, "node_modules/fecha": { "version": "4.2.3", "license": "MIT" @@ -14887,33 +14668,6 @@ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, - "node_modules/get-uri": { - "version": "6.0.3", - "license": "MIT", - "optional": true, - "dependencies": { - "basic-ftp": "^5.0.2", - "data-uri-to-buffer": "^6.0.2", - "debug": "^4.3.4", - "fs-extra": "^11.2.0" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/get-uri/node_modules/fs-extra": { - "version": "11.2.0", - "license": "MIT", - "optional": true, - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=14.14" - } - }, "node_modules/git-hooks-list": { "version": "3.1.0", "dev": true, @@ -15197,6 +14951,7 @@ }, "node_modules/hash.js": { "version": "1.1.7", + "dev": true, "license": "MIT", "dependencies": { "inherits": "^2.0.3", @@ -15275,6 +15030,7 @@ }, "node_modules/hmac-drbg": { "version": "1.0.1", + "dev": true, "license": "MIT", "dependencies": { "hash.js": "^1.0.3", @@ -15784,31 +15540,6 @@ "node": ">=6" } }, - "node_modules/ip-address": { - "version": "9.0.5", - "license": "MIT", - "optional": true, - "dependencies": { - "jsbn": "1.1.0", - "sprintf-js": "^1.1.3" - }, - "engines": { - "node": ">= 12" - } - }, - "node_modules/ip-address/node_modules/sprintf-js": { - "version": "1.1.3", - "license": "BSD-3-Clause", - "optional": true - }, - "node_modules/ip-regex": { - "version": "4.3.0", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=8" - } - }, "node_modules/ipaddr.js": { "version": "1.9.1", "license": "MIT", @@ -16333,19 +16064,6 @@ "node": ">=8" } }, - "node_modules/is2": { - "version": "2.0.9", - "license": "MIT", - "optional": true, - "dependencies": { - "deep-is": "^0.1.3", - "ip-regex": "^4.1.0", - "is-url": "^1.2.4" - }, - "engines": { - "node": ">=v0.10.0" - } - }, "node_modules/isarray": { "version": "2.0.5", "dev": true, @@ -17979,11 +17697,6 @@ "js-yaml": "bin/js-yaml.js" } }, - "node_modules/jsbn": { - "version": "1.1.0", - "license": "MIT", - "optional": true - }, "node_modules/jsdoc-type-pratt-parser": { "version": "4.1.0", "dev": true, @@ -18186,15 +17899,6 @@ "safe-buffer": "^5.0.1" } }, - "node_modules/jwk-to-pem": { - "version": "2.0.5", - "license": "Apache-2.0", - "dependencies": { - "asn1.js": "^5.3.0", - "elliptic": "^6.5.4", - "safe-buffer": "^5.0.1" - } - }, "node_modules/jws": { "version": "3.2.2", "license": "MIT", @@ -18203,19 +17907,6 @@ "safe-buffer": "^5.0.1" } }, - "node_modules/keycloak-connect": { - "version": "23.0.7", - "license": "Apache-2.0", - "dependencies": { - "jwk-to-pem": "^2.0.0" - }, - "engines": { - "node": ">=14" - }, - "optionalDependencies": { - "chromedriver": "latest" - } - }, "node_modules/keyv": { "version": "4.5.4", "dev": true, @@ -19095,10 +18786,12 @@ }, "node_modules/minimalistic-assert": { "version": "1.0.1", + "dev": true, "license": "ISC" }, "node_modules/minimalistic-crypto-utils": { "version": "1.0.1", + "dev": true, "license": "MIT" }, "node_modules/minimatch": { @@ -19320,14 +19013,6 @@ "dev": true, "license": "MIT" }, - "node_modules/netmask": { - "version": "2.0.2", - "license": "MIT", - "optional": true, - "engines": { - "node": ">= 0.4.0" - } - }, "node_modules/neverthrow": { "version": "6.2.2", "license": "MIT" @@ -20184,36 +19869,6 @@ "node": ">=6" } }, - "node_modules/pac-proxy-agent": { - "version": "7.0.2", - "license": "MIT", - "optional": true, - "dependencies": { - "@tootallnate/quickjs-emscripten": "^0.23.0", - "agent-base": "^7.0.2", - "debug": "^4.3.4", - "get-uri": "^6.0.1", - "http-proxy-agent": "^7.0.0", - "https-proxy-agent": "^7.0.5", - "pac-resolver": "^7.0.1", - "socks-proxy-agent": "^8.0.4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/pac-resolver": { - "version": "7.0.1", - "license": "MIT", - "optional": true, - "dependencies": { - "degenerator": "^5.0.0", - "netmask": "^2.0.2" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/package-json-from-dist": { "version": "1.0.0", "dev": true, @@ -20465,11 +20120,6 @@ "node": ">=0.12" } }, - "node_modules/pend": { - "version": "1.2.0", - "license": "MIT", - "optional": true - }, "node_modules/pg": { "version": "8.13.0", "license": "MIT", @@ -21274,32 +20924,6 @@ "node": ">= 0.10" } }, - "node_modules/proxy-agent": { - "version": "6.4.0", - "license": "MIT", - "optional": true, - "dependencies": { - "agent-base": "^7.0.2", - "debug": "^4.3.4", - "http-proxy-agent": "^7.0.1", - "https-proxy-agent": "^7.0.3", - "lru-cache": "^7.14.1", - "pac-proxy-agent": "^7.0.1", - "proxy-from-env": "^1.1.0", - "socks-proxy-agent": "^8.0.2" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/proxy-agent/node_modules/lru-cache": { - "version": "7.18.3", - "license": "ISC", - "optional": true, - "engines": { - "node": ">=12" - } - }, "node_modules/proxy-from-env": { "version": "1.1.0", "license": "MIT" @@ -21317,15 +20941,6 @@ "safe-buffer": "^5.1.2" } }, - "node_modules/pump": { - "version": "3.0.0", - "license": "MIT", - "optional": true, - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, "node_modules/punycode": { "version": "2.3.1", "license": "MIT", @@ -21426,13 +21041,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/random-bytes": { - "version": "1.0.0", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/randombytes": { "version": "2.1.0", "dev": true, @@ -22918,15 +22526,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/smart-buffer": { - "version": "4.2.0", - "license": "MIT", - "optional": true, - "engines": { - "node": ">= 6.0.0", - "npm": ">= 3.0.0" - } - }, "node_modules/snake-case": { "version": "3.0.4", "dev": true, @@ -22936,32 +22535,6 @@ "tslib": "^2.0.3" } }, - "node_modules/socks": { - "version": "2.8.3", - "license": "MIT", - "optional": true, - "dependencies": { - "ip-address": "^9.0.5", - "smart-buffer": "^4.2.0" - }, - "engines": { - "node": ">= 10.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/socks-proxy-agent": { - "version": "8.0.4", - "license": "MIT", - "optional": true, - "dependencies": { - "agent-base": "^7.1.1", - "debug": "^4.3.4", - "socks": "^2.8.3" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/sort-object-keys": { "version": "1.1.3", "dev": true, @@ -23662,31 +23235,6 @@ "node": ">=6" } }, - "node_modules/tcp-port-used": { - "version": "1.0.2", - "license": "MIT", - "optional": true, - "dependencies": { - "debug": "4.3.1", - "is2": "^2.0.6" - } - }, - "node_modules/tcp-port-used/node_modules/debug": { - "version": "4.3.1", - "license": "MIT", - "optional": true, - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, "node_modules/telejson": { "version": "7.2.0", "dev": true, @@ -24701,16 +24249,6 @@ } } }, - "node_modules/uid-safe": { - "version": "2.1.5", - "license": "MIT", - "dependencies": { - "random-bytes": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/umzug": { "version": "2.3.0", "license": "MIT", @@ -25690,15 +25228,6 @@ "node": ">=12" } }, - "node_modules/yauzl": { - "version": "2.10.0", - "license": "MIT", - "optional": true, - "dependencies": { - "buffer-crc32": "~0.2.3", - "fd-slicer": "~1.1.0" - } - }, "node_modules/yn": { "version": "3.1.1", "dev": true, @@ -25829,7 +25358,6 @@ "@potentiel-infrastructure/pg-event-sourcing": "*", "@react-pdf/renderer": "^3.4.5", "@sentry/node": "^7.119.2", - "connect-session-sequelize": "^7.1.7", "cookie-parser": "^1.4.7", "csv-parse": "^5.5.6", "date-fns": "^2.30.0", @@ -25838,14 +25366,12 @@ "esbuild": "^0.20.1", "express": "^4.21.1", "express-async-handler": "^1.2.0", - "express-session": "^1.18.1", "helmet": "^6.2.0", "iconv-lite": "^0.6.3", "ioredis": "^4.28.5", "isemail": "^3.2.0", "json2csv": "^5.0.7", "jsonwebtoken": "^9.0.2", - "keycloak-connect": "^23.0.4", "mediateur": "^0.1.3", "mime-types": "^2.1.35", "moment": "^2.30.1", @@ -25875,7 +25401,6 @@ "@fullhuman/postcss-purgecss": "^5.0.0", "@jest/globals": "^29.6.4", "@types/express": "^4.17.17", - "@types/express-session": "^1.18.0", "@types/ioredis": "^4.28.10", "@types/mime-types": "^2.1.4", "@types/morgan": "^1.9.9", diff --git a/packages/applications/legacy/package.json b/packages/applications/legacy/package.json index f2be47acab..c0a3b1eb7b 100644 --- a/packages/applications/legacy/package.json +++ b/packages/applications/legacy/package.json @@ -33,7 +33,6 @@ "@potentiel-infrastructure/pg-event-sourcing": "*", "@react-pdf/renderer": "^3.4.5", "@sentry/node": "^7.119.2", - "connect-session-sequelize": "^7.1.7", "cookie-parser": "^1.4.7", "csv-parse": "^5.5.6", "date-fns": "^2.30.0", @@ -42,14 +41,12 @@ "esbuild": "^0.20.1", "express": "^4.21.1", "express-async-handler": "^1.2.0", - "express-session": "^1.18.1", "helmet": "^6.2.0", "iconv-lite": "^0.6.3", "ioredis": "^4.28.5", "isemail": "^3.2.0", "json2csv": "^5.0.7", "jsonwebtoken": "^9.0.2", - "keycloak-connect": "^23.0.4", "mediateur": "^0.1.3", "mime-types": "^2.1.35", "moment": "^2.30.1", @@ -79,7 +76,6 @@ "@fullhuman/postcss-purgecss": "^5.0.0", "@jest/globals": "^29.6.4", "@types/express": "^4.17.17", - "@types/express-session": "^1.18.0", "@types/ioredis": "^4.28.10", "@types/mime-types": "^2.1.4", "@types/morgan": "^1.9.9", diff --git a/packages/applications/legacy/src/types/express-custom.d.ts b/packages/applications/legacy/src/types/express-custom.d.ts index 795574ec42..775539d278 100644 --- a/packages/applications/legacy/src/types/express-custom.d.ts +++ b/packages/applications/legacy/src/types/express-custom.d.ts @@ -7,9 +7,3 @@ declare module 'express-serve-static-core' { token: JWT; } } - -declare module 'express-session' { - interface SessionData { - apiResults?: Record; - } -} From ff54821c7edb978c144ec418453e0bf47e97364c Mon Sep 17 00:00:00 2001 From: Benjamin Levesque <14175665+benjlevesque@users.noreply.github.com> Date: Wed, 13 Nov 2024 18:22:21 +0100 Subject: [PATCH 04/16] =?UTF-8?q?=F0=9F=94=A5=20Remove=20apiResult?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../getInviterDgecValidateurPage.ts | 9 +--- .../postInviterDgecValidateur.ts | 52 +++++-------------- .../src/controllers/helpers/apiResult.ts | 42 --------------- 3 files changed, 14 insertions(+), 89 deletions(-) delete mode 100644 packages/applications/legacy/src/controllers/helpers/apiResult.ts diff --git a/packages/applications/legacy/src/controllers/admin/inviterDgecValidateur/getInviterDgecValidateurPage.ts b/packages/applications/legacy/src/controllers/admin/inviterDgecValidateur/getInviterDgecValidateurPage.ts index 2fdc1a06de..5f700a57f4 100644 --- a/packages/applications/legacy/src/controllers/admin/inviterDgecValidateur/getInviterDgecValidateurPage.ts +++ b/packages/applications/legacy/src/controllers/admin/inviterDgecValidateur/getInviterDgecValidateurPage.ts @@ -4,22 +4,17 @@ import { v1Router } from '../../v1Router'; import { vérifierPermissionUtilisateur } from '../../helpers'; import { InviterDgecValidateurPage } from '../../../views'; import { PermissionInviterDgecValidateur } from '../../../modules/utilisateur'; -import { getApiResult } from '../../helpers/apiResult'; v1Router.get( routes.ADMIN_INVITATION_DGEC_VALIDATEUR, vérifierPermissionUtilisateur(PermissionInviterDgecValidateur), asyncHandler(async (request, response) => { - const result = getApiResult(request, routes.ADMIN_INVITATION_DGEC_VALIDATEUR_ACTION); + const error = request.query.error; return response.send( InviterDgecValidateurPage({ request, - inviationRéussi: result?.status === 'OK' ? true : undefined, - formErrors: - result?.status === 'BAD_REQUEST' - ? (result.formErrors as Record) - : undefined, + inviationRéussi: !error || undefined, }), ); }), diff --git a/packages/applications/legacy/src/controllers/admin/inviterDgecValidateur/postInviterDgecValidateur.ts b/packages/applications/legacy/src/controllers/admin/inviterDgecValidateur/postInviterDgecValidateur.ts index 0eb9a28ca5..d2ea017f3b 100644 --- a/packages/applications/legacy/src/controllers/admin/inviterDgecValidateur/postInviterDgecValidateur.ts +++ b/packages/applications/legacy/src/controllers/admin/inviterDgecValidateur/postInviterDgecValidateur.ts @@ -7,14 +7,9 @@ import { vérifierPermissionUtilisateur, } from '../../helpers'; import { inviterUtilisateur } from '../../../config'; -import { - InvitationUniqueParUtilisateurError, - InvitationUtilisateurExistantError, - PermissionInviterDgecValidateur, -} from '../../../modules/utilisateur'; +import { PermissionInviterDgecValidateur } from '../../../modules/utilisateur'; import { logger } from '../../../core/utils'; import asyncHandler from '../../helpers/asyncHandler'; -import { setApiResult } from '../../helpers/apiResult'; const schema = yup.object({ role: yup @@ -41,46 +36,23 @@ v1Router.post( ) .match( () => { - setApiResult(request, { - route: routes.ADMIN_INVITATION_DGEC_VALIDATEUR_ACTION, - status: 'OK', - }); return response.redirect(routes.ADMIN_INVITATION_DGEC_VALIDATEUR); }, (error: Error) => { if (error instanceof RequestValidationError) { - setApiResult(request, { - route: routes.ADMIN_INVITATION_DGEC_VALIDATEUR_ACTION, - status: 'BAD_REQUEST', - message: 'Le formulaire contient des erreurs', - formErrors: Object.entries(error.errors).reduce((prev, [key, value]) => { - return { - ...prev, - [key.replace('error-', '')]: value, - }; - }, {}), - }); - return response.redirect(routes.ADMIN_INVITATION_DGEC_VALIDATEUR); - } - if ( - error instanceof InvitationUniqueParUtilisateurError || - error instanceof InvitationUtilisateurExistantError - ) { - setApiResult(request, { - route: routes.ADMIN_INVITATION_DGEC_VALIDATEUR_ACTION, - status: 'BAD_REQUEST', - message: error.message, - }); - return response.redirect(routes.ADMIN_INVITATION_DGEC_VALIDATEUR); + return response.redirect( + routes.ADMIN_INVITATION_DGEC_VALIDATEUR + + '?' + + new URLSearchParams({ error: 'Le formulaire contient des erreurs' }).toString(), + ); } logger.error(error); - setApiResult(request, { - route: routes.ADMIN_INVITATION_DGEC_VALIDATEUR_ACTION, - status: 'BAD_REQUEST', - message: - 'Il y a eu une erreur lors de la soumission de votre demande. Merci de recommencer.', - }); - return response.redirect(routes.ADMIN_INVITATION_DGEC_VALIDATEUR); + + return response.redirect( + routes.ADMIN_INVITATION_DGEC_VALIDATEUR + + `?` + + new URLSearchParams({ error: "Impossible d'inviter l'utilisateur" }).toString(), + ); }, ); }), diff --git a/packages/applications/legacy/src/controllers/helpers/apiResult.ts b/packages/applications/legacy/src/controllers/helpers/apiResult.ts deleted file mode 100644 index fd03caaa20..0000000000 --- a/packages/applications/legacy/src/controllers/helpers/apiResult.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { Request } from 'express'; - -export type FormErrors = Record; - -export type ApiResult = { - route: string; -} & ( - | { - status: 'OK'; - result?: TResult; - } - | { - status: 'BAD_REQUEST'; - message: string; - formErrors?: FormErrors; - } -); - -export const setApiResult = ( - request: Request, - { route, ...result }: ApiResult, -): void => { - request.session.apiResults = { - ...request.session.apiResults, - [route]: result, - }; -}; - -export const getApiResult = ( - request: Request, - route: string, -): ApiResult | undefined => { - const apiResults = request.session.apiResults; - - if (apiResults) { - const { [route]: apiResult, ...clearedApiResults } = apiResults; - - request.session.apiResults = clearedApiResults; - - return apiResult as ApiResult; - } -}; From 1aa23de0fee2ac09e22fc192207fff1acc4862ed Mon Sep 17 00:00:00 2001 From: Benjamin Levesque <14175665+benjlevesque@users.noreply.github.com> Date: Thu, 14 Nov 2024 13:29:31 +0100 Subject: [PATCH 05/16] =?UTF-8?q?=F0=9F=8E=A8=20Rework=20&=20fix?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../legacy/src/config/authN.config.ts | 3 - .../v\303\251rifierPermissionUtilisateur.ts" | 5 +- .../controllers/userAccount/getSignupPage.ts | 3 +- .../keycloak/attachUserToRequestMiddleware.ts | 80 +++++++++++-------- .../infra/keycloak/createUserCredentials.ts | 4 +- .../src/infra/keycloak/makeKeycloakAuth.ts | 53 +----------- .../infra/keycloak/resendInvitationEmail.ts | 4 +- .../src/modules/authN/queries/RegisterAuth.ts | 4 +- packages/applications/legacy/src/routes.ts | 3 - packages/applications/legacy/src/server.ts | 2 +- .../UI/molecules/dropdowns/DropdownMenu.tsx | 6 +- .../views/components/UI/organisms/Header.tsx | 6 +- .../UI/organisms/UserNavigation.tsx | 6 +- .../pages/AbonnementLettreInformation.tsx | 2 +- .../legacy/src/views/pages/Signup.tsx | 3 +- .../legacy/src/views/pages/homePage/Home.tsx | 6 +- .../components/InscriptionConnexion.tsx | 10 +-- .../routes/src/auth/auth.routes.ts | 19 +++++ .../applications/routes/src/auth/index.ts | 1 + packages/applications/routes/src/index.ts | 2 + .../routes/src/projet/projet.routes.ts | 2 + packages/applications/ssr/public/next.svg | 1 - packages/applications/ssr/public/vercel.svg | 1 - .../app/api/auth/federated-logout/route.ts | 14 +--- .../ssr/src/app/auth/signIn/page.tsx | 4 +- .../ssr/src/app/auth/signOut/page.tsx | 2 +- .../ssr/src/app/go-to-user-dashboard/route.ts | 14 ++++ .../molecules/UserHeaderQuickAccessItem.tsx | 1 + packages/applications/ssr/src/middleware.ts | 4 +- .../applications/ssr/src/types/next-auth.d.ts | 1 + .../src/utils/getAuthenticatedUser.handler.ts | 25 +++--- 31 files changed, 146 insertions(+), 145 deletions(-) create mode 100644 packages/applications/routes/src/auth/auth.routes.ts create mode 100644 packages/applications/routes/src/auth/index.ts delete mode 100644 packages/applications/ssr/public/next.svg delete mode 100644 packages/applications/ssr/public/vercel.svg create mode 100644 packages/applications/ssr/src/app/go-to-user-dashboard/route.ts diff --git a/packages/applications/legacy/src/config/authN.config.ts b/packages/applications/legacy/src/config/authN.config.ts index 9dba2021a5..44c4cd2fad 100644 --- a/packages/applications/legacy/src/config/authN.config.ts +++ b/packages/applications/legacy/src/config/authN.config.ts @@ -3,10 +3,7 @@ import { getUserByEmail } from './queries.config'; import { createUser } from './useCases.config'; const getKeycloakAuth = () => { - const { NEXT_AUTH_SESSION_TOKEN_COOKIE_NAME } = process.env; - return makeKeycloakAuth({ - NEXT_AUTH_SESSION_TOKEN_COOKIE_NAME, getUserByEmail, createUser, }); diff --git "a/packages/applications/legacy/src/controllers/helpers/v\303\251rifierPermissionUtilisateur.ts" "b/packages/applications/legacy/src/controllers/helpers/v\303\251rifierPermissionUtilisateur.ts" index 85b5dbdc22..09b79cd146 100644 --- "a/packages/applications/legacy/src/controllers/helpers/v\303\251rifierPermissionUtilisateur.ts" +++ "b/packages/applications/legacy/src/controllers/helpers/v\303\251rifierPermissionUtilisateur.ts" @@ -1,14 +1,15 @@ import { RequestHandler } from 'express'; import { Permission } from '../../modules/authN'; -import routes from '../../routes'; import { AccèsNonAutoriséPage } from '../../views'; +import { Routes } from '@potentiel-applications/routes'; +// TODO export const vérifierPermissionUtilisateur = (permission: Permission): RequestHandler => (request, response, next) => { const { user } = request; if (!user) { - response.redirect(routes.LOGIN); + response.redirect(Routes.Auth.signIn()); return; } diff --git a/packages/applications/legacy/src/controllers/userAccount/getSignupPage.ts b/packages/applications/legacy/src/controllers/userAccount/getSignupPage.ts index aa24467530..8ed333154e 100644 --- a/packages/applications/legacy/src/controllers/userAccount/getSignupPage.ts +++ b/packages/applications/legacy/src/controllers/userAccount/getSignupPage.ts @@ -2,6 +2,7 @@ import asyncHandler from '../helpers/asyncHandler'; import routes from '../../routes'; import { v1Router } from '../v1Router'; import { SignupPage } from '../../views'; +import { Routes } from '@potentiel-applications/routes'; v1Router.get( routes.SIGNUP, @@ -9,7 +10,7 @@ v1Router.get( const { user, query } = request; if (user) { - return response.redirect(routes.REDIRECT_BASED_ON_ROLE); + return response.redirect(Routes.Auth.redirectToDashboard()); } const validationErrors: Array<{ [fieldName: string]: string }> = Object.entries(query).reduce( diff --git a/packages/applications/legacy/src/infra/keycloak/attachUserToRequestMiddleware.ts b/packages/applications/legacy/src/infra/keycloak/attachUserToRequestMiddleware.ts index d979eccf24..dc22255ce3 100644 --- a/packages/applications/legacy/src/infra/keycloak/attachUserToRequestMiddleware.ts +++ b/packages/applications/legacy/src/infra/keycloak/attachUserToRequestMiddleware.ts @@ -1,14 +1,34 @@ import { NextFunction, Request, Response } from 'express'; -import { logger, ok } from '../../core/utils'; +import { ResultAsync } from '../../core/utils'; import { CreateUser, GetUserByEmail, USER_ROLES } from '../../modules/users'; import { getPermissions } from '../../modules/authN'; import { Utilisateur } from '@potentiel-domain/utilisateur'; +import { getToken, GetTokenParams } from 'next-auth/jwt'; +import NextAuthJwt from 'next-auth/jwt'; type AttachUserToRequestMiddlewareDependencies = { getUserByEmail: GetUserByEmail; createUser: CreateUser; }; +// The token is generated by next-auth on the SSR app +declare module 'next-auth/jwt' { + interface JWT extends NextAuthJwt.DefaultJWT { + idToken?: string; + accessToken: string; + } +} + +const getAccessToken = async (req: Request) => { + const token = await getToken({ + req: { cookies: req.cookies } as unknown as GetTokenParams['req'], + }); + return token?.accessToken; +}; + +const promisify = (resultAsync: ResultAsync) => + new Promise((resolve, reject) => resultAsync.match(resolve, reject)); + const makeAttachUserToRequestMiddleware = ({ getUserByEmail, createUser }: AttachUserToRequestMiddlewareDependencies) => async (request: Request, response: Response, next: NextFunction) => { @@ -18,44 +38,38 @@ const makeAttachUserToRequestMiddleware = request.path.startsWith('/css') || request.path.startsWith('/images') || request.path.startsWith('/scripts') || - request.path.startsWith('/main') + request.path.startsWith('/main') || + request.path.startsWith('/illustrations') ) { next(); return; } - const accessToken = request?.token?.accessToken as string; - - if (accessToken) { - const { - identifiantUtilisateur: { email }, - role: { nom: role }, - nom: fullName, - } = Utilisateur.convertirEnValueType(accessToken); - await getUserByEmail(email) - .andThen((user) => { - if (user) { - return ok({ ...user, role }); - } - const createUserArgs = { email, role, fullName }; - - return createUser(createUserArgs).andThen(({ id }) => { - return ok({ ...createUserArgs, id }); - }); - }) - .match( - (user) => { - request.user = { - ...user, - accountUrl: `${process.env.KEYCLOAK_SERVER}/realms/${process.env.KEYCLOAK_REALM}/account`, - permissions: getPermissions(user), - }; - }, - (e: Error) => { - logger.error(e); - }, - ); + const token = await getAccessToken(request); + + if (!token) { + next(); + return; } + const { + identifiantUtilisateur: { email }, + role: { nom: role }, + nom: fullName, + } = Utilisateur.convertirEnValueType(token); + const getOrCreateUser = async () => { + const user = await promisify(getUserByEmail(email)); + if (user) return user; + const createUserArgs = { email, role, fullName }; + + const { id } = await promisify(createUser(createUserArgs)); + return { id, ...createUserArgs }; + }; + const user = await getOrCreateUser(); + request.user = { + ...user, + accountUrl: `${process.env.KEYCLOAK_SERVER}/realms/${process.env.KEYCLOAK_REALM}/account`, + permissions: getPermissions(user), + }; next(); }; diff --git a/packages/applications/legacy/src/infra/keycloak/createUserCredentials.ts b/packages/applications/legacy/src/infra/keycloak/createUserCredentials.ts index 01da6978c8..d0fc499d74 100644 --- a/packages/applications/legacy/src/infra/keycloak/createUserCredentials.ts +++ b/packages/applications/legacy/src/infra/keycloak/createUserCredentials.ts @@ -3,8 +3,8 @@ import { authorizedTestEmails, isProdEnv } from '../../config'; import { logger, ResultAsync } from '../../core/utils'; import { CreateUserCredentials } from '../../modules/authN'; import { OtherError, UnauthorizedError } from '../../modules/shared'; -import routes from '../../routes'; import { makeKeycloakClient } from './keycloakClient'; +import { Routes } from '@potentiel-applications/routes'; const ONE_MONTH = 3600 * 24 * 30; @@ -61,7 +61,7 @@ export const createUserCredentials: CreateUserCredentials = (args) => { clientId: KEYCLOAK_USER_CLIENT_ID, actions, realm: KEYCLOAK_REALM, - redirectUri: BASE_URL + routes.REDIRECT_BASED_ON_ROLE, + redirectUri: BASE_URL + Routes.Auth.redirectToDashboard(), lifespan: ONE_MONTH, }); } else { diff --git a/packages/applications/legacy/src/infra/keycloak/makeKeycloakAuth.ts b/packages/applications/legacy/src/infra/keycloak/makeKeycloakAuth.ts index 1e7e83fd1c..90e5026d38 100644 --- a/packages/applications/legacy/src/infra/keycloak/makeKeycloakAuth.ts +++ b/packages/applications/legacy/src/infra/keycloak/makeKeycloakAuth.ts @@ -1,43 +1,17 @@ -import QueryString from 'querystring'; import { EnsureRole, RegisterAuth } from '../../modules/authN'; import { CreateUser, GetUserByEmail } from '../../modules/users'; -import routes from '../../routes'; + import { makeAttachUserToRequestMiddleware } from './attachUserToRequestMiddleware'; -import { miseAJourStatistiquesUtilisation } from '../../controllers/helpers'; import { getLogger } from '@potentiel-libraries/monitoring'; -import { Routes } from '@potentiel-applications/routes'; import { RequestHandler } from 'express'; -import { decode } from 'next-auth/jwt'; export interface KeycloakAuthDeps { - NEXT_AUTH_SESSION_TOKEN_COOKIE_NAME: string | undefined; getUserByEmail: GetUserByEmail; createUser: CreateUser; } export const makeKeycloakAuth = (deps: KeycloakAuthDeps) => { - const { NEXT_AUTH_SESSION_TOKEN_COOKIE_NAME, getUserByEmail, createUser } = deps; - - if (!NEXT_AUTH_SESSION_TOKEN_COOKIE_NAME) { - console.error('Missing NEXT_AUTH_SESSION_TOKEN_COOKIE_NAME env var'); - process.exit(1); - } - - const loadToken: RequestHandler = async (req, _, next) => { - const cookie = req.cookies[NEXT_AUTH_SESSION_TOKEN_COOKIE_NAME]; - if (cookie) { - const token = await decode({ - token: cookie, - secret: process.env.NEXTAUTH_SECRET ?? '', - }); - if (token) { - req.token = token; - } else { - console.log('could not decode token from cookie'); - } - } - next(); - }; + const { getUserByEmail, createUser } = deps; const protectRoute: RequestHandler = async (req, res, next) => { if (req.user) { @@ -67,34 +41,13 @@ export const makeKeycloakAuth = (deps: KeycloakAuthDeps) => { }); }; - const registerAuth: RegisterAuth = ({ app, router }) => { - app.use(loadToken); - + const registerAuth: RegisterAuth = ({ app }) => { app.use( makeAttachUserToRequestMiddleware({ getUserByEmail, createUser, }), ); - - router.get(routes.REDIRECT_BASED_ON_ROLE, protectRoute, async (req, res) => { - miseAJourStatistiquesUtilisation({ - type: 'connexionUtilisateur', - données: { - utilisateur: { - role: req.user.role, - }, - }, - }); - - // @ts-ignore - const queryString = new QueryString.stringify(req.query); - const redirectTo = - req.user.role === 'grd' - ? Routes.Raccordement.lister - : `${routes.LISTE_PROJETS}?${queryString}`; - return res.redirect(redirectTo); - }); }; return { diff --git a/packages/applications/legacy/src/infra/keycloak/resendInvitationEmail.ts b/packages/applications/legacy/src/infra/keycloak/resendInvitationEmail.ts index 6c5238e65e..7ce06ba73b 100644 --- a/packages/applications/legacy/src/infra/keycloak/resendInvitationEmail.ts +++ b/packages/applications/legacy/src/infra/keycloak/resendInvitationEmail.ts @@ -2,8 +2,8 @@ import { requiredAction } from '@potentiel-libraries/keycloak-cjs'; import { authorizedTestEmails, isProdEnv } from '../../config'; import { logger, ResultAsync } from '../../core/utils'; import { OtherError } from '../../modules/shared'; -import routes from '../../routes'; import { makeKeycloakClient } from './keycloakClient'; +import { Routes } from '@potentiel-applications/routes'; const { KEYCLOAK_ADMIN_CLIENT_ID, @@ -46,7 +46,7 @@ export const resendInvitationEmail = (email: string) => { clientId: KEYCLOAK_USER_CLIENT_ID, actions, realm: KEYCLOAK_REALM, - redirectUri: BASE_URL + routes.REDIRECT_BASED_ON_ROLE, + redirectUri: BASE_URL + Routes.Auth.redirectToDashboard(), lifespan: ONE_MONTH, }); } else { diff --git a/packages/applications/legacy/src/modules/authN/queries/RegisterAuth.ts b/packages/applications/legacy/src/modules/authN/queries/RegisterAuth.ts index cad3080cd6..b1f3c8687a 100644 --- a/packages/applications/legacy/src/modules/authN/queries/RegisterAuth.ts +++ b/packages/applications/legacy/src/modules/authN/queries/RegisterAuth.ts @@ -1,5 +1,5 @@ -import type { Application, Router } from 'express'; +import type { Application } from 'express'; export interface RegisterAuth { - (args: { app: Application; router: Router }): void; + (args: { app: Application }): void; } diff --git a/packages/applications/legacy/src/routes.ts b/packages/applications/legacy/src/routes.ts index 9360ea04ac..ba02c909a7 100644 --- a/packages/applications/legacy/src/routes.ts +++ b/packages/applications/legacy/src/routes.ts @@ -25,16 +25,13 @@ export { withParams }; class routes { static HOME = '/'; - static LOGIN = '/auth/signIn'; static STATS = '/stats.html'; static ABONNEMENT_LETTRE_INFORMATION = '/abonnement-lettre-information.html'; static POST_SINSCRIRE_LETTRE_INFORMATION = '/s-inscrire-a-la-lettre-d-information'; static DECLARATION_ACCESSIBILITE = '/accessibilite.html'; - static LOGOUT_ACTION = '/api/auth/federated-logout'; static SIGNUP = '/signup.html'; static POST_SIGNUP = '/signup'; - static REDIRECT_BASED_ON_ROLE = '/go-to-user-dashboard'; static ADMIN_GARANTIES_FINANCIERES = '/admin/garanties-financieres.html'; static ADMIN_AO_PERIODE = '/admin/appels-offres.html'; diff --git a/packages/applications/legacy/src/server.ts b/packages/applications/legacy/src/server.ts index c2687dc028..4a7daf80e3 100644 --- a/packages/applications/legacy/src/server.ts +++ b/packages/applications/legacy/src/server.ts @@ -107,7 +107,7 @@ export async function makeServer(port: number, sessionSecret: string) { app.use(express.json({ limit: FILE_SIZE_LIMIT_IN_MB + 'mb' })); - registerAuth({ app, router: v1Router }); + registerAuth({ app }); app.use(v1Router); app.use(express.static(path.join(__dirname, 'public'))); diff --git a/packages/applications/legacy/src/views/components/UI/molecules/dropdowns/DropdownMenu.tsx b/packages/applications/legacy/src/views/components/UI/molecules/dropdowns/DropdownMenu.tsx index a9c0635570..14e2760765 100644 --- a/packages/applications/legacy/src/views/components/UI/molecules/dropdowns/DropdownMenu.tsx +++ b/packages/applications/legacy/src/views/components/UI/molecules/dropdowns/DropdownMenu.tsx @@ -3,7 +3,7 @@ import { ArrowDownIcon } from '../../atoms/icons'; type DropdownMenuProps = ComponentProps<'li'> & { buttonChildren: React.ReactNode; - children?: (ReactElement | false)[]; + children?: ReactElement | (ReactElement | false)[]; }; export const DropdownMenu: React.FC & { DropdownItem: typeof DropdownItem } = ({ @@ -14,7 +14,9 @@ export const DropdownMenu: React.FC & { DropdownItem: typeof }: DropdownMenuProps) => { const [visible, setVisible] = useState(false); const isCurrent = children - ? children.some((subMenu) => subMenu && subMenu.props.isCurrent) + ? Array.isArray(children) + ? children.some((subMenu) => subMenu && subMenu.props.isCurrent) + : children.props.isCurrent : undefined; const ref = useRef(null); diff --git a/packages/applications/legacy/src/views/components/UI/organisms/Header.tsx b/packages/applications/legacy/src/views/components/UI/organisms/Header.tsx index 265be2d5cc..a3a161ae78 100644 --- a/packages/applications/legacy/src/views/components/UI/organisms/Header.tsx +++ b/packages/applications/legacy/src/views/components/UI/organisms/Header.tsx @@ -93,7 +93,7 @@ const Header: React.FC & { MenuItem: typeof MenuItem } = ({
- +
@@ -156,7 +156,7 @@ const QuickAccess = ({ user }: QuickAccessProps) => (
  • @@ -179,7 +179,7 @@ const QuickAccess = ({ user }: QuickAccessProps) => (
  • M'identifier diff --git a/packages/applications/legacy/src/views/components/UI/organisms/UserNavigation.tsx b/packages/applications/legacy/src/views/components/UI/organisms/UserNavigation.tsx index 7edea69a9b..51af333890 100644 --- a/packages/applications/legacy/src/views/components/UI/organisms/UserNavigation.tsx +++ b/packages/applications/legacy/src/views/components/UI/organisms/UserNavigation.tsx @@ -120,6 +120,9 @@ const MenuAdmin = (currentPage?: string) => ( Importer des dates de mise en service + + Corriger des références dossier + ( > Courriers historiques - - Corrections de références dossier - ( -
    +
    diff --git a/packages/applications/legacy/src/views/pages/Signup.tsx b/packages/applications/legacy/src/views/pages/Signup.tsx index 318fdb3369..23aa52d4ff 100644 --- a/packages/applications/legacy/src/views/pages/Signup.tsx +++ b/packages/applications/legacy/src/views/pages/Signup.tsx @@ -16,6 +16,7 @@ import { } from '../components'; import { hydrateOnClient } from '../helpers'; import { App } from '../App'; +import { Routes } from '@potentiel-applications/routes'; type SignupProps = { request: Request; @@ -155,7 +156,7 @@ const SignupFailed = ({ error }: SignupFailedProps) => (
    {error}
    - + M'identifier diff --git a/packages/applications/legacy/src/views/pages/homePage/Home.tsx b/packages/applications/legacy/src/views/pages/homePage/Home.tsx index e184a46acc..293de9065f 100644 --- a/packages/applications/legacy/src/views/pages/homePage/Home.tsx +++ b/packages/applications/legacy/src/views/pages/homePage/Home.tsx @@ -1,11 +1,11 @@ import type { Request } from 'express'; import React from 'react'; -import routes from '../../../routes'; import { Header, Footer, ArrowRightWithCircle } from '../../components'; import { hydrateOnClient } from '../../helpers/hydrateOnClient'; import { InscriptionConnexion, Benefices, PropositionDeValeur } from './components'; import { App } from '../..'; import { User } from '../../../entities'; +import { Routes } from '@potentiel-applications/routes'; type HomeProps = { request: Request; @@ -29,9 +29,9 @@ export const Home = (props: HomeProps) => { return ( -
    +
    {user && ( - +
    {getMenuText(user)} diff --git a/packages/applications/legacy/src/views/pages/homePage/components/InscriptionConnexion.tsx b/packages/applications/legacy/src/views/pages/homePage/components/InscriptionConnexion.tsx index d3999cb525..a7a5638110 100644 --- a/packages/applications/legacy/src/views/pages/homePage/components/InscriptionConnexion.tsx +++ b/packages/applications/legacy/src/views/pages/homePage/components/InscriptionConnexion.tsx @@ -1,5 +1,4 @@ import React, { useState } from 'react'; -import { User } from '../../../../entities'; import routes from '../../../../routes'; import { AccountIcon, @@ -11,6 +10,7 @@ import { LogoutBoxIcon, SecondaryLinkButton, } from '../../../components'; +import { Routes } from '@potentiel-applications/routes'; type InscriptionConnexionProps = | ({ connected: true } & BienvenueProps) @@ -51,14 +51,14 @@ const Bienvenue = ({ fullName, redirectText }: BienvenueProps) => (
    {redirectText} Me déconnecter @@ -103,7 +103,7 @@ const SignupBox = () => { )}

    - Vous avez déjà un compte ? + Vous avez déjà un compte ?

    ); @@ -144,7 +144,7 @@ const LoginBox = () => (

    Connectez-vous pour accéder aux projets.

    - + M'identifier diff --git a/packages/applications/routes/src/auth/auth.routes.ts b/packages/applications/routes/src/auth/auth.routes.ts new file mode 100644 index 0000000000..839bd572bc --- /dev/null +++ b/packages/applications/routes/src/auth/auth.routes.ts @@ -0,0 +1,19 @@ +export const signIn = (callbackUrl?: string) => { + const route = `/auth/signIn`; + if (!callbackUrl) return route; + const params = new URLSearchParams({ callbackUrl }); + return `${route}?${params}`; +}; + +// The signout page, where the user is redirected after federeated logout +export const signOut = (callbackUrl?: string) => { + const route = `/auth/signIn`; + if (!callbackUrl) return route; + const params = new URLSearchParams({ callbackUrl }); + return `${route}?${params}`; +}; + +// The route to call to initiate user logout +export const federatedLogout = () => `/api/auth/federated-logout`; + +export const redirectToDashboard = () => `/go-to-user-dashboard`; diff --git a/packages/applications/routes/src/auth/index.ts b/packages/applications/routes/src/auth/index.ts new file mode 100644 index 0000000000..047fb6f439 --- /dev/null +++ b/packages/applications/routes/src/auth/index.ts @@ -0,0 +1 @@ +export * as Auth from './auth.routes'; diff --git a/packages/applications/routes/src/index.ts b/packages/applications/routes/src/index.ts index 9f06175924..a73c3b2b11 100644 --- a/packages/applications/routes/src/index.ts +++ b/packages/applications/routes/src/index.ts @@ -7,6 +7,7 @@ import { Projet } from './projet'; import { Recours } from './éliminé'; import { Tache } from './tâche'; import { Période } from './période'; +import { Auth } from './auth'; export const Routes = { Abandon, @@ -20,4 +21,5 @@ export const Routes = { Recours, Tache, Période, + Auth, }; diff --git a/packages/applications/routes/src/projet/projet.routes.ts b/packages/applications/routes/src/projet/projet.routes.ts index 71be180c58..6308b8fc94 100644 --- a/packages/applications/routes/src/projet/projet.routes.ts +++ b/packages/applications/routes/src/projet/projet.routes.ts @@ -4,3 +4,5 @@ export const details = (identifiantProjet: string) => { const url = `/projet/${encodeParameter(identifiantProjet)}/details.html`; return url; }; + +export const lister = () => `/projets.html`; diff --git a/packages/applications/ssr/public/next.svg b/packages/applications/ssr/public/next.svg deleted file mode 100644 index 5174b28c56..0000000000 --- a/packages/applications/ssr/public/next.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/packages/applications/ssr/public/vercel.svg b/packages/applications/ssr/public/vercel.svg deleted file mode 100644 index d2f8422273..0000000000 --- a/packages/applications/ssr/public/vercel.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/packages/applications/ssr/src/app/api/auth/federated-logout/route.ts b/packages/applications/ssr/src/app/api/auth/federated-logout/route.ts index 62e17fdf3a..dff241c211 100644 --- a/packages/applications/ssr/src/app/api/auth/federated-logout/route.ts +++ b/packages/applications/ssr/src/app/api/auth/federated-logout/route.ts @@ -1,23 +1,19 @@ import { NextResponse } from 'next/server'; import { getServerSession } from 'next-auth'; -import { cookies } from 'next/headers'; -import { authOptions, issuerUrl } from '@/auth'; +import { issuerUrl } from '@/auth'; /** * This route manages logout from the SSO (keycloak). - * Without this, logging out of the app only removes session, but the user is still logged to SSO + * Without this, logging out of the app only removes cookies, but the user is still logged to SSO * @see https://github.com/nextauthjs/next-auth/discussions/3938 */ export async function GET() { - const { NEXTAUTH_URL = '', NEXT_AUTH_SESSION_TOKEN_COOKIE_NAME = 'next-auth.session-token' } = - process.env; + const { NEXTAUTH_URL = '' } = process.env; // Gets the session, with idToken const session = await getServerSession({ - ...authOptions, callbacks: { - ...authOptions.callbacks, session({ session, token }) { session.idToken = token.idToken; return session; @@ -32,13 +28,11 @@ export async function GET() { const redirectUrl = new URL('/auth/signOut', NEXTAUTH_URL); const ssoLogoutUrl = new URL(`${issuerUrl}/protocol/openid-connect/logout`); - cookies().delete(NEXT_AUTH_SESSION_TOKEN_COOKIE_NAME); - if (session.idToken) { // without this, Keycloak prompts the user for confirmation ssoLogoutUrl.searchParams.set('post_logout_redirect_uri', redirectUrl.toString()); ssoLogoutUrl.searchParams.set('id_token_hint', session.idToken); } - return NextResponse.redirect(ssoLogoutUrl); + return NextResponse.redirect(ssoLogoutUrl.toString()); } diff --git a/packages/applications/ssr/src/app/auth/signIn/page.tsx b/packages/applications/ssr/src/app/auth/signIn/page.tsx index 14308c9f94..5ee4f29537 100644 --- a/packages/applications/ssr/src/app/auth/signIn/page.tsx +++ b/packages/applications/ssr/src/app/auth/signIn/page.tsx @@ -4,12 +4,14 @@ import { redirect, useSearchParams } from 'next/navigation'; import { signIn, useSession } from 'next-auth/react'; import { useEffect } from 'react'; +import { Routes } from '@potentiel-applications/routes'; + import { PageTemplate } from '@/components/templates/Page.template'; export default function SignIn() { const params = useSearchParams(); const { status } = useSession(); - const callbackUrl = params.get('callbackUrl') ?? '/'; + const callbackUrl = params.get('callbackUrl') ?? Routes.Auth.redirectToDashboard(); useEffect(() => { if (status === 'loading') return; diff --git a/packages/applications/ssr/src/app/auth/signOut/page.tsx b/packages/applications/ssr/src/app/auth/signOut/page.tsx index 5740c2ccf6..065aa8f687 100644 --- a/packages/applications/ssr/src/app/auth/signOut/page.tsx +++ b/packages/applications/ssr/src/app/auth/signOut/page.tsx @@ -5,7 +5,7 @@ import { useEffect } from 'react'; import { PageTemplate } from '@/components/templates/Page.template'; -export default function SignIn() { +export default function SignOut() { useEffect(() => { signOut({ callbackUrl: '/' }); }, []); diff --git a/packages/applications/ssr/src/app/go-to-user-dashboard/route.ts b/packages/applications/ssr/src/app/go-to-user-dashboard/route.ts new file mode 100644 index 0000000000..d97c6b8841 --- /dev/null +++ b/packages/applications/ssr/src/app/go-to-user-dashboard/route.ts @@ -0,0 +1,14 @@ +import { redirect } from 'next/navigation'; + +import { Routes } from '@potentiel-applications/routes'; +import { Role } from '@potentiel-domain/utilisateur'; + +import { withUtilisateur } from '@/utils/withUtilisateur'; + +export const GET = async () => + withUtilisateur(async ({ role }) => { + const redirectTo = role.estÉgaleÀ(Role.grd) + ? Routes.Raccordement.lister + : Routes.Projet.lister(); + redirect(redirectTo); + }); diff --git a/packages/applications/ssr/src/components/molecules/UserHeaderQuickAccessItem.tsx b/packages/applications/ssr/src/components/molecules/UserHeaderQuickAccessItem.tsx index a8b61d3ba0..f92246ab66 100644 --- a/packages/applications/ssr/src/components/molecules/UserHeaderQuickAccessItem.tsx +++ b/packages/applications/ssr/src/components/molecules/UserHeaderQuickAccessItem.tsx @@ -42,6 +42,7 @@ export async function UserHeaderQuickAccessItem() { iconId: 'ri-logout-box-line', linkProps: { href: '/api/auth/federated-logout', + prefetch: false, }, text: 'Me déconnecter', }} diff --git a/packages/applications/ssr/src/middleware.ts b/packages/applications/ssr/src/middleware.ts index 970822e800..0728c48007 100644 --- a/packages/applications/ssr/src/middleware.ts +++ b/packages/applications/ssr/src/middleware.ts @@ -6,5 +6,7 @@ export default withAuth({ export const config = { // do not run middleware for paths matching one of following - matcher: ['/((?!api|_next/static|_next/image|auth|favicon.ico|robots.txt|images|$).*)'], + matcher: [ + '/((?!api|_next/static|_next/image|auth|favicon.ico|robots.txt|images|illustrations|$).*)', + ], }; diff --git a/packages/applications/ssr/src/types/next-auth.d.ts b/packages/applications/ssr/src/types/next-auth.d.ts index 5e50bafec5..7d19f37a5f 100644 --- a/packages/applications/ssr/src/types/next-auth.d.ts +++ b/packages/applications/ssr/src/types/next-auth.d.ts @@ -10,5 +10,6 @@ declare module 'next-auth/jwt' { declare module 'next-auth' { interface Session { idToken?: string; + accessToken?: string; } } diff --git a/packages/applications/ssr/src/utils/getAuthenticatedUser.handler.ts b/packages/applications/ssr/src/utils/getAuthenticatedUser.handler.ts index 73b4b29a61..d7312ac33e 100644 --- a/packages/applications/ssr/src/utils/getAuthenticatedUser.handler.ts +++ b/packages/applications/ssr/src/utils/getAuthenticatedUser.handler.ts @@ -1,6 +1,6 @@ import { Message, MessageHandler } from 'mediateur'; import { cookies, headers } from 'next/headers'; -import { decode } from 'next-auth/jwt'; +import { getToken, GetTokenParams } from 'next-auth/jwt'; // import * as Sentry from '@sentry/nextjs'; import { Utilisateur } from '@potentiel-domain/utilisateur'; @@ -11,23 +11,22 @@ export type GetAuthenticatedUserMessage = Message< Utilisateur.ValueType >; -const { NEXT_AUTH_SESSION_TOKEN_COOKIE_NAME = 'next-auth.session-token' } = process.env; - /** * Check for an access token * - in the session (encrypted) - * - in Authorization header + * - in Authorization header (clear) **/ const getAccessToken = async () => { - const cookiesContent = cookies(); - const sessionToken = cookiesContent.get(NEXT_AUTH_SESSION_TOKEN_COOKIE_NAME)?.value || ''; - if (sessionToken) { - const decoded = await decode({ - token: sessionToken, - secret: process.env.NEXTAUTH_SECRET ?? '', - }); - - return decoded?.accessToken; + const token = await getToken({ + req: { + cookies: cookies(), + // NB: getToken peut également récupérer le token dans le header Authorization + // mais elle attend un token chiffré, ce qui n'est pas le cas dans le cadre de l'authentification API + // headers: headers() + } as unknown as GetTokenParams['req'], + }); + if (token) { + return token.accessToken; } const authorizationHeader = headers().get('authorization'); return authorizationHeader?.replace(/Bearer /, ''); From 9ecb05005b17cc6042d6a2116a9edf5e44f57368 Mon Sep 17 00:00:00 2001 From: Benjamin Levesque <14175665+benjlevesque@users.noreply.github.com> Date: Thu, 14 Nov 2024 13:34:26 +0100 Subject: [PATCH 06/16] =?UTF-8?q?=F0=9F=8E=A8=20Links?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../legacy/src/infra/keycloak/makeKeycloakAuth.ts | 4 ++-- packages/applications/routes/src/auth/auth.routes.ts | 2 +- .../ssr/src/app/api/auth/federated-logout/route.ts | 4 +++- .../components/molecules/UserHeaderQuickAccessItem.tsx | 8 ++++++-- packages/applications/ssr/src/middleware.ts | 4 +++- packages/applications/ssr/src/utils/withErrorHandling.ts | 3 ++- 6 files changed, 17 insertions(+), 8 deletions(-) diff --git a/packages/applications/legacy/src/infra/keycloak/makeKeycloakAuth.ts b/packages/applications/legacy/src/infra/keycloak/makeKeycloakAuth.ts index 90e5026d38..52f44b0dca 100644 --- a/packages/applications/legacy/src/infra/keycloak/makeKeycloakAuth.ts +++ b/packages/applications/legacy/src/infra/keycloak/makeKeycloakAuth.ts @@ -1,3 +1,4 @@ +import { Routes } from '@potentiel-applications/routes'; import { EnsureRole, RegisterAuth } from '../../modules/authN'; import { CreateUser, GetUserByEmail } from '../../modules/users'; @@ -17,8 +18,7 @@ export const makeKeycloakAuth = (deps: KeycloakAuthDeps) => { if (req.user) { return next(); } - const params = new URLSearchParams({ callbackUrl: req.path }); - return res.redirect(`auth/signIn?${params}`); + return res.redirect(Routes.Auth.signIn(req.path)); }; const ensureRole: EnsureRole = (roles) => { diff --git a/packages/applications/routes/src/auth/auth.routes.ts b/packages/applications/routes/src/auth/auth.routes.ts index 839bd572bc..ef1b5c5fbe 100644 --- a/packages/applications/routes/src/auth/auth.routes.ts +++ b/packages/applications/routes/src/auth/auth.routes.ts @@ -7,7 +7,7 @@ export const signIn = (callbackUrl?: string) => { // The signout page, where the user is redirected after federeated logout export const signOut = (callbackUrl?: string) => { - const route = `/auth/signIn`; + const route = `/auth/signOut`; if (!callbackUrl) return route; const params = new URLSearchParams({ callbackUrl }); return `${route}?${params}`; diff --git a/packages/applications/ssr/src/app/api/auth/federated-logout/route.ts b/packages/applications/ssr/src/app/api/auth/federated-logout/route.ts index dff241c211..dd273a996f 100644 --- a/packages/applications/ssr/src/app/api/auth/federated-logout/route.ts +++ b/packages/applications/ssr/src/app/api/auth/federated-logout/route.ts @@ -1,6 +1,8 @@ import { NextResponse } from 'next/server'; import { getServerSession } from 'next-auth'; +import { Routes } from '@potentiel-applications/routes'; + import { issuerUrl } from '@/auth'; /** @@ -25,7 +27,7 @@ export async function GET() { } // after keycloak logout, redirect the user to this route to remove the session - const redirectUrl = new URL('/auth/signOut', NEXTAUTH_URL); + const redirectUrl = new URL(Routes.Auth.signOut(), NEXTAUTH_URL); const ssoLogoutUrl = new URL(`${issuerUrl}/protocol/openid-connect/logout`); if (session.idToken) { diff --git a/packages/applications/ssr/src/components/molecules/UserHeaderQuickAccessItem.tsx b/packages/applications/ssr/src/components/molecules/UserHeaderQuickAccessItem.tsx index f92246ab66..cf762e4924 100644 --- a/packages/applications/ssr/src/components/molecules/UserHeaderQuickAccessItem.tsx +++ b/packages/applications/ssr/src/components/molecules/UserHeaderQuickAccessItem.tsx @@ -31,6 +31,7 @@ export async function UserHeaderQuickAccessItem() { iconId: 'ri-user-line', linkProps: { href: accountUrl, + prefetch: false, }, text: utilisateur.nom, }} @@ -41,7 +42,7 @@ export async function UserHeaderQuickAccessItem() { quickAccessItem={{ iconId: 'ri-logout-box-line', linkProps: { - href: '/api/auth/federated-logout', + href: Routes.Auth.federatedLogout(), prefetch: false, }, text: 'Me déconnecter', @@ -58,6 +59,7 @@ export async function UserHeaderQuickAccessItem() { iconId: 'ri-account-circle-line', linkProps: { href: '/signup.html', + prefetch: false, }, text: "M'inscrire", }} @@ -66,7 +68,8 @@ export async function UserHeaderQuickAccessItem() { quickAccessItem={{ iconId: 'ri-lock-line', linkProps: { - href: '/auth/signIn', + href: Routes.Auth.signIn(), + prefetch: false, }, text: "M'identifier", }} @@ -105,6 +108,7 @@ async function getTâcheHeaderQuickAccessItem(utilisateur: Utilisateur.ValueType iconId: nombreTâches > 0 ? 'ri-mail-unread-line' : 'ri-mail-check-line', linkProps: { href: Routes.Tache.lister, + prefetch: false, }, text: `Tâches (${nombreTâches})`, }} diff --git a/packages/applications/ssr/src/middleware.ts b/packages/applications/ssr/src/middleware.ts index 0728c48007..dfa01b9c4a 100644 --- a/packages/applications/ssr/src/middleware.ts +++ b/packages/applications/ssr/src/middleware.ts @@ -1,7 +1,9 @@ import { withAuth } from 'next-auth/middleware'; +import { Routes } from '@potentiel-applications/routes'; + export default withAuth({ - pages: { signIn: '/auth/signIn' }, + pages: { signIn: Routes.Auth.signIn() }, }); export const config = { diff --git a/packages/applications/ssr/src/utils/withErrorHandling.ts b/packages/applications/ssr/src/utils/withErrorHandling.ts index be879c1115..d527d9ddd4 100644 --- a/packages/applications/ssr/src/utils/withErrorHandling.ts +++ b/packages/applications/ssr/src/utils/withErrorHandling.ts @@ -6,6 +6,7 @@ import { getLogger } from '@potentiel-libraries/monitoring'; import { DomainError } from '@potentiel-domain/core'; import { bootstrap } from '@potentiel-applications/bootstrap'; import { permissionMiddleware } from '@potentiel-domain/utilisateur'; +import { Routes } from '@potentiel-applications/routes'; import { NoAuthenticatedUserError } from './getAuthenticatedUser.handler'; @@ -23,7 +24,7 @@ export async function withErrorHandling( } if (e instanceof NoAuthenticatedUserError) { - redirect('/auth/signIn'); + redirect(Routes.Auth.signIn()); } if (e instanceof DomainError) { From 721530b4d94d786c47c4793f02882706f83a2950 Mon Sep 17 00:00:00 2001 From: Benjamin Levesque <14175665+benjlevesque@users.noreply.github.com> Date: Thu, 14 Nov 2024 14:02:34 +0100 Subject: [PATCH 07/16] =?UTF-8?q?=F0=9F=90=9B=20Fix=20mw?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/applications/ssr/src/middleware.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/applications/ssr/src/middleware.ts b/packages/applications/ssr/src/middleware.ts index dfa01b9c4a..e56ced03bb 100644 --- a/packages/applications/ssr/src/middleware.ts +++ b/packages/applications/ssr/src/middleware.ts @@ -1,9 +1,8 @@ import { withAuth } from 'next-auth/middleware'; -import { Routes } from '@potentiel-applications/routes'; - export default withAuth({ - pages: { signIn: Routes.Auth.signIn() }, + // NB: importing Routes is not working in the middleware + pages: { signIn: '/auth/signIn' }, }); export const config = { From 57fc415bf7e01a8d786e917ea9c679efa7c45a2d Mon Sep 17 00:00:00 2001 From: Benjamin Levesque <14175665+benjlevesque@users.noreply.github.com> Date: Thu, 14 Nov 2024 14:35:55 +0100 Subject: [PATCH 08/16] =?UTF-8?q?=F0=9F=93=9D=20Doc?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/applications/ssr/src/auth.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/applications/ssr/src/auth.ts b/packages/applications/ssr/src/auth.ts index 3b0a2375ac..60e99860bd 100644 --- a/packages/applications/ssr/src/auth.ts +++ b/packages/applications/ssr/src/auth.ts @@ -29,6 +29,9 @@ export const authOptions: AuthOptions = { updateAge: FIFTEEN_MINUTES, }, callbacks: { + // Stores accessToken and idToken to the auth cookie + // accessToken is used to get user information + // idToken is necessary to logout of keycloak jwt({ token, account }) { if (account?.access_token) { token.accessToken = account.access_token; From d41a968d73dcb688484490faa9aec74e6b2a2a2d Mon Sep 17 00:00:00 2001 From: Benjamin Levesque <14175665+benjlevesque@users.noreply.github.com> Date: Thu, 14 Nov 2024 15:25:54 +0100 Subject: [PATCH 09/16] =?UTF-8?q?=E2=9C=85=20Fix=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../attachUserToRequestMiddleware.spec.ts | 149 ++++++------------ .../keycloak/attachUserToRequestMiddleware.ts | 66 +++++--- 2 files changed, 89 insertions(+), 126 deletions(-) diff --git a/packages/applications/legacy/src/infra/keycloak/attachUserToRequestMiddleware.spec.ts b/packages/applications/legacy/src/infra/keycloak/attachUserToRequestMiddleware.spec.ts index b3baf2efbd..9ff9ab6e8c 100644 --- a/packages/applications/legacy/src/infra/keycloak/attachUserToRequestMiddleware.spec.ts +++ b/packages/applications/legacy/src/infra/keycloak/attachUserToRequestMiddleware.spec.ts @@ -14,43 +14,62 @@ describe(`attachUserToRequestMiddleware`, () => { path, } as express.Request; const nextFunction = jest.fn(); + const getAccessToken = jest.fn(() => Promise.resolve(undefined)); const middleware = makeAttachUserToRequestMiddleware({ getUserByEmail: jest.fn(), createUser: makeFakeCreateUser(), + getAccessToken, }); middleware(request, {} as express.Response, nextFunction); it('should not attach the user to the request and execute the next function', () => { expect(request.user).toBeUndefined(); + expect(getAccessToken).not.toHaveBeenCalled(); expect(nextFunction).toHaveBeenCalled(); }); }); }); describe(`when the path is not a static one`, () => { - describe(`when there is no user email in the keycloak access token`, () => { - const hasRealmRole = jest.fn(); - - const request = { - path: '/a-protected-path', - } as express.Request; - const token = { - content: {}, - hasRealmRole, + const request = { path: '/a-protected-path' } as express.Request; + const makeFakeGetAccessToken = (role: string, username: string) => async () => { + const iat = Math.floor(Date.now() / 1000); + const claims = { + exp: iat, + iat: iat + 300, + auth_time: iat, + jti: 'jti', + iss: 'http://localhost:8080/realms/Potentiel', + aud: ['realm-management', 'account'], + sub: 'sub', + typ: 'Bearer', + azp: 'potentiel-web', + sid: 'sid', + acr: '0', + realm_access: { + roles: [role], + }, + scope: 'openid email profile', + email_verified: true, + name: 'Admin Test', + preferred_username: username, + given_name: 'Admin', + family_name: 'Test', + email: username, }; - request['kauth'] = { grant: { access_token: token } }; - - token.content['email'] = undefined; - const nextFunction = jest.fn(); - + return `xx.${btoa(JSON.stringify(claims))}.xx`; + }; + describe(`when there is no user email in the keycloak access token`, () => { const middleware = makeAttachUserToRequestMiddleware({ getUserByEmail: jest.fn(), createUser: makeFakeCreateUser(), + getAccessToken: makeFakeGetAccessToken('admin', ''), }); - middleware(request, {} as express.Response, nextFunction); - it('should not attach the user to the request and execute the next function', () => { + it('should not attach the user to the request and execute the next function', async () => { + const nextFunction = jest.fn(); + await middleware(request, {} as express.Response, nextFunction); expect(request.user).toBeUndefined(); expect(nextFunction).toHaveBeenCalled(); }); @@ -59,25 +78,10 @@ describe(`attachUserToRequestMiddleware`, () => { describe(`when there is a user email in the keycloak access token`, () => { describe(`when the user exists in Potentiel`, () => { describe(`when no role in the keycloak access token`, () => { - const hasRealmRole = jest.fn((role) => false); - - const request = { - path: '/a-protected-path', - } as express.Request; - - const token = { - content: {}, - hasRealmRole, - }; - request['kauth'] = { grant: { access_token: token } }; - const userEmail = 'user@email.com'; - token.content['email'] = userEmail; - const user: User = { email: userEmail, - fullName: 'User', id: 'user-id', role: undefined as unknown as UserRole, }; @@ -91,39 +95,23 @@ describe(`attachUserToRequestMiddleware`, () => { const middleware = makeAttachUserToRequestMiddleware({ getUserByEmail, createUser: makeFakeCreateUser(), + getAccessToken: makeFakeGetAccessToken('', userEmail), }); - middleware(request, {} as express.Response, nextFunction); - it('should attach the user to the request with no role and execute the next function', () => { - expect(request.user).toMatchObject({ - ...user, - accountUrl: expect.any(String), - }); + it('should not attach the user to the request', async () => { + await middleware(request, {} as express.Response, nextFunction); + expect(request.user).toBeUndefined(); expect(nextFunction).toHaveBeenCalled(); }); }); describe(`when there is a role in the keycloak access token`, () => { const tokenUserRole = 'admin'; - const hasRealmRole = jest.fn((role) => (role === tokenUserRole ? true : false)); - - const request = { - path: '/a-protected-path', - } as express.Request; - - const token = { - content: {}, - hasRealmRole, - }; - request['kauth'] = { grant: { access_token: token } }; const userEmail = 'user@email.com'; - token.content['email'] = userEmail; - const user: User = { email: userEmail, - fullName: 'User', id: 'user-id', role: 'porteur-projet', }; @@ -137,19 +125,18 @@ describe(`attachUserToRequestMiddleware`, () => { const middleware = makeAttachUserToRequestMiddleware({ getUserByEmail, createUser: makeFakeCreateUser(), + getAccessToken: makeFakeGetAccessToken(tokenUserRole, userEmail), }); - middleware(request, {} as express.Response, nextFunction); - it('should attach the user to the request with role from token', () => { + it('should attach the user to the request with role from token', async () => { const expectedUser = { ...user, role: tokenUserRole, accountUrl: expect.any(String), }; + await middleware(request, {} as express.Response, nextFunction); expect(request.user).toMatchObject(expectedUser); - }); - it('should execute the next function', () => { expect(nextFunction).toHaveBeenCalled(); }); }); @@ -157,26 +144,8 @@ describe(`attachUserToRequestMiddleware`, () => { describe(`when the user does not exist in Potentiel`, () => { describe(`when no role in the keycloak access token`, () => { - const hasRealmRole = jest.fn(() => false); - - const request = { - path: '/a-protected-path', - session: {}, - } as express.Request; - //@ts-ignore - request.session.destroy = jest.fn(); - - const token = { - content: {}, - hasRealmRole, - }; - request['kauth'] = { grant: { access_token: token } }; - const userEmail = 'user@email.com'; - const userName = 'User'; - token.content['email'] = userEmail; - token.content['name'] = userName; const getUserByEmail: GetUserByEmail = jest.fn(() => okAsync(null)); const userId = 'user-id'; @@ -187,48 +156,28 @@ describe(`attachUserToRequestMiddleware`, () => { const middleware = makeAttachUserToRequestMiddleware({ getUserByEmail, createUser, + getAccessToken: makeFakeGetAccessToken('porteur-projet', userEmail), }); - middleware(request, {} as express.Response, nextFunction); - it('should attach a new user to the request', () => { + it('should attach a new user to the request', async () => { + await middleware(request, {} as express.Response, nextFunction); expect(request.user).toMatchObject({ email: userEmail, - fullName: userName, id: userId, role: 'porteur-projet', accountUrl: expect.any(String), permissions: expect.anything(), }); - }); - - it('should destroy the request session', () => { - expect(request.session.destroy).toHaveBeenCalled(); - }); - it('should execute the next function', () => { expect(nextFunction).toHaveBeenCalled(); }); }); describe(`when there is a role in the keycloak access token`, () => { const userRole = 'admin'; - const hasRealmRole = jest.fn((role) => (role === userRole ? true : false)); - - const request = { - path: '/a-protected-path', - } as express.Request; - - const token = { - content: {}, - hasRealmRole, - }; - request['kauth'] = { grant: { access_token: token } }; const userEmail = 'user@email.com'; - const userName = 'User'; - token.content['email'] = userEmail; - token.content['name'] = userName; const getUserByEmail: GetUserByEmail = jest.fn(() => okAsync(null)); const userId = 'user-id'; @@ -239,21 +188,19 @@ describe(`attachUserToRequestMiddleware`, () => { const middleware = makeAttachUserToRequestMiddleware({ getUserByEmail, createUser, + getAccessToken: makeFakeGetAccessToken(userRole, userEmail), }); - middleware(request, {} as express.Response, nextFunction); - it('should attach a new user to the request with the same role of the token', () => { + it('should attach a new user to the request with the same role of the token', async () => { + await middleware(request, {} as express.Response, nextFunction); expect(request.user).toMatchObject({ email: userEmail, - fullName: userName, id: userId, role: userRole, accountUrl: expect.any(String), permissions: expect.anything(), }); - }); - it('should execute the next function', () => { expect(nextFunction).toHaveBeenCalled(); }); }); diff --git a/packages/applications/legacy/src/infra/keycloak/attachUserToRequestMiddleware.ts b/packages/applications/legacy/src/infra/keycloak/attachUserToRequestMiddleware.ts index dc22255ce3..1db1dbd980 100644 --- a/packages/applications/legacy/src/infra/keycloak/attachUserToRequestMiddleware.ts +++ b/packages/applications/legacy/src/infra/keycloak/attachUserToRequestMiddleware.ts @@ -1,5 +1,5 @@ import { NextFunction, Request, Response } from 'express'; -import { ResultAsync } from '../../core/utils'; +import { logger, ResultAsync } from '../../core/utils'; import { CreateUser, GetUserByEmail, USER_ROLES } from '../../modules/users'; import { getPermissions } from '../../modules/authN'; import { Utilisateur } from '@potentiel-domain/utilisateur'; @@ -9,6 +9,7 @@ import NextAuthJwt from 'next-auth/jwt'; type AttachUserToRequestMiddlewareDependencies = { getUserByEmail: GetUserByEmail; createUser: CreateUser; + getAccessToken?: (req: Request) => Promise; }; // The token is generated by next-auth on the SSR app @@ -19,18 +20,27 @@ declare module 'next-auth/jwt' { } } -const getAccessToken = async (req: Request) => { - const token = await getToken({ - req: { cookies: req.cookies } as unknown as GetTokenParams['req'], - }); - return token?.accessToken; +const getNextAuthAccessToken = async (req: Request) => { + try { + const token = await getToken({ + req: { cookies: req.cookies } as unknown as GetTokenParams['req'], + }); + return token?.accessToken; + } catch (e) { + logger.error('getToken failed'); + logger.error(e); + } }; const promisify = (resultAsync: ResultAsync) => new Promise((resolve, reject) => resultAsync.match(resolve, reject)); const makeAttachUserToRequestMiddleware = - ({ getUserByEmail, createUser }: AttachUserToRequestMiddlewareDependencies) => + ({ + getUserByEmail, + createUser, + getAccessToken = getNextAuthAccessToken, + }: AttachUserToRequestMiddlewareDependencies) => async (request: Request, response: Response, next: NextFunction) => { if ( // Theses paths should be prefixed with /static in the future @@ -51,26 +61,32 @@ const makeAttachUserToRequestMiddleware = next(); return; } - const { - identifiantUtilisateur: { email }, - role: { nom: role }, - nom: fullName, - } = Utilisateur.convertirEnValueType(token); - const getOrCreateUser = async () => { - const user = await promisify(getUserByEmail(email)); - if (user) return user; - const createUserArgs = { email, role, fullName }; + try { + const { + identifiantUtilisateur: { email }, + role: { nom: role }, + nom: fullName, + } = Utilisateur.convertirEnValueType(token); + + const getOrCreateUser = async () => { + const createUserArgs = { email, role, fullName }; + const user = await promisify(getUserByEmail(email)); + if (user) return { id: user.id, ...createUserArgs }; - const { id } = await promisify(createUser(createUserArgs)); - return { id, ...createUserArgs }; - }; - const user = await getOrCreateUser(); - request.user = { - ...user, - accountUrl: `${process.env.KEYCLOAK_SERVER}/realms/${process.env.KEYCLOAK_REALM}/account`, - permissions: getPermissions(user), - }; + const { id } = await promisify(createUser(createUserArgs)); + return { id, ...createUserArgs }; + }; + const user = await getOrCreateUser(); + request.user = { + ...user, + accountUrl: `${process.env.KEYCLOAK_SERVER}/realms/${process.env.KEYCLOAK_REALM}/account`, + permissions: getPermissions(user), + }; + } catch (e) { + logger.error('Auth failed'); + logger.error(e); + } next(); }; From 67a51940630134f11f7413a1e9482d1a1e70a63f Mon Sep 17 00:00:00 2001 From: Benjamin Levesque <14175665+benjlevesque@users.noreply.github.com> Date: Thu, 14 Nov 2024 16:07:05 +0100 Subject: [PATCH 10/16] =?UTF-8?q?=F0=9F=90=9B=20Handle=20legacy=20session?= =?UTF-8?q?=20compat?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ssr/src/app/api/auth/federated-logout/route.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/applications/ssr/src/app/api/auth/federated-logout/route.ts b/packages/applications/ssr/src/app/api/auth/federated-logout/route.ts index dd273a996f..88aa2a8c82 100644 --- a/packages/applications/ssr/src/app/api/auth/federated-logout/route.ts +++ b/packages/applications/ssr/src/app/api/auth/federated-logout/route.ts @@ -2,6 +2,7 @@ import { NextResponse } from 'next/server'; import { getServerSession } from 'next-auth'; import { Routes } from '@potentiel-applications/routes'; +import { getLogger } from '@potentiel-libraries/monitoring'; import { issuerUrl } from '@/auth'; @@ -34,7 +35,9 @@ export async function GET() { // without this, Keycloak prompts the user for confirmation ssoLogoutUrl.searchParams.set('post_logout_redirect_uri', redirectUrl.toString()); ssoLogoutUrl.searchParams.set('id_token_hint', session.idToken); + return NextResponse.redirect(ssoLogoutUrl.toString()); } - return NextResponse.redirect(ssoLogoutUrl.toString()); + getLogger().warn('A user logged out without an id token, the keycloak session is still active'); + return NextResponse.redirect(redirectUrl, { status: 302 }); } From 2519b07719cff691892c94156df3e2347e5cea97 Mon Sep 17 00:00:00 2001 From: Benjamin Levesque <14175665+benjlevesque@users.noreply.github.com> Date: Thu, 14 Nov 2024 21:58:15 +0100 Subject: [PATCH 11/16] =?UTF-8?q?=F0=9F=94=A5=20Remove=20session=20secret?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/applications/legacy/src/index.ts | 8 +------- packages/applications/legacy/src/server.ts | 2 +- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/packages/applications/legacy/src/index.ts b/packages/applications/legacy/src/index.ts index 92cce7d7d5..bbd8c04e31 100644 --- a/packages/applications/legacy/src/index.ts +++ b/packages/applications/legacy/src/index.ts @@ -11,11 +11,5 @@ mandatoryVariables.forEach((variable) => { } }); -const sessionSecret = process.env.SESSION_SECRET; -if (!sessionSecret) { - console.error('Missing SESSION_SECRET environment variable'); - process.exit(1); -} - const port: number = Number(process.env.PORT) || 3000; -makeServer(port, sessionSecret); +makeServer(port); diff --git a/packages/applications/legacy/src/server.ts b/packages/applications/legacy/src/server.ts index 4a7daf80e3..9ac097a654 100644 --- a/packages/applications/legacy/src/server.ts +++ b/packages/applications/legacy/src/server.ts @@ -21,7 +21,7 @@ import { MulterError } from 'multer'; setDefaultOptions({ locale: LOCALE.fr }); dotenv.config(); -export async function makeServer(port: number, sessionSecret: string) { +export async function makeServer(port: number) { try { await registerSagas(); From f0ecae9dd5a942abea935740181d1c983a9de2b5 Mon Sep 17 00:00:00 2001 From: Benjamin Levesque <14175665+benjlevesque@users.noreply.github.com> Date: Fri, 15 Nov 2024 11:30:07 +0100 Subject: [PATCH 12/16] =?UTF-8?q?=F0=9F=90=9B=20Handle=20different=20cooki?= =?UTF-8?q?e=20format?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ssr/src/app/auth/signIn/page.tsx | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/packages/applications/ssr/src/app/auth/signIn/page.tsx b/packages/applications/ssr/src/app/auth/signIn/page.tsx index 5ee4f29537..1bac710ee9 100644 --- a/packages/applications/ssr/src/app/auth/signIn/page.tsx +++ b/packages/applications/ssr/src/app/auth/signIn/page.tsx @@ -10,18 +10,24 @@ import { PageTemplate } from '@/components/templates/Page.template'; export default function SignIn() { const params = useSearchParams(); - const { status } = useSession(); + const { status, data } = useSession(); const callbackUrl = params.get('callbackUrl') ?? Routes.Auth.redirectToDashboard(); useEffect(() => { - if (status === 'loading') return; - if (status === 'authenticated') { - redirect(callbackUrl); + switch (status) { + case 'authenticated': + // This checks that the session is up to date with the necessary requirements + // it's useful when changing what's inside the cookie for instance + if (!data.accessToken) { + redirect(Routes.Auth.signOut(callbackUrl)); + break; + } + redirect(callbackUrl); + break; + case 'unauthenticated': + signIn('keycloak', { callbackUrl }); } - if (status === 'unauthenticated') { - signIn('keycloak', { callbackUrl }); - } - }, [status, callbackUrl]); + }, [status, callbackUrl, data]); return ( From 1ceb9d19608896b233354903600011bdd0a17c2b Mon Sep 17 00:00:00 2001 From: Benjamin Levesque <14175665+benjlevesque@users.noreply.github.com> Date: Fri, 15 Nov 2024 11:35:15 +0100 Subject: [PATCH 13/16] =?UTF-8?q?=F0=9F=90=9B=20Fix=20api=20redirection=20?= =?UTF-8?q?on=20401=20+=20wrong=20401/403=20status=20code?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ssr/src/utils/PageWithErrorHandling.tsx | 12 +++- .../applications/ssr/src/utils/apiAction.ts | 65 ++++--------------- .../ssr/src/utils/withErrorHandling.ts | 9 ++- 3 files changed, 28 insertions(+), 58 deletions(-) diff --git a/packages/applications/ssr/src/utils/PageWithErrorHandling.tsx b/packages/applications/ssr/src/utils/PageWithErrorHandling.tsx index e8c1382d11..03b57848d2 100644 --- a/packages/applications/ssr/src/utils/PageWithErrorHandling.tsx +++ b/packages/applications/ssr/src/utils/PageWithErrorHandling.tsx @@ -1,11 +1,14 @@ 'use server'; +import { redirect } from 'next/navigation'; + import { AggregateNotFoundError, DomainError, InvalidOperationError, OperationRejectedError, } from '@potentiel-domain/core'; +import { Routes } from '@potentiel-applications/routes'; import { CustomErrorPage } from '@/components/pages/custom-error/CustomError.page'; @@ -13,7 +16,8 @@ import { withErrorHandling } from './withErrorHandling'; export const PageWithErrorHandling = async ( render: () => Promise, -): Promise => withErrorHandling(render, renderDomainError, renderUnknownError); +): Promise => + withErrorHandling(render, renderDomainError, redirectOnAuthenticationError, renderUnknownError); const renderDomainError = (e: DomainError) => { if (e instanceof AggregateNotFoundError) { @@ -29,6 +33,10 @@ const renderDomainError = (e: DomainError) => { return <>; }; -const renderUnknownError = () => { +const renderUnknownError = (_: Error) => { return ; }; + +const redirectOnAuthenticationError = () => { + redirect(Routes.Auth.signIn()); +}; diff --git a/packages/applications/ssr/src/utils/apiAction.ts b/packages/applications/ssr/src/utils/apiAction.ts index e8f8d0ece4..65d792b392 100644 --- a/packages/applications/ssr/src/utils/apiAction.ts +++ b/packages/applications/ssr/src/utils/apiAction.ts @@ -1,3 +1,5 @@ +import { STATUS_CODES } from 'node:http'; + import { AggregateNotFoundError, DomainError, @@ -8,14 +10,14 @@ import { import { withErrorHandling } from './withErrorHandling'; export const apiAction = async (action: () => Promise) => - withErrorHandling(action, mapDomainError, mapTo500); + withErrorHandling(action, mapDomainError, mapTo401, mapTo500); const mapDomainError = (e: DomainError) => { if (e instanceof InvalidOperationError) { return mapTo400(e); } if (e instanceof OperationRejectedError) { - return mapTo401(); + return mapTo403(); } if (e instanceof AggregateNotFoundError) { return mapTo404(e); @@ -24,59 +26,20 @@ const mapDomainError = (e: DomainError) => { return mapTo500(); }; -const mapTo404 = (e: Error) => { - return Response.json( - { - message: e.message, - }, - { - status: 404, - statusText: 'Not Found', - headers: { - 'content-type': 'text/plain', - }, - }, - ); -}; - -const mapTo400 = (e: Error) => { - return Response.json( - { - message: e.message, - }, - { - status: 400, - statusText: 'Bad Request', - }, - ); -}; - -const mapTo401 = () => { - return Response.json( - { - message: 'Opération rejetée', - }, +const mapToHttpError = (status: number, message: string) => + Response.json( + { message }, { - status: 401, - statusText: 'Unauthorized', + status, + statusText: STATUS_CODES[status], headers: { 'content-type': 'text/plain', }, }, ); -}; -const mapTo500 = () => { - return Response.json( - { - message: 'Une erreur est survenue', - }, - { - status: 500, - statusText: 'Internal Server Error', - headers: { - 'content-type': 'text/plain', - }, - }, - ); -}; +const mapTo400 = (e: Error) => mapToHttpError(400, e.message); +const mapTo401 = () => mapToHttpError(401, "L'authentification a échoué"); +const mapTo403 = () => mapToHttpError(403, 'Opération rejetée'); +const mapTo404 = (e: Error) => mapToHttpError(404, e.message); +const mapTo500 = () => mapToHttpError(500, 'Une erreur est survenue'); diff --git a/packages/applications/ssr/src/utils/withErrorHandling.ts b/packages/applications/ssr/src/utils/withErrorHandling.ts index d527d9ddd4..280634d900 100644 --- a/packages/applications/ssr/src/utils/withErrorHandling.ts +++ b/packages/applications/ssr/src/utils/withErrorHandling.ts @@ -1,19 +1,18 @@ import { isNotFoundError } from 'next/dist/client/components/not-found'; import { isRedirectError } from 'next/dist/client/components/redirect'; -import { redirect } from 'next/navigation'; import { getLogger } from '@potentiel-libraries/monitoring'; import { DomainError } from '@potentiel-domain/core'; import { bootstrap } from '@potentiel-applications/bootstrap'; import { permissionMiddleware } from '@potentiel-domain/utilisateur'; -import { Routes } from '@potentiel-applications/routes'; import { NoAuthenticatedUserError } from './getAuthenticatedUser.handler'; export async function withErrorHandling( action: () => Promise, onDomainError: (error: DomainError) => TResult, - onUnknowmError: (error: Error) => TResult, + onAuthenticationError: () => TResult, + onUnknownError: (error: Error) => TResult, ): Promise { try { await bootstrap({ middlewares: [permissionMiddleware] }); @@ -24,7 +23,7 @@ export async function withErrorHandling( } if (e instanceof NoAuthenticatedUserError) { - redirect(Routes.Auth.signIn()); + return onAuthenticationError(); } if (e instanceof DomainError) { @@ -33,6 +32,6 @@ export async function withErrorHandling( } getLogger().error(e as Error); - return onUnknowmError(e as Error); + return onUnknownError(e as Error); } } From 39ee2eb1bb34809e6f605a7998a20e56d43cc8d5 Mon Sep 17 00:00:00 2001 From: Benjamin Levesque <14175665+benjlevesque@users.noreply.github.com> Date: Fri, 15 Nov 2024 17:16:24 +0100 Subject: [PATCH 14/16] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Stocker=20data=20uti?= =?UTF-8?q?lisateur=20dans=20le=20cookie,=20=C3=A0=20la=20place=20de=20l'a?= =?UTF-8?q?ccessToken?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- keycloak/import/realm-dev.json | 118 ++++++++++++++++++ .../keycloak/attachUserToRequestMiddleware.ts | 47 ++++--- .../legacy/src/types/express-custom.d.ts | 9 -- .../app/api/auth/federated-logout/route.ts | 53 ++++---- .../ssr/src/app/auth/signIn/page.tsx | 2 +- .../ssr/src/app/go-to-user-dashboard/route.ts | 13 +- packages/applications/ssr/src/auth.ts | 45 ------- .../applications/ssr/src/auth/authOptions.ts | 58 +++++++++ .../applications/ssr/src/auth/convertToken.ts | 67 ++++++++++ packages/applications/ssr/src/auth/index.ts | 2 + .../applications/ssr/src/types/next-auth.d.ts | 6 +- .../src/utils/getAuthenticatedUser.handler.ts | 38 ++---- .../utilisateur/src/groupe.valueType.ts | 2 +- .../utilisateur/src/utilisateur.valueType.ts | 76 ++--------- 14 files changed, 342 insertions(+), 194 deletions(-) delete mode 100644 packages/applications/legacy/src/types/express-custom.d.ts delete mode 100644 packages/applications/ssr/src/auth.ts create mode 100644 packages/applications/ssr/src/auth/authOptions.ts create mode 100644 packages/applications/ssr/src/auth/convertToken.ts create mode 100644 packages/applications/ssr/src/auth/index.ts diff --git a/keycloak/import/realm-dev.json b/keycloak/import/realm-dev.json index 369315ef64..60335cc55a 100644 --- a/keycloak/import/realm-dev.json +++ b/keycloak/import/realm-dev.json @@ -725,6 +725,22 @@ "realmRoles": ["default-roles-potentiel"], "notBefore": 0, "groups": ["/GestionnairesRéseau/17X100A100A0001A"] + }, + { + "id": "c16130dd-13be-4a22-8a50-293a2b3cdafc", + "username": "service-account-integration-grd-enedis", + "lastName": "Integration GRD Enedis", + "email": "integration-grd-enedis@clients", + "emailVerified": false, + "createdTimestamp": 1731675236457, + "enabled": true, + "totp": false, + "serviceAccountClientId": "integration-grd-enedis", + "disableableCredentialTypes": [], + "requiredActions": [], + "realmRoles": ["default-roles-potentiel"], + "notBefore": 0, + "groups": ["/GestionnairesRéseau/17X100A100A0001A"] } ], "scopeMappings": [ @@ -1162,6 +1178,108 @@ ], "defaultClientScopes": ["web-origins", "roles", "profile", "email"], "optionalClientScopes": ["address", "phone", "offline_access", "microprofile-jwt"] + }, + { + "id": "e6ef1124-c854-4c59-9089-5168c328560f", + "clientId": "integration-grd-enedis", + "name": "", + "description": "", + "rootUrl": "", + "adminUrl": "", + "baseUrl": "", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "secret": "aWLTypom26xaQEhRjWoraV6GJcuCQRMs", + "redirectUris": ["/*"], + "webOrigins": ["/*"], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": false, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": true, + "publicClient": false, + "frontchannelLogout": true, + "protocol": "openid-connect", + "attributes": { + "realm_client": "false", + "oidc.ciba.grant.enabled": "false", + "client.secret.creation.time": "1731675236", + "backchannel.logout.session.required": "true", + "oauth2.device.authorization.grant.enabled": "false", + "backchannel.logout.revoke.offline.tokens": "false" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": true, + "nodeReRegistrationTimeout": -1, + "protocolMappers": [ + { + "id": "8a989462-50db-44e0-b3f6-a9a9b67adb6e", + "name": "user-groups", + "protocol": "openid-connect", + "protocolMapper": "oidc-group-membership-mapper", + "consentRequired": false, + "config": { + "full.path": "true", + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "id.token.claim": "true", + "lightweight.claim": "false", + "access.token.claim": "true", + "claim.name": "groups" + } + }, + { + "id": "533de63a-8683-48ff-b2fc-6b6ea56cfa9c", + "name": "Client IP Address", + "protocol": "openid-connect", + "protocolMapper": "oidc-usersessionmodel-note-mapper", + "consentRequired": false, + "config": { + "user.session.note": "clientAddress", + "id.token.claim": "true", + "introspection.token.claim": "true", + "access.token.claim": "true", + "claim.name": "clientAddress", + "jsonType.label": "String" + } + }, + { + "id": "9c9e5ac6-5cd6-42ae-bc95-bdc1c06e04c4", + "name": "Client Host", + "protocol": "openid-connect", + "protocolMapper": "oidc-usersessionmodel-note-mapper", + "consentRequired": false, + "config": { + "user.session.note": "clientHost", + "id.token.claim": "true", + "introspection.token.claim": "true", + "access.token.claim": "true", + "claim.name": "clientHost", + "jsonType.label": "String" + } + }, + { + "id": "b4c7d993-3d5f-499c-8f5b-fea7be1567fb", + "name": "Client ID", + "protocol": "openid-connect", + "protocolMapper": "oidc-usersessionmodel-note-mapper", + "consentRequired": false, + "config": { + "user.session.note": "client_id", + "id.token.claim": "true", + "introspection.token.claim": "true", + "access.token.claim": "true", + "claim.name": "client_id", + "jsonType.label": "String" + } + } + ], + "defaultClientScopes": ["web-origins", "acr", "roles", "profile", "basic", "email"], + "optionalClientScopes": ["address", "phone", "offline_access", "microprofile-jwt"] } ], "clientScopes": [ diff --git a/packages/applications/legacy/src/infra/keycloak/attachUserToRequestMiddleware.ts b/packages/applications/legacy/src/infra/keycloak/attachUserToRequestMiddleware.ts index 1db1dbd980..c84b6a9c36 100644 --- a/packages/applications/legacy/src/infra/keycloak/attachUserToRequestMiddleware.ts +++ b/packages/applications/legacy/src/infra/keycloak/attachUserToRequestMiddleware.ts @@ -1,31 +1,44 @@ import { NextFunction, Request, Response } from 'express'; import { logger, ResultAsync } from '../../core/utils'; -import { CreateUser, GetUserByEmail, USER_ROLES } from '../../modules/users'; -import { getPermissions } from '../../modules/authN'; -import { Utilisateur } from '@potentiel-domain/utilisateur'; +import { CreateUser, GetUserByEmail } from '../../modules/users'; +import { getPermissions, Permission } from '../../modules/authN'; +import { Role, Utilisateur } from '@potentiel-domain/utilisateur'; import { getToken, GetTokenParams } from 'next-auth/jwt'; -import NextAuthJwt from 'next-auth/jwt'; +import { PlainType } from '@potentiel-domain/core'; type AttachUserToRequestMiddlewareDependencies = { getUserByEmail: GetUserByEmail; createUser: CreateUser; - getAccessToken?: (req: Request) => Promise; + getUtilisateur?: (req: Request) => Promise; }; // The token is generated by next-auth on the SSR app declare module 'next-auth/jwt' { - interface JWT extends NextAuthJwt.DefaultJWT { + interface JWT { idToken?: string; - accessToken: string; + utilisateur?: PlainType; } } -const getNextAuthAccessToken = async (req: Request) => { +declare module 'express-serve-static-core' { + interface Request { + user: { + email: string; + role: Role.RawType; + fullName: string; + id: string; + accountUrl: string; + permissions: Permission[]; + }; + } +} + +const getNextAuthUtilisateur = async (req: Request) => { try { const token = await getToken({ req: { cookies: req.cookies } as unknown as GetTokenParams['req'], }); - return token?.accessToken; + return token?.utilisateur && Utilisateur.bind(token.utilisateur); } catch (e) { logger.error('getToken failed'); logger.error(e); @@ -39,7 +52,7 @@ const makeAttachUserToRequestMiddleware = ({ getUserByEmail, createUser, - getAccessToken = getNextAuthAccessToken, + getUtilisateur = getNextAuthUtilisateur, }: AttachUserToRequestMiddlewareDependencies) => async (request: Request, response: Response, next: NextFunction) => { if ( @@ -55,18 +68,18 @@ const makeAttachUserToRequestMiddleware = return; } - const token = await getAccessToken(request); - - if (!token) { - next(); - return; - } try { + const utilisateur = await getUtilisateur(request); + if (!utilisateur) { + next(); + return; + } + const { identifiantUtilisateur: { email }, role: { nom: role }, nom: fullName, - } = Utilisateur.convertirEnValueType(token); + } = utilisateur; const getOrCreateUser = async () => { const createUserArgs = { email, role, fullName }; diff --git a/packages/applications/legacy/src/types/express-custom.d.ts b/packages/applications/legacy/src/types/express-custom.d.ts deleted file mode 100644 index 775539d278..0000000000 --- a/packages/applications/legacy/src/types/express-custom.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { UtilisateurReadModel } from '../modules/utilisateur/récupérer/UtilisateurReadModel'; -import { JWT } from 'next-auth/jwt'; -declare module 'express-serve-static-core' { - // eslint-disable-next-line - interface Request { - user: UtilisateurReadModel; - token: JWT; - } -} diff --git a/packages/applications/ssr/src/app/api/auth/federated-logout/route.ts b/packages/applications/ssr/src/app/api/auth/federated-logout/route.ts index 88aa2a8c82..0d93d97874 100644 --- a/packages/applications/ssr/src/app/api/auth/federated-logout/route.ts +++ b/packages/applications/ssr/src/app/api/auth/federated-logout/route.ts @@ -11,33 +11,38 @@ import { issuerUrl } from '@/auth'; * Without this, logging out of the app only removes cookies, but the user is still logged to SSO * @see https://github.com/nextauthjs/next-auth/discussions/3938 */ -export async function GET() { +export const GET = async () => { const { NEXTAUTH_URL = '' } = process.env; + const redirectUrl = new URL(Routes.Auth.signOut(), NEXTAUTH_URL); - // Gets the session, with idToken - const session = await getServerSession({ - callbacks: { - session({ session, token }) { - session.idToken = token.idToken; - return session; + try { + // Gets the session, with idToken + const session = await getServerSession({ + callbacks: { + session({ session, token }) { + session.idToken = token.idToken; + return session; + }, }, - }, - }); - if (!session) { - return NextResponse.redirect(NEXTAUTH_URL); - } + }); + if (!session) { + return NextResponse.redirect(NEXTAUTH_URL); + } - // after keycloak logout, redirect the user to this route to remove the session - const redirectUrl = new URL(Routes.Auth.signOut(), NEXTAUTH_URL); - const ssoLogoutUrl = new URL(`${issuerUrl}/protocol/openid-connect/logout`); + // after keycloak logout, redirect the user to this route to remove the session + const ssoLogoutUrl = new URL(`${issuerUrl}/protocol/openid-connect/logout`); - if (session.idToken) { - // without this, Keycloak prompts the user for confirmation - ssoLogoutUrl.searchParams.set('post_logout_redirect_uri', redirectUrl.toString()); - ssoLogoutUrl.searchParams.set('id_token_hint', session.idToken); - return NextResponse.redirect(ssoLogoutUrl.toString()); - } + if (session.idToken) { + // without this, Keycloak prompts the user for confirmation + ssoLogoutUrl.searchParams.set('post_logout_redirect_uri', redirectUrl.toString()); + ssoLogoutUrl.searchParams.set('id_token_hint', session.idToken); + return NextResponse.redirect(ssoLogoutUrl.toString()); + } - getLogger().warn('A user logged out without an id token, the keycloak session is still active'); - return NextResponse.redirect(redirectUrl, { status: 302 }); -} + getLogger().warn('A user logged out without an id token, the keycloak session is still active'); + return NextResponse.redirect(redirectUrl); + } catch (e) { + getLogger().error(new Error('Logout error', { cause: e })); + return NextResponse.redirect(redirectUrl); + } +}; diff --git a/packages/applications/ssr/src/app/auth/signIn/page.tsx b/packages/applications/ssr/src/app/auth/signIn/page.tsx index 1bac710ee9..36255bb53c 100644 --- a/packages/applications/ssr/src/app/auth/signIn/page.tsx +++ b/packages/applications/ssr/src/app/auth/signIn/page.tsx @@ -18,7 +18,7 @@ export default function SignIn() { case 'authenticated': // This checks that the session is up to date with the necessary requirements // it's useful when changing what's inside the cookie for instance - if (!data.accessToken) { + if (!data.utilisateur) { redirect(Routes.Auth.signOut(callbackUrl)); break; } diff --git a/packages/applications/ssr/src/app/go-to-user-dashboard/route.ts b/packages/applications/ssr/src/app/go-to-user-dashboard/route.ts index d97c6b8841..3627352528 100644 --- a/packages/applications/ssr/src/app/go-to-user-dashboard/route.ts +++ b/packages/applications/ssr/src/app/go-to-user-dashboard/route.ts @@ -3,12 +3,15 @@ import { redirect } from 'next/navigation'; import { Routes } from '@potentiel-applications/routes'; import { Role } from '@potentiel-domain/utilisateur'; -import { withUtilisateur } from '@/utils/withUtilisateur'; +import { getOptionalAuthenticatedUser } from '@/utils/getAuthenticatedUser.handler'; -export const GET = async () => - withUtilisateur(async ({ role }) => { - const redirectTo = role.estÉgaleÀ(Role.grd) +export const GET = async () => { + const utilisateur = await getOptionalAuthenticatedUser(); + if (utilisateur) { + const redirectTo = utilisateur.role.estÉgaleÀ(Role.grd) ? Routes.Raccordement.lister : Routes.Projet.lister(); redirect(redirectTo); - }); + } + redirect(Routes.Auth.signIn()); +}; diff --git a/packages/applications/ssr/src/auth.ts b/packages/applications/ssr/src/auth.ts deleted file mode 100644 index 60e99860bd..0000000000 --- a/packages/applications/ssr/src/auth.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { AuthOptions } from 'next-auth'; -import KeycloakProvider from 'next-auth/providers/keycloak'; - -const FIFTEEN_MINUTES = 15 * 60; -const ONE_DAY = 24 * 60 * 60; - -export const issuerUrl = `${process.env.KEYCLOAK_SERVER}/realms/${process.env.KEYCLOAK_REALM}`; - -export const authOptions: AuthOptions = { - providers: [ - KeycloakProvider({ - clientId: process.env.KEYCLOAK_USER_CLIENT_ID ?? '', - clientSecret: process.env.KEYCLOAK_USER_CLIENT_SECRET ?? '', - issuer: issuerUrl, - idToken: true, - profile(profile) { - return { - id: profile.sub, - name: profile.name ?? profile.preferred_username, - email: profile.email, - image: profile.picture, - }; - }, - }), - ], - session: { - strategy: 'jwt', - maxAge: ONE_DAY, - updateAge: FIFTEEN_MINUTES, - }, - callbacks: { - // Stores accessToken and idToken to the auth cookie - // accessToken is used to get user information - // idToken is necessary to logout of keycloak - jwt({ token, account }) { - if (account?.access_token) { - token.accessToken = account.access_token; - } - if (account?.id_token) { - token.idToken = account.id_token; - } - return token; - }, - }, -}; diff --git a/packages/applications/ssr/src/auth/authOptions.ts b/packages/applications/ssr/src/auth/authOptions.ts new file mode 100644 index 0000000000..23b4044b12 --- /dev/null +++ b/packages/applications/ssr/src/auth/authOptions.ts @@ -0,0 +1,58 @@ +import { AuthOptions } from 'next-auth'; +import KeycloakProvider from 'next-auth/providers/keycloak'; + +import { getLogger } from '@potentiel-libraries/monitoring'; + +import { convertToken } from './convertToken'; + +const ONE_HOUR = 60 * 60; + +export const issuerUrl = `${process.env.KEYCLOAK_SERVER}/realms/${process.env.KEYCLOAK_REALM}`; + +export const authOptions: AuthOptions = { + providers: [ + KeycloakProvider({ + clientId: process.env.KEYCLOAK_USER_CLIENT_ID ?? '', + clientSecret: process.env.KEYCLOAK_USER_CLIENT_SECRET ?? '', + issuer: issuerUrl, + }), + ], + session: { + strategy: 'jwt', + // This is the max age for the next-auth cookie + // It is renewed on each page refresh, so this represents inactivity time. + // Moreover, the user will not be disconnected after expiration (if their Keycloak session still exists), + // but there will be a redirection to keycloak. + maxAge: ONE_HOUR, + }, + callbacks: { + // Stores user data and idToken to the next-auth cookie + jwt({ token, account }) { + // NB `account` is defined only at login + if (account?.access_token) { + try { + const utilisateur = convertToken(account.access_token); + token.utilisateur = utilisateur; + } catch (e) { + getLogger('Auth').error( + new Error("Impossible de convertir l'accessToken en Utilisateur", { cause: e }), + ); + return token; + } + } + // Stores the id token as it is required to logout of Keycloak + if (account?.id_token) { + token.idToken = account.id_token; + } + return token; + }, + session({ session, token }) { + { + if (token.utilisateur) { + session.utilisateur = token.utilisateur; + } + return session; + } + }, + }, +}; diff --git a/packages/applications/ssr/src/auth/convertToken.ts b/packages/applications/ssr/src/auth/convertToken.ts new file mode 100644 index 0000000000..1f1857d091 --- /dev/null +++ b/packages/applications/ssr/src/auth/convertToken.ts @@ -0,0 +1,67 @@ +import { decodeJwt } from 'jose'; +import { z } from 'zod'; + +import { OperationRejectedError, PlainType } from '@potentiel-domain/core'; +import { Role, Groupe, IdentifiantUtilisateur, Utilisateur } from '@potentiel-domain/utilisateur'; +import { Option } from '@potentiel-libraries/monads'; + +export const convertToken = (token: string): PlainType => { + const { email, nom, roles, groupes } = parseToken(token); + + const role = roles.find((r) => Role.estUnRoleValide(r)); + const groupe = groupes.find((g) => Groupe.estUnGroupeValide(g)); + + return { + role: Role.convertirEnValueType(role ?? ''), + groupe: groupe ? Groupe.convertirEnValueType(groupe) : Option.none, + nom, + identifiantUtilisateur: IdentifiantUtilisateur.convertirEnValueType(email), + }; +}; + +const jwtSchema = z.object({ + name: z.string(), + email: z.string(), + realm_access: z.object({ + roles: z.array(z.string()), + }), + groups: z.array(z.string()).optional(), +}); + +const parseToken = (token: string) => { + try { + if (!token) { + throw new EmptyTokenError(); + } + + const decodedJwt = decodeJwt(token); + const { + name, + email, + realm_access: { roles }, + groups, + } = jwtSchema.parse(decodedJwt); + + return { + nom: name, + email, + roles, + groupes: groups ?? [], + }; + } catch (e) { + throw new TokenInvalideError(e as Error); + } +}; + +class TokenInvalideError extends OperationRejectedError { + constructor(cause: Error) { + super(`Le format du token utilisateur n'est pas valide.`); + this.cause = cause; + } +} + +class EmptyTokenError extends Error { + constructor() { + super(`Token vide`); + } +} diff --git a/packages/applications/ssr/src/auth/index.ts b/packages/applications/ssr/src/auth/index.ts new file mode 100644 index 0000000000..ca160157ab --- /dev/null +++ b/packages/applications/ssr/src/auth/index.ts @@ -0,0 +1,2 @@ +export { authOptions, issuerUrl } from './authOptions'; +export { convertToken } from './convertToken'; diff --git a/packages/applications/ssr/src/types/next-auth.d.ts b/packages/applications/ssr/src/types/next-auth.d.ts index 7d19f37a5f..13f0110e13 100644 --- a/packages/applications/ssr/src/types/next-auth.d.ts +++ b/packages/applications/ssr/src/types/next-auth.d.ts @@ -1,15 +1,17 @@ import NextAuthJwt from 'next-auth/jwt'; +import { Utilisateur } from '@potentiel-domain/utilisateur'; + declare module 'next-auth/jwt' { interface JWT extends NextAuthJwt.JWT { idToken?: string; - accessToken?: string; + utilisateur?: PlainType; } } declare module 'next-auth' { interface Session { idToken?: string; - accessToken?: string; + utilisateur?: PlainType; } } diff --git a/packages/applications/ssr/src/utils/getAuthenticatedUser.handler.ts b/packages/applications/ssr/src/utils/getAuthenticatedUser.handler.ts index d7312ac33e..f64d35273b 100644 --- a/packages/applications/ssr/src/utils/getAuthenticatedUser.handler.ts +++ b/packages/applications/ssr/src/utils/getAuthenticatedUser.handler.ts @@ -1,43 +1,29 @@ import { Message, MessageHandler } from 'mediateur'; -import { cookies, headers } from 'next/headers'; -import { getToken, GetTokenParams } from 'next-auth/jwt'; +import { getServerSession } from 'next-auth'; +import { headers } from 'next/headers'; // import * as Sentry from '@sentry/nextjs'; import { Utilisateur } from '@potentiel-domain/utilisateur'; +import { authOptions, convertToken } from '@/auth'; + export type GetAuthenticatedUserMessage = Message< 'System.Authorization.RécupérerUtilisateur', {}, Utilisateur.ValueType >; -/** - * Check for an access token - * - in the session (encrypted) - * - in Authorization header (clear) - **/ -const getAccessToken = async () => { - const token = await getToken({ - req: { - cookies: cookies(), - // NB: getToken peut également récupérer le token dans le header Authorization - // mais elle attend un token chiffré, ce qui n'est pas le cas dans le cadre de l'authentification API - // headers: headers() - } as unknown as GetTokenParams['req'], - }); - if (token) { - return token.accessToken; - } - const authorizationHeader = headers().get('authorization'); - return authorizationHeader?.replace(/Bearer /, ''); -}; - export const getOptionalAuthenticatedUser = async (): Promise< Utilisateur.ValueType | undefined > => { - const accessToken = await getAccessToken(); - if (accessToken) { - return Utilisateur.convertirEnValueType(accessToken); + const session = await getServerSession(authOptions); + if (session?.utilisateur) { + return Utilisateur.bind(session.utilisateur); + } + const authorizationHeader = headers().get('Authorization'); + if (authorizationHeader && authorizationHeader.toLowerCase().startsWith('bearer ')) { + const utilisateur = convertToken(authorizationHeader.slice(7)); + return utilisateur && Utilisateur.bind(utilisateur); } }; diff --git a/packages/domain/utilisateur/src/groupe.valueType.ts b/packages/domain/utilisateur/src/groupe.valueType.ts index 867c436229..3af1d776b2 100644 --- a/packages/domain/utilisateur/src/groupe.valueType.ts +++ b/packages/domain/utilisateur/src/groupe.valueType.ts @@ -30,7 +30,7 @@ export const convertirEnValueType = (value: string): ValueType => { }; export const bind = ({ nom, type }: PlainType) => { - return convertirEnValueType(`${type}/${nom}`); + return convertirEnValueType(`/${type}/${nom}`); }; export const estUnGroupeValide = (value: string) => { diff --git a/packages/domain/utilisateur/src/utilisateur.valueType.ts b/packages/domain/utilisateur/src/utilisateur.valueType.ts index 817b371e46..9322dc8302 100644 --- a/packages/domain/utilisateur/src/utilisateur.valueType.ts +++ b/packages/domain/utilisateur/src/utilisateur.valueType.ts @@ -1,4 +1,4 @@ -import { OperationRejectedError, ReadonlyValueType } from '@potentiel-domain/core'; +import { PlainType, ReadonlyValueType } from '@potentiel-domain/core'; import { Option } from '@potentiel-libraries/monads'; import * as Role from './role.valueType'; @@ -12,16 +12,21 @@ export type ValueType = ReadonlyValueType<{ groupe: Option.Type; }>; -export const convertirEnValueType = (value: string): ValueType => { - const { nom, identifiantUtilisateur, role, groupe } = convertToken(value); +export const bind = ({ + nom, + identifiantUtilisateur, + groupe, + role, +}: PlainType): ValueType => { + const _identifiantUtilisateur = IdentifiantUtilisateur.bind(identifiantUtilisateur); return { nom, - identifiantUtilisateur, - role, - groupe, + role: Role.bind(role), + identifiantUtilisateur: _identifiantUtilisateur, + groupe: Option.isSome(groupe) ? Groupe.bind(groupe) : Option.none, estÉgaleÀ(valueType) { return this.nom === valueType.nom && - this.identifiantUtilisateur.estÉgaleÀ(identifiantUtilisateur) && + this.identifiantUtilisateur.estÉgaleÀ(_identifiantUtilisateur) && this.role.estÉgaleÀ(valueType.role) && Option.isSome(this.groupe) ? Option.isSome(valueType.groupe) && this.groupe.estÉgaleÀ(valueType.groupe) @@ -29,60 +34,3 @@ export const convertirEnValueType = (value: string): ValueType => { }, }; }; - -const convertToken = (token: string) => { - const { email, nom, roles, groupes } = parseToken(token); - - const role = roles.find((r) => Role.estUnRoleValide(r)); - const groupe = groupes.find((g) => Groupe.estUnGroupeValide(g)); - - return { - role: Role.convertirEnValueType(role ?? ''), - groupe: groupe ? Groupe.convertirEnValueType(groupe) : Option.none, - nom, - identifiantUtilisateur: IdentifiantUtilisateur.convertirEnValueType(email), - }; -}; - -const parseToken = (token: string) => { - try { - if (!token) { - throw new EmptyTokenError(); - } - const { - name, - email, - realm_access: { roles }, - groups, - } = JSON.parse(Buffer.from(token.split('.')[1], 'base64').toString()) as { - name: string; - email: string; - realm_access: { - roles: Array; - }; - groups: string[]; - }; - - return { - nom: name, - email, - roles, - groupes: groups ?? [], - }; - } catch (e) { - throw new TokenInvalideError(e as Error); - } -}; - -class TokenInvalideError extends OperationRejectedError { - constructor(cause: Error) { - super(`Le format du token utilisateur n'est pas valide.`); - this.cause = cause; - } -} - -class EmptyTokenError extends Error { - constructor() { - super(`Token vide`); - } -} From 016c1cc7604da94c454c8f7f4ebb7da4f5d22063 Mon Sep 17 00:00:00 2001 From: Benjamin Levesque <14175665+benjlevesque@users.noreply.github.com> Date: Fri, 15 Nov 2024 17:24:40 +0100 Subject: [PATCH 15/16] =?UTF-8?q?=F0=9F=8F=B7=EF=B8=8F=20Fix=20type?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/applications/ssr/src/types/next-auth.d.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/applications/ssr/src/types/next-auth.d.ts b/packages/applications/ssr/src/types/next-auth.d.ts index 13f0110e13..ebb129ebd6 100644 --- a/packages/applications/ssr/src/types/next-auth.d.ts +++ b/packages/applications/ssr/src/types/next-auth.d.ts @@ -1,9 +1,8 @@ -import NextAuthJwt from 'next-auth/jwt'; - import { Utilisateur } from '@potentiel-domain/utilisateur'; +import { PlainType } from '@potentiel-domain/core'; declare module 'next-auth/jwt' { - interface JWT extends NextAuthJwt.JWT { + interface JWT { idToken?: string; utilisateur?: PlainType; } From 6dd595d4889913366d54d2f14a031a347dd36425 Mon Sep 17 00:00:00 2001 From: Benjamin Levesque <14175665+benjlevesque@users.noreply.github.com> Date: Fri, 15 Nov 2024 17:56:07 +0100 Subject: [PATCH 16/16] =?UTF-8?q?=F0=9F=90=9B=20Fixes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../v\303\251rifierPermissionUtilisateur.ts" | 1 - .../attachUserToRequestMiddleware.spec.ts | 53 +++++++------------ .../app/api/auth/federated-logout/route.ts | 3 ++ 3 files changed, 21 insertions(+), 36 deletions(-) diff --git "a/packages/applications/legacy/src/controllers/helpers/v\303\251rifierPermissionUtilisateur.ts" "b/packages/applications/legacy/src/controllers/helpers/v\303\251rifierPermissionUtilisateur.ts" index 09b79cd146..738f7b9ccb 100644 --- "a/packages/applications/legacy/src/controllers/helpers/v\303\251rifierPermissionUtilisateur.ts" +++ "b/packages/applications/legacy/src/controllers/helpers/v\303\251rifierPermissionUtilisateur.ts" @@ -3,7 +3,6 @@ import { Permission } from '../../modules/authN'; import { AccèsNonAutoriséPage } from '../../views'; import { Routes } from '@potentiel-applications/routes'; -// TODO export const vérifierPermissionUtilisateur = (permission: Permission): RequestHandler => (request, response, next) => { diff --git a/packages/applications/legacy/src/infra/keycloak/attachUserToRequestMiddleware.spec.ts b/packages/applications/legacy/src/infra/keycloak/attachUserToRequestMiddleware.spec.ts index 9ff9ab6e8c..e647174c80 100644 --- a/packages/applications/legacy/src/infra/keycloak/attachUserToRequestMiddleware.spec.ts +++ b/packages/applications/legacy/src/infra/keycloak/attachUserToRequestMiddleware.spec.ts @@ -5,6 +5,8 @@ import { User } from '../../entities'; import { GetUserByEmail, UserRole } from '../../modules/users'; import { makeFakeCreateUser } from '../../__tests__/fakes'; import { makeAttachUserToRequestMiddleware } from './attachUserToRequestMiddleware'; +import { IdentifiantUtilisateur, Role, Utilisateur } from '@potentiel-domain/utilisateur'; +import { Option } from '@potentiel-libraries/monads'; describe(`attachUserToRequestMiddleware`, () => { const staticPaths = ['/fonts', '/css', '/images', '/scripts', '/main']; @@ -14,18 +16,18 @@ describe(`attachUserToRequestMiddleware`, () => { path, } as express.Request; const nextFunction = jest.fn(); - const getAccessToken = jest.fn(() => Promise.resolve(undefined)); + const getUtilisateur = jest.fn(() => Promise.resolve(undefined)); const middleware = makeAttachUserToRequestMiddleware({ getUserByEmail: jest.fn(), createUser: makeFakeCreateUser(), - getAccessToken, + getUtilisateur, }); middleware(request, {} as express.Response, nextFunction); it('should not attach the user to the request and execute the next function', () => { expect(request.user).toBeUndefined(); - expect(getAccessToken).not.toHaveBeenCalled(); + expect(getUtilisateur).not.toHaveBeenCalled(); expect(nextFunction).toHaveBeenCalled(); }); }); @@ -33,38 +35,19 @@ describe(`attachUserToRequestMiddleware`, () => { describe(`when the path is not a static one`, () => { const request = { path: '/a-protected-path' } as express.Request; - const makeFakeGetAccessToken = (role: string, username: string) => async () => { - const iat = Math.floor(Date.now() / 1000); - const claims = { - exp: iat, - iat: iat + 300, - auth_time: iat, - jti: 'jti', - iss: 'http://localhost:8080/realms/Potentiel', - aud: ['realm-management', 'account'], - sub: 'sub', - typ: 'Bearer', - azp: 'potentiel-web', - sid: 'sid', - acr: '0', - realm_access: { - roles: [role], - }, - scope: 'openid email profile', - email_verified: true, - name: 'Admin Test', - preferred_username: username, - given_name: 'Admin', - family_name: 'Test', - email: username, - }; - return `xx.${btoa(JSON.stringify(claims))}.xx`; + const makeFakeGetUtilisateur = (role: string, username: string) => async () => { + return Utilisateur.bind({ + groupe: Option.none, + identifiantUtilisateur: IdentifiantUtilisateur.convertirEnValueType(username), + nom: '', + role: Role.convertirEnValueType(role), + }); }; describe(`when there is no user email in the keycloak access token`, () => { const middleware = makeAttachUserToRequestMiddleware({ getUserByEmail: jest.fn(), createUser: makeFakeCreateUser(), - getAccessToken: makeFakeGetAccessToken('admin', ''), + getUtilisateur: makeFakeGetUtilisateur('admin', ''), }); it('should not attach the user to the request and execute the next function', async () => { @@ -95,10 +78,10 @@ describe(`attachUserToRequestMiddleware`, () => { const middleware = makeAttachUserToRequestMiddleware({ getUserByEmail, createUser: makeFakeCreateUser(), - getAccessToken: makeFakeGetAccessToken('', userEmail), + getUtilisateur: makeFakeGetUtilisateur('', userEmail), }); - it('should not attach the user to the request', async () => { + it('should not attach the user to the request and execute the next function', async () => { await middleware(request, {} as express.Response, nextFunction); expect(request.user).toBeUndefined(); expect(nextFunction).toHaveBeenCalled(); @@ -125,7 +108,7 @@ describe(`attachUserToRequestMiddleware`, () => { const middleware = makeAttachUserToRequestMiddleware({ getUserByEmail, createUser: makeFakeCreateUser(), - getAccessToken: makeFakeGetAccessToken(tokenUserRole, userEmail), + getUtilisateur: makeFakeGetUtilisateur(tokenUserRole, userEmail), }); it('should attach the user to the request with role from token', async () => { @@ -156,7 +139,7 @@ describe(`attachUserToRequestMiddleware`, () => { const middleware = makeAttachUserToRequestMiddleware({ getUserByEmail, createUser, - getAccessToken: makeFakeGetAccessToken('porteur-projet', userEmail), + getUtilisateur: makeFakeGetUtilisateur('porteur-projet', userEmail), }); it('should attach a new user to the request', async () => { @@ -188,7 +171,7 @@ describe(`attachUserToRequestMiddleware`, () => { const middleware = makeAttachUserToRequestMiddleware({ getUserByEmail, createUser, - getAccessToken: makeFakeGetAccessToken(userRole, userEmail), + getUtilisateur: makeFakeGetUtilisateur(userRole, userEmail), }); it('should attach a new user to the request with the same role of the token', async () => { diff --git a/packages/applications/ssr/src/app/api/auth/federated-logout/route.ts b/packages/applications/ssr/src/app/api/auth/federated-logout/route.ts index 0d93d97874..1ad9ca1d74 100644 --- a/packages/applications/ssr/src/app/api/auth/federated-logout/route.ts +++ b/packages/applications/ssr/src/app/api/auth/federated-logout/route.ts @@ -46,3 +46,6 @@ export const GET = async () => { return NextResponse.redirect(redirectUrl); } }; + +// forces the route handler to be dynamic +export const dynamic = 'force-dynamic';