Skip to content

Using a WebView

Authenticating via a WebView might be useful to let users authenticate with CAS or the regular PRONOTE authentication page. Here, this guide will teach you how you could implement this kind of authentication and how you could link it to Pawnote at the end.

Reference in PRONOTE source code
ObjetMoteurProfil.js
// ObjetApplication.js [GApplication]
this.urlInfoMobile = 'InfoMobileApp.json?id=0D264427-EEFC-4810-A9E9-346942A862A4';
eBro = lThis.externalBrowser = cordova.InAppBrowser.open(lThis.profilCourant.serverUrl + GApplication.urlInfoMobile, ECibleInAppBrowser.blank, GApplication.getParametresInAppBrowser({keyboardDisplayRequiresUserAction : 'no', clearcache: 'no', clearsessioncache:'no'}));
eBro.addEventListener('loadstop', _surChargementInfoMobile);
eBro.addEventListener('loaderror', _surLoadError);
eBro.addEventListener('exit', _surExit);

You have to navigate to the mobile app information page, where we’ll inject cookies.

const pronote = "https://demo.index-education.net/pronote";
webview.open(pronote + "/InfoMobileApp.json?id=0D264427-EEFC-4810-A9E9-346942A862A4");
Reference in PRONOTE source code
eBro.executeScript({
code: '(function(){try{'
+ 'var lJetonCas = "", lJson = JSON.parse(document.body.innerText);'
+ 'lJetonCas = !!lJson && !!lJson.CAS && lJson.CAS.jetonCAS;'
+ 'document.cookie = "appliMobile=;expires=" + new Date(0).toUTCString();'
+ 'if(!!lJetonCas) {'
+ 'document.cookie = "validationAppliMobile="+lJetonCas+";expires=" + new Date(new Date().getTime() + (5*60*1000)).toUTCString();'
+ 'document.cookie = "uuidAppliMobile=' + device.uuid + ';expires=" + new Date(new Date().getTime() + (5*60*1000)).toUTCString();'
+ 'document.cookie = "ielang=' + GTraductions.lcid[GTraductions.lang] + ';expires=" + new Date(new Date().getTime() + (365*24*60*60*1000)).toUTCString();'
+ 'return "ok";'
+ '} else return "ko";'
+ '} catch(e){return "ko";}})();'
})

We’re removing the following cookies:

  • appliMobile

We’re adding the following cookies:

  • ielang, language of the app, always keep this to 1036 so API constants are in French

…and only if a CAS is defined…

  • validationAppliMobile, value of the /CAS/jetonCAS
  • uuidAppliMobile, value of the device UUID

To do this, you have to inject the following JavaScript code into the WebView. Of course, you can use another script or another way, as long as you’re injecting the cookies with correct values.

// Should be the same when you'll authenticate with Pawnote.
const deviceUUID = "your-device-uuid";
// We read the JSON from the page itself.
const json = JSON.parse(document.body.innerText);
const tokenCAS = !!json && !!json.CAS && json.CAS.jetonCAS;
// Remove `appliMobile` cookie.
document.cookie = "appliMobile=; expires=" + new Date(0).toUTCString();
if (!!tokenCAS) {
const date = new Date(new Date().getTime() + (5 * 60 * 1000)).toUTCString();
document.cookie = "validationAppliMobile=" + tokenCAS + "; expires=" + date;
document.cookie = "uuidAppliMobile=" + deviceUUID + "; expires=" + date;
}
document.cookie = "ielang=1036; expires=" + new Date(new Date().getTime() + (365 * 24 * 60 * 60 * 1000)).toUTCString();
Reference in PRONOTE source code
eBro.executeScript({
code: 'location.assign("' + lThis.profilCourant.serverUrl + lThis.profilCourant.espaceUrl + '?fd=1")'
});

You can do this by either injecting the following JavaScript to the WebView, or simply ask the WebView to navigate to another page.

const pronote = "https://demo.index-education.net/pronote";
location.assign(pronote + "/mobile.eleve.html?fd=1");

FYI, ?fd=1 bypasses the “proper User-Agent requirement” and might make your life easier when working with WebViews.

Here, we went to the student webspace but you can of course go to any of your liking, you have to ask the user before hand if you want to handle multiple webspaces.

Make sure to not forget the prefix mobile. so you get the mobile page instead of the desktop page.

Reference in PRONOTE source code
// ...on each `loadstop` event...
eBro.executeScript({
code: '(function(){return JSON.stringify(location);})();'
}, function (aData) {
if (aData && aData[0] && aData[0].length) {
aData[0] = JSON.parse(aData[0]);
}
if (aData && aData[0] && lThis.profilCourant.serverUrl.toLowerCase().indexOf(aData[0].host.toLowerCase()) > -1) {
GApplication.log(GApplication.niveauxLog.TRACE, '- _surChargementCAS - retour sur le .net');
// ...NEXT STEP...
}
});

Once you navigated into the webspace, a CAS redirection might’ve happened and the user have to authenticate themselves to their CAS.

You have to make sure you don’t trigger any specific scripts when the user is on these pages since we don’t have to interact with them.

Eventually you have to listen to the URL of every redirections since it is necessary for the next step.

