Skip to content

Commit

Permalink
feat(user): add certification routes
Browse files Browse the repository at this point in the history
  • Loading branch information
douglasduteil committed Feb 4, 2025
1 parent 0799802 commit 57e17b3
Show file tree
Hide file tree
Showing 12 changed files with 335 additions and 2 deletions.
1 change: 1 addition & 0 deletions cypress/e2e/signin_with_certification_dirigeant/env.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
DO_NOT_SEND_MAIL="True"
36 changes: 36 additions & 0 deletions cypress/e2e/signin_with_certification_dirigeant/fixtures.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
INSERT INTO users
(id, email, email_verified, email_verified_at, encrypted_password, created_at, updated_at,
given_name, family_name, phone_number, job, encrypted_totp_key, totp_key_verified_at, force_2fa)
VALUES
(1, '[email protected]', true, CURRENT_TIMESTAMP,
'$2a$10$kzY3LINL6..50Fy9shWCcuNlRfYq0ft5lS.KCcJ5PzrhlWfKK4NIO', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP,
'Jean', 'Certification', '0123456789', 'Dirigeant',
null, null, false);

INSERT INTO organizations
(id, siret, created_at, updated_at)
VALUES
(1, '21340126800130', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP);

INSERT INTO users_organizations
(user_id, organization_id, is_external, verification_type, has_been_greeted)
VALUES
(1, 1, false, 'domain', true);

INSERT INTO oidc_clients
(client_name, client_id, client_secret, redirect_uris,
post_logout_redirect_uris, scope, client_uri, client_description,
userinfo_signed_response_alg, id_token_signed_response_alg,
authorization_signed_response_alg, introspection_signed_response_alg)
VALUES
('Oidc Test Client',
'standard_client_id',
'standard_client_secret',
ARRAY [
'http://localhost:4000/login-callback'
],
ARRAY []::varchar[],
'openid email profile organization',
'http://localhost:4000/',
'ProConnect test client. More info: https://github.com/numerique-gouv/proconnect-test-client.',
null, null, null, null);
33 changes: 33 additions & 0 deletions cypress/e2e/signin_with_certification_dirigeant/index.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
describe("sign-in with a client requiring certification dirigeant", () => {
beforeEach(() => {
cy.visit("http://localhost:4000");
cy.setRequestedAcrs([
"https://proconnect.gouv.fr/assurance/certification-dirigeant",
]);
});

it("should sign-in an return the right acr value", function () {
cy.get("button#custom-connection").click({ force: true });

cy.login("[email protected]");

cy.contains("Authentifier votre statut");
cy.contains("S’identifier avec").click();

cy.contains("Vous allez vous connecter en tant que ");
cy.contains("Jacintha Froment");
cy.contains("Continuer").click();
cy.contains("Continuer").click();
cy.contains(
"vous devez accepter la transmission de vos données FranceConnect",
);
cy.contains(
"J'accepte que FranceConnect transmette mes données au service pour me connecter",
).click();
cy.contains("Continuer").click();

cy.contains(
'"acr": "https://proconnect.gouv.fr/assurance/certification-dirigeant"',
);
});
});
5 changes: 5 additions & 0 deletions src/config/notification-messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,11 @@ Si vous avez oublié votre mot de passe cliquez sur « Mot de passe oublié ?
type: "success",
description: "L’application d’authentification a été modifiée.",
},
certification_franceconnect_data_transmission_agreement_required: {
type: "error",
description:
"Erreur : vous devez accepter la transmission de vos données FranceConnect pour permettre la certification dirigeante.",
},
"2fa_successfully_enabled": {
type: "success",
description: "La validation en deux étapes a bien été activée.",
Expand Down
107 changes: 107 additions & 0 deletions src/controllers/user/certification-dirigeant.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
//

import type { NextFunction, Request, Response } from "express";
import { z } from "zod";
import { csrfToken } from "../../middlewares/csrf-protection";
import getNotificationsFromRequest from "../../services/get-notifications-from-request";

//

export async function getCertificationDirigeantController(
req: Request,
res: Response,
next: NextFunction,
) {
try {
return res.render("user/certification-dirigeant", {
csrfToken: csrfToken(req),
pageTitle: "Certification dirigeant",
});
} catch (error) {
next(error);
}
}

export async function postCertificationDirigeantController(
_req: Request,
res: Response,
next: NextFunction,
) {
try {
return res.redirect("/users/certification-dirigeant/login-as");
} catch (error) {
next(error);
}
}

//

export async function getCertificationDirigeantLoginAsController(
req: Request,
res: Response,
next: NextFunction,
) {
try {
return res.render("user/certification-dirigeant-login-as", {
csrfToken: csrfToken(req),
notifications: await getNotificationsFromRequest(req),
pageTitle: "Se connecter en tant que",
});
} catch (error) {
next(error);
}
}

export async function postCertificationDirigeantLoginAsController(
req: Request,
res: Response,
next: NextFunction,
) {
try {
const schema = z.object({
agreement: z.literal("on").optional(),
});

const { agreement } = await schema.parseAsync(req.body);

if (agreement !== "on") {
return res.redirect(
"/users/certification-dirigeant/login-as?notification=certification_franceconnect_data_transmission_agreement_required",
);
}

console.log({ agreement });
req.session.__user_certified = true;
// return res.redirect("/users/sign-in");
} catch (error) {
next(error);
}
}

//

export async function getCertificationDirigeantRepresentingController(
req: Request,
res: Response,
next: NextFunction,
) {
try {
const userOrganizations = [
{
id: "1",
siret: "12345678901234",
cached_libelle: "Organisation 1",
cached_adresse: "123 rue de la paix",
cached_libelle_activite_principale: "Activité principale 1",
},
];
return res.render("user/select-organization", {
csrfToken: csrfToken(req),
illustration: "illu-password.svg",
pageTitle: "Choisir une organisation",
userOrganizations,
});
} catch (error) {
next(error);
}
}
10 changes: 9 additions & 1 deletion src/managers/session/authenticated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -244,5 +244,13 @@ export const isIdentityConsistencyChecked = async (req: Request) => {
throw new Error("link should be set");
}

return link?.verification_type !== null;
return [
"code_sent_to_official_contact_email",
"domain",
"imported_from_inclusion_connect",
"imported_from_coop_mediation_numerique",
"in_liste_dirigeants_rna",
"official_contact_email",
"bypassed",
].includes(link?.verification_type ?? "");
};
28 changes: 27 additions & 1 deletion src/middlewares/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -227,12 +227,38 @@ export const checkUserIsVerifiedMiddleware = (
}
});

