Let's open the PRONOTE client using administrative rights.
Let's click the
button on the right
to add more columns to our view.
Drag to the right the following columns.
- Id. connexion (PRONOTE username you'd use normally)
- Id. PRONOTE-ENT (IDs to use in our CAS that links PRONOTE and our CAS)
- Id. CAS (ID returned by our CAS, will be reused on later connections to quickly identify the user)
As you can see, we now have these values.
For the example, I'll authenticate to lea.bally account using our CAS.
Let's note down her "Identifiant PRONOTE-ENT": A8BT6EGHDW3QFPXC
Nothing really interesting here, we're simply following specifications and examples from https://apereo.github.io/cas/7.2.x/protocol/CAS-Protocol-Specification.html.
type TICKET = string;
const tickets = new Map<TICKET, string>();
const assignTicketTo = (user: string): TICKET => {
const randomData = randomBytes(32).toString("base64url");
const ticket = `ST-${randomData}` as TICKET;
tickets.set(ticket, user);
return ticket;
};// This is where PRONOTE will redirect us to!
.get("/cas/login", page, {
detail: { summary: "credential requestor" },
query: t.Object({
service: t.String(),
}),
})// When we submit the form in GET /cas/login
// we should have a way to retrieve the ID we noted earlier!
// In this example, the ID is retrieved from the page (a simple input!) where I gave A8BT6EGHDW3QFPXC.
.post(
"/cas/login",
async ({ body, set }) => {
const id = body.id;
const service = new URL(body.service);
// Let's redirect to <pronote>?ticket=ST-....
const ticket = assignTicketTo(id);
service.searchParams.set("ticket", ticket);
set.status = 302;
set.headers.location = service.href;
return null;
},
{
detail: { summary: "credential acceptor" },
body: t.Form({
id: t.String(), // this is "A8BT6EGHDW3QFPXC" !!
service: t.String(),
}),
}
)// This is where the PRONOTE server will request to verify your ticket!
// My CAS ID here is cas-A8BT6EGHDW3QFPXC, I simply append `cas-` in front (SubjectName)
// I'll give the PRONOTE ID in the `pronoteid` CAS attribute.
.post("/cas/samlValidate", async ({ set, request }) => {
const body = await request.text();
const start = "<samlp:AssertionArtifact>";
const end = "</samlp:AssertionArtifact>";
const startIndex = body.indexOf(start);
const endIndex = body.indexOf(end);
const ticket = body.substring(startIndex + start.length, endIndex).trim();
const id = tickets.get(ticket);
set.headers["content-type"] = "text/xml";
const instant = new Date().toISOString();
tickets.delete(ticket);
return `
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
<SOAP-ENV:Header />
<SOAP-ENV:Body>
<Response xmlns="urn:oasis:names:tc:SAML:1.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:1.0:assertion"
xmlns:samlp="urn:oasis:names:tc:SAML:1.0:protocol" xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" IssueInstant="${instant}"
MajorVersion="1" MinorVersion="1" Recipient="https://eiger.iad.vt.edu/dat/home.do"
ResponseID="_cas_response_${id}">
<Status>
<StatusCode Value="samlp:Success">
</StatusCode>
</Status>
<Assertion xmlns="urn:oasis:names:tc:SAML:1.0:assertion" AssertionID="_e5c23ff7a3889e12fa01802a47331653"
IssueInstant="${instant}" Issuer="localhost" MajorVersion="1"
MinorVersion="1">
<AttributeStatement>
<Subject>
<NameIdentifier>cas-${id}</NameIdentifier>
<SubjectConfirmation>
<ConfirmationMethod>
urn:oasis:names:tc:SAML:1.0:cm:artifact
</ConfirmationMethod>
</SubjectConfirmation>
</Subject>
<Attribute AttributeName="idpronote" AttributeNamespace="http://www.ja-sig.org/products/cas/">
<AttributeValue>${id}</AttributeValue>
</Attribute>
</AttributeStatement>
<AuthenticationStatement AuthenticationInstant="${instant}"
AuthenticationMethod="urn:oasis:names:tc:SAML:1.0:am:password">
<Subject>
<NameIdentifier>cas-${id}</NameIdentifier>
<SubjectConfirmation>
<ConfirmationMethod>
urn:oasis:names:tc:SAML:1.0:cm:artifact
</ConfirmationMethod>
</SubjectConfirmation>
</Subject>
</AuthenticationStatement>
</Assertion>
</Response>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>
`;
})Let's run this server and go back to PRONOTE!
Let's go into "Déléguer l'authentification" submenu.
Let's create a new CAS configuration from there!
Click the gear to open the configuration dialog, we'll use the following settings.
- Mon ENT : Configuration Manuelle
- URL du serveur CAS : Configuration personnalisée
- URL d'authentification :
<server>/cas/login - URL de validation :
<server>/cas/samlValidate
Now, hit the "Paramètres" button on the top right corner and fill in the following settings.
- Définition de l'utilisateur commun à CAS et PRONOTE : Utiliser l'identifiant utilisateur CAS (Subject)
- Reconnaissance de l'utilisateur dans PRONOTE à la première connexion : Avec l'identifiant PRONOTE de l'utilisateur
- Attribut CAS contenant l'identifiant PRONOTE :
idpronote
Click "Valider" to submit those settings.
Make sure to check "Autoriser l'authentification directe par PRONOTE.net" so you can still
log in the old way using ?login=true at the end of the URL.
Finally, hit "Valider" on this dialog and you're done configuring your CAS on PRONOTE.
Double click on the "Actif" cell to enable the CAS configuration.
Also, make sure to enable the CAS configuration for each webspace you want to enable CAS.
Now, publish!
Let's go to <pronote>/ and we'll get redirected to our hand made CAS.
If we authenticate, we get correctly redirected to PRONOTE with our user authenticated!
If we go back to our PRONOTE client, we can see our cas-<id> value has been added to the CAS ID column!