Reference in PRONOTE source code
if (!lThis.profilCourant.jeton) {
eBro.executeScript({
code: `(function(){window.hookAccesDepuisAppli = function(){this.passerEnModeValidationAppliMobile('${lThis.profilCourant.login.replace('\'', '\\\'') || ''}', '${device.uuid}', undefined, undefined, '${JSON.stringify(device)}')};return '';})()`
});
} else {
eBro.executeScript({
code: `(function(){window.hookAccesDepuisAppli = function(){this.passerEnModeValidationAppliMobile('${lThis.profilCourant.login.replace('\'', '\\\'') || ''}', '${device.uuid}', '${lThis.profilCourant.jeton}', '${lThis.profilCourant.codeJeton}', '${JSON.stringify(device)}')};return '';})()`
});
}
window.returnToApp = setInterval(function () {
eBro.executeScript({
code: '(function(){return window.loginState ? JSON.stringify(window.loginState) : \'\';})();'
}, lThis.onLogin.bind(lThis, aData[0].href));
}, 250);

Once redirected to the user’s webspace you have to immediately inject the following JavaScript.

const device = JSON.stringify({
uuid: "your-device-uuid",
model: "...",
platform: "Android"
});
// Works only if the injected script is ran BEFORE page scripts.
window.hookAccesDepuisAppli = function () {
this.passerEnModeValidationAppliMobile('', device.uuid, void 0, void 0, device);
}
// Works when the injected script is ran AFTER page scripts.
//
// NOTE: window.GInterface might change after some versions so this
// is not an official and reliable solution!
try {
window.GInterface.passerEnModeValidationAppliMobile('', device.uuid, void 0, void 0, device);
} catch {}
setInterval(() => {
webview.sendMessage(window.loginState ? JSON.stringify(window.loginState) : void 0);
}, 250);

Where webview.sendMessage() sends a message to your app. On your app, you have to listen to these messages and try to parse the JSON out of them, see next step.

We configured a hook that activates a special mode of the login where it’ll send login requests as a mobile app. This will tell PRONOTE to define window.loginState when authentication is successful.

Reference in PRONOTE source code
onLogin (aURL, aState) {
if (aState && aState[0]) {
aState[0] = JSON.parse(aState[0]);
GApplication.log(GApplication.niveauxLog.TRACE, 'onLogin()');
// On a réussi on va enregistrer le profil
if (aState[0].status === 0 && !!this.profilCourant.serverUrl) {
if (window.returnToApp) {
clearInterval(window.returnToApp);
}
if (this.externalBrowser && aState[0].status === 0) {
this.externalBrowser.close();
}
this.externalBrowser = null;
GApplication.log(GApplication.niveauxLog.TRACE, '- onLogin() - success');
var lObjURLCourant = this.estUrlValide(this.profilCourant.serverUrl + this.profilCourant.espaceUrl);
var aObjURL = this.estUrlValide(aURL);
if (lObjURLCourant.protocol !== aObjURL.protocol || lObjURLCourant.host !== aObjURL.host || lObjURLCourant.pathname !== aObjURL.pathname || lObjURLCourant.espace !== aObjURL.espace) {
GApplication.log(GApplication.niveauxLog.TRACE, '- onLogin() - changement d\'URL ' + (this.profilCourant.serverUrl + this.profilCourant.espaceUrl) + ' -> ' + (aObjURL.protocol + '//' + aObjURL.host + aObjURL.pathname + aObjURL.espace));
this.profilCourant.serverUrl = aObjURL.protocol + '//' + aObjURL.host + aObjURL.pathname;
this.profilCourant.espaceUrl = aObjURL.espace;
}
this.profilCourant.login = aState[0].login;
this.profilCourant.mdp = aState[0].mdp;
this.profilCourant.libelle = aState[0].libelle;
this.profilCourant.modeJeton = !!this.profilCourant.codeJeton && !!this.profilCourant.jeton;
delete this.profilCourant.codeJeton;
delete this.profilCourant.jeton;
this.indProfilCourant = this.indexCourant; // pour ajouter un profil
// blindage des cas de rebond
if (this.indProfilCourant === -1) {
var lClesProfils = this.profils.map(function (aEle) {
return aEle.serverUrl + aEle.espaceUrl + aEle.login;
});
this.indProfilCourant = lClesProfils.indexOf(this.profilCourant.serverUrl + this.profilCourant.espaceUrl + this.profilCourant.login);
}
if (this.indProfilCourant > -1) {
this.profils[this.indProfilCourant] = this.profilCourant;
} else {
this.profilCourant = this.profils.push(this.profilCourant) - 1;
}
this.profils = this.profils.filter(this.filtrerProfil);
this.ecrireProfils(function () {
GApplication.montrerPopup();
GApplication.fermerPopup();
GApplication.chargerInterface(GApplication.interfaces.profils);
});
GApplication.log(GApplication.niveauxLog.DEBUG, {nom: 'profil_configuration success login', url: this.profilCourant.serverUrl, result: {}});
} else if (aState[0].status === 1 && !this.profilCourant.jeton) {
GApplication.log(GApplication.niveauxLog.TRACE, '- onLogin() - fail');
this.profilCourant.essai++;
}
}
}

When you receive the message, you have to check if the message isn’t empty and obviously contains a JSON.

Here’s the structure of the loginState object when defined.

interface LoginState {
/**
* When login failed, value is 1.
*/
status: 0
/**
* Username of the authenticated user.
*/
login: string
/**
* A refresh token that will be used for Pawnote!
*/
mdp: string
/**
* The name of the user.
* @example "PARENT Fanny"
*/
libelle: string
}

As mentionned above, mdp is actually a refresh token so you can authenticate using loginToken().

You only have to use the same deviceUUID as mentionned earlier and use login as username and mdp as token. You have to also know the account type you redirected, but that can be easily guessed thanks to the redirected URL.