export const checkUserHasPersonalInformationsMiddleware = (
export const checkUserNeedCertificationDirigeantMiddleware = (
req: Request,
res: Response,
next: NextFunction,
) =>
checkUserIsVerifiedMiddleware(req, res, async (error) => {
try {
if (error) return next(error);

const isRequested = req.session.certificationDirigeantRequested;
const isAlreadyCertified = req.session.__user_certified;

console.log("Loooooooooool");
console.log({ isRequested, isAlreadyCertified });
console.trace();

if (isAlreadyCertified) return next();

if (isRequested) return res.redirect("/users/certification-dirigeant");

return next();
} catch (error) {
next(error);
}
});

export const checkUserHasPersonalInformationsMiddleware = (
req: Request,
res: Response,
next: NextFunction,
) =>
checkUserNeedCertificationDirigeantMiddleware(req, res, async (error) => {
try {
if (error) return next(error);

Expand Down
44 changes: 44 additions & 0 deletions src/routers/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,13 @@ import {
} from "../controllers/organization";
import { postSignInWithAuthenticatorAppController } from "../controllers/totp";
import { get2faSignInController } from "../controllers/user/2fa-sign-in";
import {
getCertificationDirigeantController,
getCertificationDirigeantLoginAsController,
getCertificationDirigeantRepresentingController,
postCertificationDirigeantController,
postCertificationDirigeantLoginAsController,
} from "../controllers/user/certification-dirigeant";
import { postDeleteUserController } from "../controllers/user/delete";
import { postCancelModerationAndRedirectControllerFactory } from "../controllers/user/edit-moderation";
import { issueSessionOrRedirectController } from "../controllers/user/issue-session-or-redirect";
Expand Down Expand Up @@ -418,6 +425,43 @@ export const userRouter = () => {
postDeleteUserController,
);

userRouter.get(
"/certification-dirigeant",
rateLimiterMiddleware,
csrfProtectionMiddleware,
getCertificationDirigeantController,
);

userRouter.post(
"/certification-dirigeant",
rateLimiterMiddleware,
csrfProtectionMiddleware,
postCertificationDirigeantController,
);

userRouter.get(
"/certification-dirigeant/login-as",
rateLimiterMiddleware,
csrfProtectionMiddleware,
getCertificationDirigeantLoginAsController,
);

userRouter.post(
"/certification-dirigeant/login-as",
rateLimiterMiddleware,
csrfProtectionMiddleware,
postCertificationDirigeantLoginAsController,
checkUserSignInRequirementsMiddleware,
issueSessionOrRedirectController,
);

userRouter.get(
"/certification-dirigeant/representing",
rateLimiterMiddleware,
csrfProtectionMiddleware,
getCertificationDirigeantRepresentingController,
);

return userRouter;
};

Expand Down
1 change: 1 addition & 0 deletions src/types/express-session.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export interface UnauthenticatedSessionData {
referrerPath?: string;
authForProconnectFederation?: boolean;
certificationDirigeantRequested?: boolean;
__user_certified?: boolean;
}

export type AmrValue =
Expand Down
25 changes: 25 additions & 0 deletions src/views/user/certification-dirigeant-login-as.ejs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<div>
<%- include('../partials/notifications.ejs', {notifications: notifications}) %>
<h2 class="fr-h2">Vous allez vous connecter en tant que :</h2>
<center>
<h1 class="fr-h3 blue-france">Jacintha Froment</h1>
</center>

<form action="/users/certification-dirigeant/login-as" method="post">
<input type="hidden" name="_csrf" value="<%= csrfToken; %>" />

<fieldset class="fr-fieldset" aria-labelledby="agreement">
<div class="fr-fieldset__element">
<div class="fr-checkbox-group">
<input name="agreement" id="agreement" type="checkbox" />
<label class="fr-label" for="agreement">
J'accepte que FranceConnect transmette mes données au service pour
me connecter
</label>
</div>
</div>
</fieldset>

<button class="fr-btn" type="submit">Continuer</button>
</form>
</div>
28 changes: 28 additions & 0 deletions src/views/user/certification-dirigeant.ejs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<div>
<h1 class="fr-h3">Authentifier votre statut</h1>

<p>
Vous pouvez authentifier instantanément votre statut de dirigeant grâce à
FranceConnect.
</p>

<form action="/users/certification-dirigeant" method="post">
<input type="hidden" name="_csrf" value="<%= csrfToken; %>" />

<div class="fr-connect-group">
<button class="fr-connect">
<span class="fr-connect__login">S’identifier avec</span>
<span class="fr-connect__brand">FranceConnect</span>
</button>
<p>
<a
href="https://franceconnect.gouv.fr/"
target="_blank"
rel="noopener"
title="Qu’est-ce que FranceConnect ? - nouvelle fenêtre"
>Qu’est-ce que FranceConnect ?</a
>
</p>
</div>
</form>
</div>
19 changes: 19 additions & 0 deletions test/acr-checks.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -283,4 +283,23 @@ describe("certificationDirigeantRequested", () => {

assert.equal(certificationDirigeantRequested(prompt), false);
});

it("should return false if non self asserted acr are requested", () => {
const prompt = {
details: {
acr: {
essential: true,
values: [
"https://proconnect.gouv.fr/assurance/certification-dirigeant",
"https://proconnect.gouv.fr/assurance/consistency-checked",
"https://proconnect.gouv.fr/assurance/consistency-checked-2fa",
],
},
},
name: "login",
reasons: ["essential_acrs"],
};

assert.equal(certificationDirigeantRequested(prompt), false);
});
});

0 comments on commit 57e17b3

Please sign in to comment.