<% /*
    $Revision$
    $Id$
    File:               aut/login.inc
    Description:        Inlog functionaliteit
    Parameters:
    Context:

*/
%>
<!-- #include file="../api2/model_prs_perslid.inc" -->
<%
// Elders is prs_key geauthenticeerd. Registreer die hier als de actieve gebruiker.
// stateless komen we hier al niet
function doLogin(prs_key, params)
{
    __Log("==== doLogin " + prs_key);
    params = params || {};
    // Paranoia mode
    var sql = "SELECT prs_perslid_login"
            + "     , prs_perslid_einddatum"
            + "     , prs_perslid_loglevel"
            + "  FROM prs_perslid"
            + " WHERE prs_perslid_verwijder IS NULL"
            + "   AND prs_perslid_inactief IS NULL"
            + "   AND prs_perslid_key = " + prs_key;
    var oRs = Oracle.Execute(sql);
    if (oRs.Eof)
    {
        __DoLog("Niet bestaande, verwijderde of inactieve persoon " + prs_key + " geweigerd in doLogin.")
        _AiAi("INTERNAL_ERROR_INVALID_LOGIN_" + prs_key);
    }
    var first_login = (oRs("prs_perslid_login").Value == null);
    var einddatum = oRs("prs_perslid_einddatum").Value != null?new Date(oRs("prs_perslid_einddatum").Value): null;
    var loglevel = oRs("prs_perslid_loglevel").Value;
    oRs.Close();
    if (loglevel > 0)
    {   // We doen geen __Logging=3, dan zou voor een gebruiker zo maar de logging in beeld komen
        __Logging = 1; // Voor dit bestand
        Session("logging") = __Logging;
        __Log("Logging for this user ({0}) is forced on".format(Session("userident")||String(prs_key)), '#f00');
    }

    if (S("prs_einddatum_login_grace") >= 0 && einddatum)
    {
        var last_allow_login = einddatum;
        last_allow_login.setDate(last_allow_login.getDate() + S("prs_einddatum_login_grace"))
        if (new Date().midnight() > last_allow_login)
        {
            doLogoff();
            shared.simpel_page(L("lcl_einddatum_login_expired"));
        }
    }

    if ("isFACFACinternal" in params) // vanuit JWT-sso
    {
        var deze = new Perslid(prs_key);
        // SSO naar een FACFAC gebruiker mag alleen als aut_idp_internal aan staat
        if (deze.has("WEB_FACFAC") && !params.isFACFACinternal)
        {
            __DoLog("Illegal login WEB_FACFAC");
            shared.internal_error("IDP '{0}' cannot be used for users with WEB_FACFAC (prs_key={1}).".format(params.idp_code, prs_key));
        }
        // Als aut_idp_internal aan staat mag alleen je alleen SSO doen naar een FACFAC gebruiker
        // Tenzij S("idp_internal_anyuser") true is, dan mag je toch naar iedereen
        // Dat doen we op OTA via custenc.wsc, dat doen we niet in PROD
        if (params.isFACFACinternal && !S("idp_internal_anyuser") && !deze.has("WEB_FACFAC"))
        {
            shared.internal_error("IDP '{0}' can only be used for users with WEB_FACFAC (prs_key={1}).".format(params.idp_code, prs_key));
        }
    }

    /* global */ user_key = prs_key;
    //user_lang = oRs(1).Value; // globale moet er nog uit!
    if (typeof LCL_Disable == "undefined")
    {
        __Log("==== LCL opnieuw laden voor nieuwe gebruiker");
        lcl.loadLCL();
    }

    setASPFIXATION();

    var ip = String(Request.ServerVariables("REMOTE_ADDR"));
    Session("last_ip") = ip; // onthouden voor ip_locking

    if (!params.noFacSession && !params.stateless) // Oldstyle API krijgt er anders te veel
    {
        Session.Contents.Remove("stateless");
        // Nu maar eens oudere opruimen. Effectief blijft altijd
        // de laatste sessie van een gebruiker staan.
        // Mogelijk ruim je ook toch verlopen sessies van anderen op. Geen probleem.
        var sql = "DELETE FROM fac_session"
               + "  WHERE fac_session_expire < SYSDATE";
        Oracle.Execute(sql);

        var agent = String(Request.ServerVariables("HTTP_USER_AGENT"));

        // Welbeschouwd gebruiken we het volgende FAC_SESSION record nooit, we werken
        // altijd met ASPSESSION cookie==>IIS SESSION==>user_key
        var sql = "INSERT INTO fac_session"
               + "    (fac_session_sessionid_hash,"
               + "     prs_perslid_key,"
               + "     fac_session_expire,"
               + "     fac_session_useragent,"
               + "     fac_session_ip)"
               + " VALUES(" + safe.quoted_sql(shared.makehash(Session("FACSESSIONID"))) + ", "
               +        user_key + ","
               + "      SYSDATE+1, " // 24 uur is genoeg
               +        safe.quoted_sql(agent, 256) + ","
               +        safe.quoted_sql(ip, 64) + ")";
        Oracle.Execute(sql);
    }
    else if (params.stateless)
    {
        Session("stateless") = 1;
        Session.Abandon(); // Altijd, voor de zekerheid
    }
    var registersql = "UPDATE prs_perslid"
                    + "   SET prs_perslid_login = SYSDATE"
                    + " WHERE prs_perslid_key=" + user_key
                    + "   AND (prs_perslid_login IS NULL OR prs_perslid_login < SYSDATE - 1/24/60)"; // Eens per minuut bijwerken is genoeg
    Oracle.Execute(registersql);

    Session("user_key") = user_key; // Nu ben je pas *echt* ingelogd
    /* global */ user = new Perslid(user_key);
    Session("userident") = "{0} ({1})".format(user.naam(), user.oslogin() || user.oslogin2() || user.prs_perslid_email() || "??"); // Handig voor in logging
    __Log("Authenticated '{0}' successfully".format(Session("userident")));

    Session.Contents.Remove("must_accept_terms");
    if (S("fac_accept_terms") > 0
        && (!(Session("org_user_key") > 0))
        && !user.isGroupedUser()
        && !params.noFacSession
        && !user.has("WEB_FACTAB") // die er niet mee lastig vallen
       )
    {
        var termsPath = custpath + "/bdradrfiles/" + L("lcl_terms_filename");
        var fso = Server.CreateObject("Scripting.FileSystemObject");
        var termsFile = Server.MapPath(termsPath);
        if (!fso.FileExists(termsFile))
        {
            __DoLog("fac_accept_terms is set but '{0}' is not found.".format(termsPath), '#f00');
        }
        else
        {
            var uvers = String(user.terms_version()).split("|")[0]; // Haal de digest er af
            if (uvers != L("lcl_terms_filename"))
                Session("must_accept_terms") = 1; // Wordt opgepikt in common.inc
        }
    }

    // Normaal gesproken zou ik hier new Date() gebruiken
    // Omdat zelfs kleine afwijkingen al grote problemen zouden kunnen
    // geven consequent altijd de Oracle-tijd gebruiken
    var sql = "SELECT SYSDATE FROM DUAL";
    var oRs = Oracle.Execute(sql);
    Session("login_date") = new Date(oRs(0).Value).getTime();
    oRs.Close();

    // FACFAC tracken we altijd
    if (!params.noFacSession && !params.stateless && !Session("org_user_key")) // fac_scan_cust genereert er anders te veel, niet bij terugkeer uit impersonation
    {
        if (user.has("WEB_FACFAC"))
        {
            var tekst = L("lcl_logged_on").format(Session("ASPFIXATION").slice(-6), Session("fclt_realuser"), Session("last_ip"));
            if (Session("fclt_realuserreason"))
                tekst += "\n" + Session("fclt_realuserreason");
            shared.trackaction("PRSLOG", user_key, tekst);
        }
    }

    if (first_login && S("fac_firstlogin_expire") > 0)
    {
        var sql = "INSERT INTO fac_menu"
                + " ( fac_menu_altlabel"
                + " , fac_menu_alturl"
                + " , fac_menu_altgroep"
                + " , fac_menu_level"
                + " , prs_perslid_key"
                + " , fac_menu_volgnr"
                + " ) VALUES"
                + " (" + safe.quoted_sql(L("lcl_firstlogin_url"), 30)
                + " ," + safe.quoted_sql(S("fac_firstlogin_url"))
                + " ,5"
                + " ,2"
                + " ," + user_key
                + " ,(SELECT 10 + COALESCE(MAX(fac_menu_volgnr), 0)"
                  + "   FROM fac_menu"
                  + "  WHERE prs_perslid_key = " + user_key
                  + ")"
                + " )";
        Oracle.Execute(sql, true);
    }

    var fac_lang = getQParamSafe("fac_lang", "").toUpperCase(); // overrule via param
    // Liever geen session maar m_connections heeft dit al nodig voor zijn fac.initsession
    // Bovendien voorkomen we zo dat een simpele user.lang() al een _require_prs_perslid triggert
    Session("user_lang") = fac_lang || user.dblang();
    if (user.lcid()) { Session.LCID = user.lcid(); }
    Session("time_zone") = user.timezone() || S("fac_server_timezone");

    return true;
}
function doLoginStateless(prs_key, params)
{
    /* global */ user_key = prs_key;
    Session("user_key") = user_key; // Nu ben je pas *echt* ingelogd
    /* global */ user = new Perslid(user_key); // wordt mogelijk nog overruled door impersonate
    // Ik wil voor API2 Stateless calls geen SELECT op PRS_PERSLID triggeren met de volgende statements
    // Session("userident") = "{0} ({1})".format(user.naam(), user.oslogin() || user.oslogin2() || user.prs_perslid_email() || "??"); // Handig voor in logging
    Session("stateless") = 1;
    Session.Abandon(); // Altijd, voor de zekerheid
}

// Session.Abondon is gevaarlijk: dan verlies je ook CustomerID etc.
// Bovendien krijg je met IIS dan nog steeds geen nieuwe ASPSESSIONID
function doLogoff(allow_auto_sso)
{
    if (!allow_auto_sso)
        Session("no_sso") = 1; // Voorkom auto_sso
    Session.Contents.Remove("user_key");
    Session.Contents.Remove("userident");
    Session.Contents.Remove("ASPFIXATION");
    Session.Contents.Remove("must_reset_password");
    Session.Contents.Remove("must_set_otp");
    Session.Contents.Remove("login_by_fallback");
    deleteSessionCookie("fcltid");
    deleteCookie("ASPFIXATION");
    if (Session("aut_client_key") > 0)
    {
        var sql = "DELETE FROM aut_client_perslid"
                + " WHERE prs_perslid_key = " + user_key
                + "   AND aut_client_key = " + Session("aut_client_key");
        Oracle.Execute(sql);
    }
}

// Inloggen via een fcltid-cookie of een session die met QR-code is gescand
function setUserFromSession (p_session)
{
    // TODO: Hier wil ik ooit de user_key eigenlijk ook hebben uit
    //       een cookie zodat deze query efficienter wordt.
    var sql = "SELECT fac_session_key, prs_perslid_key, fac_session_data "
            + "  FROM fac_session "
            + " WHERE fac_session_expire > sysdate "
            + "   AND fac_session_sessionid_hash IN ({0}, {1})".format(safe.quoted_sql(shared.makehashMD5(p_session)),  // 30 dagen na de release mag de makehashMD5 weg
                                                                       safe.quoted_sql(shared.makehash(p_session)));
    var oRs = Oracle.Execute( sql );
    if (!oRs.eof)
    {
       doLogin(oRs("prs_perslid_key").Value);
       var sessionData = oRs("fac_session_data").value;

       // verwijder de huidige sessie
       sql = "DELETE fac_session"
           + " WHERE fac_session_key = " + oRs("fac_session_key").Value;
       Oracle.Execute(sql);
       // makeSessionCookie(sessionData); aanroeper bepaalt maar of er een nieuwe sessie komt
    }
    oRs.Close();
}

function makeSessionCookie (sessionData)
{
   var sessionId = shared.random(32);

   // maak nieuwe sessie aan
   var agent = String(Request.ServerVariables("HTTP_USER_AGENT"));
   var ip = String(Request.ServerVariables("REMOTE_ADDR"));

   sql = "INSERT INTO fac_session ( "
       + "   fac_session_sessionid_hash, "
       + "   prs_perslid_key, "
       + "   fac_session_data, "
       + "   fac_session_expire,"
       + "   fac_session_useragent,"
       + "   fac_session_ip) "
       + " VALUES ( "
       +     safe.quoted_sql(shared.makehash(sessionId)) + ", "
       +     user_key + ", "
       +     safe.quoted_sql(sessionData) + ", "
       + "   sysdate + " + S("login_remember_days") + ", " // sessie timeout op 30 dagen.
       +     safe.quoted_sql(agent, 256) + ","
       +     safe.quoted_sql(ip, 64) + " )"
    Oracle.Execute(sql);

    // set de nieuwe sessionID als cookie.
    Response.Cookies("fcltid")=sessionId;
    Response.Cookies("fcltid").Path = rooturl + "/";
    if (S("auto_https") & 2)
        Response.Cookies("fcltid").Secure=true;
    expireCookie("fcltid", "d", S("login_remember_days"));

    // fcltcust is niet per se nodig voor Facilitor maar wel handig bij interne ontwikkeling
    Response.Cookies("fcltcust") = customerId;
    Response.Cookies("fcltcust").Path = rooturl + "/";
    if (S("auto_https") & 2)
        Response.Cookies("fcltcust").Secure=true;
    expireCookie("fcltcust", "d", S("login_remember_days"));
}

function deleteSessionCookie (cookiename)
{
    var session = String(Request.Cookies(cookiename));
    if (session)
    {
        var sql = "DELETE fac_session"
                + " WHERE prs_perslid_key = " + user_key // index-performance
                + "   AND fac_session_sessionid_hash = " + safe.quoted_sql(shared.makehash(session));
        Oracle.Execute(sql);
    }

    // Cookie wissen
    deleteCookie(cookiename);
}

function deleteCookie (cookiename)
{
    Response.Cookies(cookiename)="";
    Response.Cookies(cookiename).Path = rooturl + "/";
    if (S("auto_https") & 2)
        Response.Cookies(cookiename).Secure=true;
    expireCookie(cookiename, "yyyy", -1);
}

function pbkdf2(wachtwoord, passsalt, workfactor, len)
{
    //__Log("P: {0}, S: {1}, W: {2}, L:{3}".format(wachtwoord, passsalt, workfactor, len))
    if (workfactor < 1 ||
        workfactor > 20) // workfactor 2^24 (16777216) duurt op FACWS001 zo'n 45 seconde
                         // Daarom voorlopig limiteren op 2^20 (1048576, zo'n 3 seconde)
    {
        __DoLog(workfactor);
        INTERNAL_ERROR_BADWORKFACTOR;
    }
    len = len || S("prs_password_hash_length"); // default 20
    // __Log("Wachtwoord: {0}, salt: {1}, workfactor: {2}, len: {3}".format(wachtwoord, passsalt, Math.pow(2, workfactor), len));
    var oSLNKDWF = new ActiveXObject("SLNKDWF.About");
    var oCrypto = new ActiveXObject("SLNKDWF.Crypto");
    var usStart = oSLNKDWF.usTimer;
    var is_hash = oCrypto.hex_pbkdf2(wachtwoord, passsalt, Math.pow(2, workfactor), len);
    var tm = ((oSLNKDWF.usTimer - usStart)/1000).toFixed(1);
    __Log("Calculating hash with workfactor {0} ({1}) took {2} ms".format(workfactor, Math.pow(2, workfactor), tm));
    return is_hash;
}

function otpcodes(otpsecret)
{
    if (!otpsecret)
        return false;
    var arr = otpsecret.split("$");
    switch (arr[0])
    {
        case "1": // TOTP,  formaat 1$30$size$offset$seed
            if (arr.length != 5)
            {
                __Log(arr);
                INTERNAL_ERROR_BADTOTP;
            }
            var result = { tokentime: new Date(),
                           otpstep: parseInt(arr[1], 10),
                           otpsize: parseInt(arr[2], 10),
                           otpoffset: parseInt(arr[3], 10),
                           otpseed: arr[4],
                           codes: [] };
            //Response.Write("<p>Current time: {0}".format(toISODateTimeString(result.tokentime, true)));
            if (result.otpoffset != 0)
            {
                result.tokentime.setSeconds(result.tokentime.getSeconds() + result.otpoffset)
                //Response.Write("</br>Token time: {0} ({1} s)".format(toISODateTimeString(result.tokentime, true), result.otpoffset));
            }
            var ts = Math.round(result.tokentime.getTime() / 1000);
            var cnt = Math.floor(ts  / result.otpstep) ; // Google doet 30 seconde
            result.tokenpassed = (ts  / result.otpstep - cnt); // zo ver zijn we al in de periode gevorderd
            __Log("OTP time {0} counter {1}".format(toISODateTimeString(result.tokentime, true), cnt));
            var oCrypto = new ActiveXObject("SLNKDWF.Crypto");
            for (var i = -Math.floor(S("prs_password_otp_window") / result.otpstep); i <= Math.floor(S("prs_password_otp_window") / result.otpstep); i++)
            {
                var onecode = { otpshould : oCrypto.hotp(result.otpseed, cnt + i, result.otpsize),
                                dtsfrom: new Date(((cnt + i) * result.otpstep - result.otpoffset) * 1000),
                                dtsto: new Date(((cnt + i + 1) * result.otpstep - result.otpoffset)* 1000),
                                offset: i * result.otpstep,
                                counter: cnt + i
                              }
                result.codes.push(onecode);
            }

            break;
        default:
            INTERNAL_ERROR_BADOTP;
    }
    return result;
}

// params: {strict: true } matcht uitsluitend prs_perslid_wachtwoord_hash en niet prs_perslid_authenticatie
function testpassword(prs_key, wachtwoord, params)
{
    params = params || {};
    if (!wachtwoord)
        return false;

    var sql = "SELECT prs_perslid_key"
            + "     , prs_perslid_flags"
            + "     , prs_perslid_authenticatie"
            + "     , prs_perslid_authenticatie_exp"
            + "     , prs_perslid_salt"
            + "     , prs_perslid_wachtwoord_hash"
            + "     , prs_perslid_oslogin"
            + "     , prs_perslid_apikey"
            + "  FROM prs_perslid"
            + " WHERE prs_perslid_key = " + prs_key;
    var oRs = Oracle.Execute(sql);

    var passsalt = oRs("prs_perslid_salt").Value;
    var passhash = oRs("prs_perslid_wachtwoord_hash").Value;
    var mobauth = oRs("prs_perslid_authenticatie").Value;
    var mobauthexp = new Date(oRs("prs_perslid_authenticatie_exp").Value);
    var apikey = oRs("prs_perslid_apikey").Value;
    var oslogin = oRs("prs_perslid_oslogin").Value;
    oRs.Close();

    if (!params.strict && mobauth && mobauthexp && new Date() <= mobauthexp) // Mobile 'verzonnen' wachtwoord nog niet verlopen?
    {
        var mobww = wachtwoord.toLowerCase(); // wij sturen lowercase base32
        mobww = mobww.replace(/0/i, 'o'); // 0 / 1 / 8 komen daar niet in voor
        mobww = mobww.replace(/1/i, 'l');
        mobww = mobww.replace(/8/i, 'b');
        if (mobauth == mobww)
        {
            var sql = "UPDATE prs_perslid" // geen hergebruik
                    + "   SET prs_perslid_authenticatie = NULL"
                    + " WHERE prs_perslid_key = " + prs_key;
            Oracle.Execute(sql);

            return true; // Goed
        }
        __Log("Mobile token check failed");
        // Wel doorgaan met gewoon wachtwoord controle, dat staan we ook toe
    }

    // geen wachtwoord
    if (!passhash)
        return false;

    // gewoon wachtwoord
    // Noot: we zouden hier kunnen testen op
    //   AND (prs_perslid_wachtwoord_exp IS NULL OR prs_perslid_wachtwoord_exp > SYSDATE)
    // Met een verlopen wachtwoord mag je echter wel inloggen maar wordt je (via common.inc)
    // wel gedwongen daarna je wachtwoord te wijzigen
    var arr = passhash.split("$");
    if (arr.length == 1) // Old style
    {
        return false; // die is altijd ongeldig en kan alleen via 'wachtwoord vergeten' aangepast worden
    }
    else // new style
    {
        switch (arr[0])
        {
            case "1": // PBKDF2,  1$workfactor$hash
                var workfactor = parseInt(arr[1], 10);
                var should_hash = arr[2];
                if (arr.length != 3 ||
                    isNaN(workfactor) ||
                    should_hash.length < 40) // workfactor 2^24 (16777216) duurt op FACWS001 zo'n 45 seconde
                                             // Daarom voorlopig limiteren op 2^20 (1048576, zo'n 3 seconde)
                {
                    __DoLog(arr);
                    INTERNAL_ERROR_BADHASH;
                }
                var is_hash = pbkdf2(wachtwoord, passsalt, workfactor, should_hash.length / 2);
                if (should_hash != is_hash)
                {
                    __Log("Hash mismatch {0}<>{1}".format(should_hash, is_hash));
                    return false;
                }
                break;
            default:
                __DoLog(arr);
                INTERNAL_ERROR_UNKNOWN_PASSHASH;
        }
    }

    var person = new Perslid(prs_key); // het globale user-object komt pas in common.inc
    if (person.password_expired())
    {
        Session("must_reset_password") = 1;
        // redirect gebeurt in common.inc, nu nog te vroeg
    }

    if (workfactor != S("prs_password_hash_factor"))
        setpassword(prs_key, wachtwoord, null, true); // upgrade
    return true;
}


function setpassword(prs_key, wachtwoord, expired, upgrade)
{
    if (S("prs_password_hash_factor") == 0 || !wachtwoord) // Old style
    {
        var sql = "BEGIN prs.setpassword(" + prs_key + ", " + safe.quoted_sql(wachtwoord) + "); END;";
        Oracle.Execute(sql);
    }
    else
    {
        var passsalt = shared.random(32);
        var workfactor = S("prs_password_hash_factor");
        var new_hash = pbkdf2(wachtwoord, passsalt, workfactor);
        if (upgrade)
            var newexpire = "prs_perslid_wachtwoord_exp"; // effectief ongewijzigd
        else if (expired)
            var newexpire = "SYSDATE";
        else if (S("prs_password_expiration") > 0)
            var newexpire = "SYSDATE + " + S("prs_password_expiration");
        else
            newexpire = "NULL";
        var sql = "UPDATE prs_perslid"
                + "   SET prs_perslid_wachtwoord_exp = " + newexpire
                + "     , prs_perslid_salt = " + safe.quoted_sql(passsalt)
                + "     , prs_perslid_wachtwoord_hash = " + safe.quoted_sql('1${0}${1}'.format(workfactor, new_hash))
                + "     , prs_perslid_credentials_datum = SYSDATE"
                + " WHERE prs_perslid_key = " + prs_key;
        Oracle.Execute(sql);
    }
}

function escapeRegExp(str)
{
  return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
}

function normalizePassword(pwd)
{
    if (!pwd)
        return "";
    var chartrimmer = "0123456789!@#$%^&*(){}[];:\"\'<>,.?\\/+_=-~`"; // '
    pwd = pwd.toLowerCase();
    pwd = pwd.replace(" ", "");
    pwd = pwd.replace(new RegExp("^[" + escapeRegExp(chartrimmer) + "]+", "g"), "");
    pwd = pwd.replace(new RegExp("[" + escapeRegExp(chartrimmer) + "]+$", "g"), "");
    pwd = pwd.replace(/\_/, "")
             .replace(/\./, "")
             .replace(/\+/, "")
             .replace(/\$/, 's')
             .replace(/\5/, 's')
             .replace(/\0/, 'o')
             .replace(/\4/, 'a')
             .replace(/\3/, 'e')
             .replace(/\@/, 'a')
             .replace(/\^/, 'a')
             .replace(/\(/, 'c')
             .replace(/\6/, 'g')
             .replace(/\1/, 'i')
             .replace(/\7/, 't')
             .replace(/\8/, 'b')
             .replace(/\2/, 'z')
             .replace(/\!/, 'i');
    return pwd;
}

function testWordlist(pwd2, wordlist)
{
    var wordlist = [];
    for (var i = 0; wordlist.length; i++)
        wordlist[i] = normalizePassword(wordlist[i]);
    testNormalizedWordlist(pwd2, wordlist);
}

function testNormalizedWordlist(pwd2, wordlist)
{
    for(var i = 0; i < wordlist.length; i++)
    {   // Lengte controle voor korte oslogin en zo
        if(wordlist[i] && wordlist[i].length > 3 && wordlist[i] == pwd2)
            abort_with_warning(L("lcl_invalid_pwd_too_common").format(wordlist[i])); // te bekend
    }
}

function validatePassword(theUser_key, newPassword, extrawordlist) // aborts on failure
{
  // Basis controle (voornamelijk totaal lengte)
  if (S("prs_password_validation_mode") & 1) // Regexp controleren
  {
      if (S("prs_password_validation") && !new RegExp(S("prs_password_validation")).test(newPassword))
        abort_with_warning(L("lcl_invalid_pwd_format"));
  }

  // wachtwoordenlijst controleren
  if (S("prs_password_validation_mode") & 2)
  {
    var pwd2 = normalizePassword(newPassword);
    if (pwd2.length < 4)
    {
        abort_with_warning(L("lcl_invalid_pwd_normalized_short")); // te weinig gewone letters
    }
    testWordlist(pwd2, S("prs_common_passwords"));

    if (theUser_key > 0)
    {
        var user_info = new Perslid(theUser_key);
        var userwordlist = [customerId,
                            user_info.oslogin(),
                            user_info.oslogin2(),
                            user_info.naam(),
                            user_info.prs_perslid_achternaam(),
                            user_info.prs_perslid_voornaam(),
                            user_info.prs_perslid_telefoonnr(),
                            user_info.prs_perslid_mobiel(),
                            user_info.prs_perslid_email(),
                            user_info.prs_perslid_nr(),
                            user_info.afdeling().naam()];
        for (var i = 0; i < userwordlist.length; i++)
        {
            // ABC006 mag geen wachtwoord abc006 gebruiken
            if (userwordlist[i] && newPassword.toLowerCase().indexOf(userwordlist[i].toLowerCase()) > -1)
                abort_with_warning(L("lcl_invalid_pwd_too_common").format(userwordlist[i]))
        }
        testNormalizedWordlist(pwd2.toLowerCase(), userwordlist);
        testWordlist(pwd2, userwordlist); // wijzigt userwordlist!
    }
    if (extrawordlist) // zelfregistratie
        testWordlist(pwd2, extrawordlist);

    var fso = new ActiveXObject("Scripting.FileSystemObject");
    var fread = fso.OpenTextFile(Server.MapPath("./top-200000.inc"), 1); // For Reading
    var wordlist = fread.ReadAll();
    fread.Close();
    wordlist = wordlist.split("\n");
    testNormalizedWordlist(pwd2, wordlist);
  }

  if (S("prs_password_validation_mode") & 4) // charactersets
  {
    //Regular Expressions.
    var regex = new Array();
    regex.push("[A-Z]"); //Uppercase Alphabet.
    regex.push("[a-z]"); //Lowercase Alphabet.
    regex.push("[0-9]"); //Digit.
    regex.push("[$@$!%*#?&]"); //Special Character.

    var passed = 0;
    for (var i = 0; i < regex.length; i++) {
        if (new RegExp(regex[i]).test(newPassword)) {
            passed++;
        }
    }
    if (passed < 3)
        abort_with_warning(L("lcl_invalid_pwd_charsets"));

    // we zouden nog op herhalingen kunnen controleren.
  }

  var pResult = new HookResult();
  if (!custfunc.prs_validatepassword(theUser_key, newPassword, pResult))
  {
    abort_with_warning(pResult.errmsg);
  }
}

function testotp (prs_key, otprequest)
{
    var sql = "SELECT prs_perslid_otpsecret"
            + "     , prs_perslid_otpcounter"
            + "  FROM prs_perslid"
            + " WHERE prs_perslid_key = " + prs_key;

    var oRs = Oracle.Execute(sql);
    var otpsecret = oRs("prs_perslid_otpsecret").Value;
    var otpcounter = oRs("prs_perslid_otpcounter").Value || -1;
    oRs.Close();

    return verify_otp(prs_key, otprequest, otpsecret, otpcounter);
}

function verify_otp (prs_key, otprequest, otpsecret, otpcounter)
{
    var otpresult = otpcodes(otpsecret);

    __Log("Otprequest: " + otprequest);
    var otp_oke = false;
    if (otprequest.length == otpresult.otpsize && otprequest.match(/^[0-9]*$/)) // quick check exact 6 cijfers
    {
        for (var i = 0; i < otpresult.codes.length && !otp_oke; i++)
        {
            var code = otpresult.codes[i];
            __Log(code);
            if (code.counter > otpcounter) // Hij mag niet eerder toegepast zijn
            {
                var otpshould = code.otpshould;
                if (otprequest == code.otpshould)
                {
                    otp_oke = true;
                    otpcounter = code.counter; // TODO: We zouden moeten bijwerken
                    var sql = "UPDATE prs_perslid"
                            + "   SET prs_perslid_otpcounter = " + otpcounter
                            + " WHERE prs_perslid_key = " + prs_key;
                    Oracle.Execute(sql);
                }
            }
        }
    }
    if (!otp_oke)
    {
        __Log("OTP check failed");
        __Log(otpresult);
        return false;
    }
    return otp_oke;
}

// Op basis van usernaam/wachtwoord gaan we iemand authenticeren
function getIdentity(username, wachtwoord, params)
{
    var result = { success: false, fail_reason: L("lcl_login_wrong") };
    params = params || {};

    if (!username || username == 'undefined')
        return result;

    if (username.indexOf("\\") > -1 && S("aut_login_strip_domain") == 1)
        username = username.split("\\")[1]; // strip domain name

    var lockout_name = customerId + "_LOGINATTEMPTS";
    if (params.noPassword)
    {
        // Via SAML/JWT/Integrated authentication doen we geen lockout
    }
    else
    {   // Brute force protection
        var dtExpire = new Date();
        dtExpire.setMinutes(dtExpire.getMinutes() - S("prs_login_lockout_expire"));
        Application.Lock();
        {
            var lockout = myJSON.parse(Application(lockout_name) || "[]");
            var founddata = null;
            for (var i = 0; i < lockout.length; i++)
            {
                var lockdata = lockout[i];
                if (lockdata.lastdate < dtExpire) // Als laatste fout poging 15 minuten geleden is vergeten we alles
                {
                    lockout.splice(i, 1); // verwijderen
                    i--;
                    continue;
                }
                if (lockdata.username == username.toLowerCase())
                {
                    founddata = lockdata;
                    lockdata.count ++;
                    lockdata.lastdate = new Date();
                }
            }
            if (!founddata)
            {
                founddata = { username: username.toLowerCase(),
                              count: 1,
                              firstdate: new Date(),
                              lastdate: new Date()
                            }
                lockout.push(founddata);
            }
            Application(lockout_name) = JSON.stringify(lockout);
        }
        Application.UnLock();

        if (founddata && founddata.count > S("prs_login_attempts"))
        {
            var dtRetry = new Date();
            dtRetry.setMinutes(dtRetry.getMinutes() + S("prs_login_lockout_expire"));
            if (founddata.count == S("prs_login_attempts") + 1  // Alleen eerste keer loggen
                    || Application("otap_environment") == "O") // Maar altijd op OTAP
            {
                shared.auditfail(L("lcl_autfai_toomany").format(founddata.count, S("prs_login_attempts"), username));
            }
            result.fail_reason = L("lcl_prs_login_lockout").format(username, toISODateTimeString(dtRetry));
            return result;
        }
        if (founddata && founddata.count > 1)
        {
            var oSLNKDWF = new ActiveXObject("SLNKDWF.About");
            // maximaal 80 seconde slapen, anders ASP-timeout
            var sleepsec = Math.min(80, S("prs_login_lockout_delay") * Math.pow(S("prs_login_lockout_delayfactor"), founddata.count - 1));
            __Log("Vanwege {0} pogingen ga ik {1}ms slapen".format(founddata.count, 1000*sleepsec), "ffd0d0");
            oSLNKDWF.Sleep(1000 * sleepsec);
        }
    }

    var logins = [];
    logins.push(" prs_perslid_apikey = " + safe.quoted_sql(username, 128)); // Die mag altijd wel
    logins.push(" upper(prs_perslid_email) = " + safe.quoted_sql_upper(username));
    logins.push(" prs_perslid_oslogin = " + safe.quoted_sql_upper(username, 256));
    logins.push(" prs_perslid_oslogin2 = " + safe.quoted_sql_upper(username, 256));

    var block_flags = 4+8; // Niet grouped of blocked, 2==unconfirmed staan we nog heel even toe
    if (!params.stateless)
    {
        block_flags += 1; // systemuser ook niet interactief
    }
    var sql = "SELECT prs_perslid_key "
            + "     , prs_perslid_flags"
            + "     , prs_perslid_otpsecret"
            + "     , prs_perslid_otpcounter"
            + "     , prs_perslid_apikey"
            + "     , prs_perslid_loglevel"
            + "     , prs_perslid_oslogin"
            + "     , prs_perslid_naam"
            + "  FROM prs_perslid"
            + " WHERE prs_perslid_verwijder IS NULL"
            + "   AND prs_perslid_inactief IS NULL"
            + "   AND (" + logins.join(" OR ") + ")"
            + "   AND BITAND(prs_perslid_flags, " + block_flags + ") = 0";

    var oRs = Oracle.Execute(sql);
    if (oRs.Eof) // Gebruikersnaam niet eens gevonden
    {   // Wel ongeveer lengte van een passwordhash vertragen
        var oSLNKDWF = new ActiveXObject("SLNKDWF.About");
        var oCrypto = new ActiveXObject("SLNKDWF.Crypto");
        var workfactor = S("prs_password_hash_factor");
        var usStart = oSLNKDWF.usTimer;
        var test_hash = oCrypto.hex_pbkdf2("password", "salt", Math.pow(2, workfactor - 5), 20); // 1/32e van een echt wachtwoord als test
        var tmicro = oSLNKDWF.usTimer - usStart;
        __Log("Vanwege niet gevonden gebruiker {0}ms slapen".format(Math.round(tmicro / 1000 * 32)), "ffd0d0");
        var oSLNKDWF = new ActiveXObject("SLNKDWF.About");
        var sleepmsec = Math.min(80000, tmicro / 1000 * 31);
        oSLNKDWF.Sleep(sleepmsec);
        oRs.Close();
        return result;
    }
    var loglevel = oRs("prs_perslid_loglevel").Value;
    var oslogin = oRs("prs_perslid_oslogin").Value;
    var naam = oRs("prs_perslid_naam").Value;

    var otpsecret = oRs("prs_perslid_otpsecret").Value;
    var otpcounter = oRs("prs_perslid_otpcounter").Value || -1;

    var found = false;
    if (oRs("prs_perslid_apikey").Value === username)
    {
        params.stateless = true;
        wachtwoord = null; // die is verder irrelevant
        found = true; // En zijn we verder wel klaar
    }
    else if (/* nog niet vanuit SAML/default.asp params.noPassword && */ wachtwoord == null) // SSO
        found = true; // En zijn we verder wel klaar
    else
        found = testpassword(oRs("prs_perslid_key").Value, wachtwoord, { strict: false } );

    if (!found)
        return result;

    if ((oRs("prs_perslid_flags").Value & 2) == 2)
    {
        result.fail_reason = L("lcl_self_register_unconfirmed");
        return result;
    }

    if (!otpsecret || !wachtwoord)
    {
        var deze = new Perslid(oRs("prs_perslid_key").Value);
        if (wachtwoord && deze.has("WEB_FACFAC"))
        {
            result.fail_reason = L("lcl_login_needs_otpsecret");
            return result;
        }
        result = { success: true, user_key: oRs("prs_perslid_key").Value };
    }
    else if (params.otpcode && testotp(oRs("prs_perslid_key").Value, params.otpcode))
        result = { success: true, user_key: oRs("prs_perslid_key").Value };
    else // Wordt opgepikt door login_save.asp
        result = { success: true, otp_user_key: oRs("prs_perslid_key").Value };

    var prev_key = oRs("prs_perslid_key").Value;
    oRs.MoveNext();
    if (!oRs.EOF)
    {   // Het is in theorie denkbaar dat je via SAML of JWT binnenkomt en authenticatie
        // doet op een veld (zoals email) waar uniciteit niet is gegarandeerd
        __DoLog("Login failed: User {0} is not unique (prs_perslid_key {1} and {2} both match)".format(username, prev_key, oRs("prs_perslid_key").Value), "#ff0000");
        // deze foutmelding geeft geen terugkoppeling
        // var result = { success: false, fail_reason: "User {0} is not unique".format(username) };
        // daarom vooralsnog maar even via abort
        shared.simpel_page(L("aut_idp_ident_notunique").format(username));
    }
    oRs.Close();

    if (result.user_key > 0)
    {   // Success! Wis eventuele lockout
        Application.Lock();
        var lockout = myJSON.parse(Application(lockout_name) || "[]");
        for (var i = 0; i < lockout.length; i++)
        {
            var lockdata = lockout[i];
            if (lockdata.username == username.toLowerCase())
            {
                lockout.splice(i, 1); // verwijderen
                i--;
            }
        }
        Application(lockout_name) = JSON.stringify(lockout);
        Application.UnLock();

        if (loglevel > 0 && params.stateless) // stateless gebeurt dit verder niet
        {                                     // Volgens mij is Basic Authentication (SCIM) de enige flow die hier terecht komt
            Session("userident") = oslogin || naam || String(result.user_key);
            // We doen geen __Logging=3, dan zou voor een gebruiker zo maar de logging in beeld komen
            __Logging = 1; // Voor dit bestand
            Session("logging") = __Logging;
            __Log("Logging for this user ({0}) is forced on.".format(Session("userident")), '#f00');
        }
    }

    return result;
}

//
// zet Session("user_key") als username en wachtwoord geldig zijn.
// Login na verzending via sms moet binnen 1 kwartier ingevuld zijn.
// resultaat: true bij succesvolle login, false bij niet succesvol
// drie username opties:
//     - prs_perslid_oslogin
//     - prs_perslid_oslogin2
//     - upper(prs_perslid_email)
// drie wachtwoord opties
//     - leeg (single-signon)
//     - prs_perslid_wachtwoord (of eigenlijk: prs_perslid_salt en prs_perslid_wachtwoord_hash)
//     - prs_perslid_authenticatie (en prs_perslid_authenticatie_exp > sysdate)
// de laatste wordt gebruikt voor mobile/SMS

function tryLogin(username, wachtwoord, params)
{
    params = params || {};
    Session.Contents.Remove("org_user_key");

    var ident = getIdentity(username, wachtwoord, params);
    if (!ident.success)
        return false;

    if (ident.user_key > 0)
    {
        if (ident.stateless || params.stateless)
            doLoginStateless(ident.user_key, params);
        else
            doLogin(ident.user_key, params);
    }

    return true;
}

var base64s = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";

function decode_b64(encStr) {
  var bits, decOut = '', i = 0;
  for(; i<encStr.length; i += 4){
    bits =
     (base64s.indexOf(encStr.charAt(i))    & 0xff) <<18 |
     (base64s.indexOf(encStr.charAt(i +1)) & 0xff) <<12 |
     (base64s.indexOf(encStr.charAt(i +2)) & 0xff) << 6 |
      base64s.indexOf(encStr.charAt(i +3)) & 0xff;
    decOut += String.fromCharCode(
     (bits & 0xff0000) >>16, (bits & 0xff00) >>8, bits & 0xff);
    }
  if(encStr.charCodeAt(i -2) == 61)
    undecOut=decOut.substring(0, decOut.length -2);
  else if(encStr.charCodeAt(i -1) == 61)
    undecOut=decOut.substring(0, decOut.length -1);
  else undecOut=decOut;

  return unescape(undecOut);		//line add for chinese char
}

function IntegratedSSO()
{
    var username = String( Request.ServerVariables("REMOTE_USER") ).toUpperCase();
    __Log('REMOTE_USER = '+username);
    if (username =='' || username=='UNDEFINED')
    {
        username = String( Request.ServerVariables("HTTP_USER") ).toUpperCase();
        __Log('HTTP_USER = '+username);
        if (username =='' || username=='UNDEFINED')
        {
            username = String( Request.ServerVariables("HTTP_LOGIN") ).toUpperCase();
            __Log('HTTP_LOGIN = '+username);
            if (username =='' || username=='UNDEFINED')
            {
                // HTTP_LOGIN, REMOTE_USER or HTTP_USER is not (yet) set. Forcing Windows Authentication
                Response.Status = "401 Unauthorized";
                shared.simpel_page("os_logon is set, trying 401 Unautorized<br>If this page stays, check IIS if integrated authentication is turned on.");
                Response.End(); // Reloads current file
            }
         }
     }
    if (username !='' && username!='UNDEFINED')
    {
        tryLogin(username, null, { noPassword: true });
    }
}

// aud is identificatie van de partij die de informatie *wil weten*
function jwt_create(perslid_key, aud)
{
    var thisPrs = new Perslid(perslid_key)

    var sql = "SELECT *"
            + "  FROM aut_sp"
            + " WHERE aut_sp_audience = " + safe.quoted_sql(aud); // TODO ook issuer meenemen?
    var oRs = Oracle.Execute(sql);
    if (oRs.Eof)
        shared.internal_error("Service provider for '{0}' is not configured for {1}".format(safe.html(aud), customerId));
    var sp_key = oRs("aut_sp_key").value;
    if (oRs("aut_sp_loglevel").Value > 0)
      __Logging = oRs("aut_sp_loglevel").Value;

    if (oRs("fac_functie_key").Value)
        user.checkAutorisation(oRs("fac_functie_key").Value); // dan moet je die hebben

    var params = {
                   iss: oRs("aut_sp_issuer").Value,
                   aud: aud,
                   secret: oRs("aut_sp_secret").Value
                 };
    oRs.Close();

    var claim =
    {
        jti: "#" + (perslid_key) + "#" + new Date().getTime(),
        iat: Math.round(new Date().getTime() / 1000),
        iss: params.iss,
        aud: params.aud
    }

    // aut_sp_map bevat de attributen die wij vrijgeven voor deze SP

    if (sp_key > 0)
    {
        var sql = "SELECT *"
                + "  FROM aut_sp_map"
                + " WHERE aut_sp_key = " + sp_key;
        var oRs = Oracle.Execute(sql);
        while (!oRs.Eof)
        {
            var clm = oRs("aut_sp_map_to").Value; // zo gaat hij heten in de JWT
            switch (oRs("aut_sp_map_from").Value) // zie model_aut_sp_map.inc voor codering
            {
                // Zo veel mogelijk volgens http://openid.net/specs/openid-connect-basic-1_0.html#StandardClaims
                case 1: claim[clm] = thisPrs.oslogin();
                        break;
                case "sub":                claim[clm] = String(perslid_key);   // Als enige gegarandeerd en blijvend uniek voor deze issuer
                                           break;                              // http://openid.net/specs/openid-connect-basic-1_0.html#ClaimStability
                case 3: claim[clm] = thisPrs.prs_perslid_voornaam();
                        break;
                case 2: claim[clm] = thisPrs.prs_perslid_achternaam();
                        break;
//                case "middle_name":        claim[clm] = thisPrs.prs_perslid_tussenvoegsel();
//                                           break;
//                case "name":               claim[clm] = thisPrs.naam();
//                                           break;
                case 9:              claim[clm] = thisPrs.prs_perslid_email();
                        break;
//                case "gender":             claim[clm] = { "0": "female", "1": "male" }[thisPrs.prs_perslid_geslacht()] || "";
//                                           break;
                case "locale": claim[clm] = { "nl": "nl-NL", // RFC5646
                                              "en": "en-GB",
                                              "de": "de-DE",
                                              "fr": "fr-FR",
                                              "no": "nn-NO",
                                              "sv": "sv-SE",
                                              "da": "da-DK",
                                              "fi": "fi-FI" }[String(thisPrs.dblang()).toLowerCase()] || "";
                        break;
//                case "zoneinfo":           claim[clm] = thisPrs.prs_perslid_timezone();
//                                           break;
                // de custom claims
                case 100: /* fclt_authorization */
                          var sql = "SELECT fac_groep_omschrijving"
                                  + "  FROM fac_gebruikersgroep fgg, fac_groep fg"
                                  + " WHERE fgg.fac_groep_key = fg.fac_groep_key"
                                  + "   AND SUBSTR(fac_groep_omschrijving, 1, 1) <> '_'"
                                  + "   AND prs_perslid_key = " + user_key;
                          var oRs2 = Oracle.Execute(sql);
                          var aarr = [];
                          while (!oRs2.Eof)
                          {
                              aarr.push(oRs2("fac_groep_omschrijving").value);
                              oRs2.MoveNext();
                          }
                          oRs2.Close();
                          claim[clm] = aarr.join("|");
                          break;
                case 101: /* fclt_occupation */
                          var wps = user.werkplekken();
                          var warr = [];
                          for (var i2 = 0; i2 < wps.length; i2++)
                              warr.push(wps[i2].prs_werkplek_aanduiding());
                          claim[clm] = warr.join("|");
                          break;
                // case 102 virtuele ondersteunen we nog niet
            }
            oRs.MoveNext();
        }
        oRs.Close();
    }
    __Log(claim);
    return jwt_encode(claim, params.secret);
}

function jwt_encode(claim, secret)
{
    var header =
    {
      "typ":"JWT",
      "alg":"HS256"
    };

    claim.ver = "1.0";
    var oCrypto = new ActiveXObject("SLNKDWF.Crypto");

    var sheader   = oCrypto.base64(JSON.stringify(header, false)); // no padding
    var sclaim    = oCrypto.base64(JSON.stringify(claim, false));
    var signature = oCrypto.hex_hmac_sha256(secret, sheader + "." + sclaim);
    var sig64     = oCrypto.hex2base64(signature, false, true); // no padding, urlsafe
    var jwt       = sheader + "." + sclaim + "." + sig64;

    return jwt;
}

function jwt_decode(token)
{
  // check token
  if (!token) {
    return { err: 'No token supplied' };
  }
  // check segments
  var segments = token.split('.');
  if (segments.length !== 3) {
    return { err:  'Not enough or too many segments' };
  }

  // All segment should be base64
  var result = { headerSeg: segments[0],
                 payloadSeg: segments[1],
                 signature64: segments[2]
               }

  var oCrypto = new ActiveXObject("SLNKDWF.Crypto");

  try
  {
      // base64 decode and parse JSON

      // FSN#39763  SLNKDWFx64.DLL v4.16 heeft nog een bug in het decoderen van
      //            url-safe encoded teksten. Daarom hier voor-corrigeren
      //            De jwt_verify moet straks wel op de originelen
      result.orgheaderSeg  = result.headerSeg;
      result.orgpayloadSeg = result.payloadSeg;
      result.headerSeg  = result.headerSeg.replace(/\-/g, "+").replace(/\_/g, "/");
      result.payloadSeg = result.payloadSeg.replace(/\-/g, "+").replace(/\_/g, "/");

      result.header = JSON.parse(oCrypto.base64_decode(result.headerSeg));
      result.payload = JSON.parse(oCrypto.base64_decode(result.payloadSeg));
  }
  catch (s)
  {
    return { err: "Invalid JSON: {0} {1}".format(s.name, s.message) };
  }

  return result;
};

function jwt_verify(claim, secret, skew, duration)
{
  skew = skew || 0;
  duration = duration || 0;
  if (claim.header.alg != "HS256")
    return { err:  "Only HS256 is supported" };

  var oCrypto = new ActiveXObject("SLNKDWF.Crypto");
  var sig = oCrypto.hex_hmac_sha256(secret, claim.orgheaderSeg + "." + claim.orgpayloadSeg);
  var sig64 = oCrypto.hex2base64(sig, false, true); // no padding, urlsafe

  var now = new Date().getTime() / 1000;
  if (claim.payload.iat)
  {
    // Support for nbf and exp claims.
    // According to the RFC, they should be in seconds.
    if (claim.payload.nbf && now + skew < claim.payload.nbf ) {
      return { err:  'Token not yet active' };
    }

    if (claim.payload.exp)
    {
        if (now > claim.payload.exp + skew) {
            var txt = 'JWT token for {0} has expired at {1}'.format(claim.payload.username || claim.payload.perslid_key || claim.payload.email, toISODateTimeString(new Date(claim.payload.exp * 1000)))
            jwt_warn(claim, txt)
            return { err: txt };
        }
        else if (claim.payload.iat)
        {
            var dur = Math.floor((claim.payload.exp - claim.payload.iat)  / 3600 / 24); // dagen
            var now = new Date().getTime() / 1000;
            var togo = Math.floor((claim.payload.exp - now) / 3600 / 24); // dagen
            if (dur > 14 && togo < 14)
            {
                var txt = "JWT token for {0} will expire soon at {1}".format(claim.payload.username || claim.payload.perslid_key || claim.payload.email, toISODateTimeString(new Date(claim.payload.exp * 1000)));
                jwt_warn(claim, txt)
            }
        }
    }

    // Onze eigen duration/expiration controleren we ook nog
    if (claim.payload.iat + duration < now - skew) {
        __DoLog("Token expired. Now is {0}, got {1}, skew {2}".format(toISODateTimeString(new Date(now * 1000)),
                                                                      toISODateTimeString(new Date(claim.payload.iat * 1000)),
                                                                      skew));
      return { err:  'Token expired at {0}'.format(toISODateTimeString(new Date((claim.payload.iat + duration) * 1000))) };
    }
    if (claim.payload.iat > now + skew) {
        __DoLog("Token not yet active. Now is {0}, got {1}, skew {2}".format(toISODateTimeString(new Date(now * 1000)),
                                                                             toISODateTimeString(new Date(claim.payload.iat * 1000)),
                                                                      skew));
        return { err:  'Token not yet active' };
    }
  }
  if (claim.signature64 == sig64)
    return { success: true }

  return { err: "Token signature did not verify" };
}

// Only warn once per day (or after application pool recycle)
function jwt_warn(claim, subject)
{
    var jwt_warn_name = customerId + "_JWT_WARN_" + claim.payload.jti; // uniek als het goed is
    if (!Application(jwt_warn_name))
    {
        Application.Lock();
        Application(jwt_warn_name) = toISODateTimeString(new Date(claim.payload.exp * 1000));
        Application.Unlock();

        var body = JSON.stringify(claim.payload, null, 4);
        __DoLog(subject, "#FF0080")
        __DoLog(body);
        __DoLog("Bovenstaande wordt per e-mail gestuurd naar iedereen met WEB_PRSSYS");
        // Naar iedereen met PRSSYS
        var sql = "BEGIN fac.putsystemnotification({0}, 3); END;".format(safe.quoted_sql(subject + "<br><pre>" + body + "</pre>")); // 3=portal+email
        Oracle.Execute(sql);
    }
}

// Kopie van UTILS\wsf_shared.js
function getBearerToken(prs_key, idp_code, duration)
{
    var sql = "SELECT aut_idp_audience"
            + "     , aut_idp_issuer"
            + "     , aut_idp_secret"
            + "     , aut_idp_duration"
            + "  FROM aut_idp"
            + " WHERE aut_idp_code = " + safe.quoted_sql_upper(idp_code);
    var oRs = Oracle.Execute(sql);
    // logisch? if (oRs("aut_idp_duration").Value)
    duration = Math.min(oRs("aut_idp_duration").Value, duration || 30);
    var iat = Math.round(new Date().getTime() / 1000);
    var exp = iat + duration;

    var claim =
    {
        iat: iat,
        exp: exp,
        jti: "#" + prs_key + "#" + shared.random(32),
        aud: oRs("aut_idp_audience").Value,
        iss: oRs("aut_idp_issuer").Value,
        perslid_key: prs_key,
        ver: "1.0"
    }
    var jwt = jwt_encode(claim, oRs("aut_idp_secret").Value);
    oRs.Close();

    return jwt;
}

function trySSO(ssocode)
{
    var sql = "SELECT *"
            + "  FROM aut_idp"
            + " WHERE aut_idp_code = " + safe.quoted_sql_upper(ssocode); // een trigger zorgt dat aut_idp_code uppercase is
    var oRs = Oracle.Execute(sql);
    if (oRs.Eof)
        shared.internal_error("Identity provider '{0}' is not configured for {1}".format(safe.html(ssocode), customerId));

    var isFACFACinternal = oRs("aut_idp_internal").Value != 0;
    var ip_restrict = oRs("aut_idp_ipfilter").Value;
    if (oRs("aut_idp_ipauto").Value)
        ip_restrict = ""; // dan is het geen te forceren filter
    if (isFACFACinternal && S("idp_internal_anyuser"))
        ip_restrict = ""; // dan niet al te moeilijk doen
    var ip_ok = true;
    if (ip_restrict) // Application("otap_environment") != "O")
    {
        var ip = String(Request.ServerVariables("REMOTE_ADDR"));
        if (IP.isIPv4(ip) && ip != '::1' && ip != '127.0.0.1') // local access always ok
        {
            ip_ok = IP.inAnySubnet(ip, ip_restrict);
            __Log("SSO IP-restrictie {0} versus remote {1}: {2}".format(ip_restrict, ip, ip_ok));
            if (!ip_ok)
            {   // Always allow Private networks (RFC 1918)
                ip_ok = IP.inAnySubnet(ip, "192.168.0.0/16,172.16.0.0/12,10.0.0.0/8");
            }
        }
    }

    if (!ip_ok)
        shared.internal_error(L("aut_ipdenied").format(ip, ssocode, oRs("aut_idp_omschrijving").Value)); // TODO of 400 code forbidden?

    var return_to = String(Request.ServerVariables("URL")).substr(rooturl.length) + "?" + String(Request.ServerVariables("QUERY_STRING"));
    return_to = return_to.replace(/^\/default.asp/i, "/"); // default.asp vooraan hoeft niet, ik wil cleane url

    if (oRs("aut_idp_type").Value == 3) // Oldstyle SecureSSO, die doet het verder zelf
    {
        shared.internal_error("This single signon method is no longer supported");
    }
    else if (oRs("aut_idp_type").Value == 4) // JWT
    {
        var audience = oRs("aut_idp_audience").Value;
        var issuer = oRs("aut_idp_issuer").Value;
        var url = oRs("aut_idp_remote_loginurl").Value;
        if (!url) // regulier bij Logcenter-sso CUSTOMER als gebruiker (nog) niet bekend is
            shared.internal_error("User unknown and Identity Provider '{0}' has no Remote Login URL".format(ssocode));
        if (url.indexOf("://") < 0) // geen protocol?
            url = HTTP.urlzelf() + "/" + url;
        url += (url.indexOf("?")>=0?"&":"?") + "aud=" + safe.url(audience) + "&iss=" + safe.url(issuer) ;
        var redirect_uri = HTTP.urlzelf() + "/"; // TODO: /appl/aut/jwt is misschien logischer als 'endpoint'
        url += "&redirect_uri={0}&return_to={1}".format(safe.url(redirect_uri), safe.url(return_to));
    }
    else if (oRs("aut_idp_type").Value == 5) // SAML
    {
        // var url = oRs("aut_idp_remote_loginurl").Value; // /Shibboleth.sso/Login?entityID=https://idp.testshib.org/idp/shibboleth
        // Je moet de entityId in de issuer invullen
        var url = "/Shibboleth.sso/Login?entityID={0}".format(safe.url(oRs("aut_idp_issuer").Value))
        var tmparam = toISODateTimeString(new Date()).replace(" ", "_"); // troubleshooting
        // shib kent geen return_to parameter dus zelf maar in redirect_uri verwerken
        var redirect_uri = HTTP.urlzelf() + "/appl/aut/saml/?return_to={0}&fac_id={1}&fclttm={2}".format(safe.url(return_to), customerId, tmparam);
        url += "&target={0}".format(safe.url(redirect_uri));
    }
    else
        shared.internal_error("IDP '{0}' type {1} not supported yet.".format(safe.html(ssocode), oRs("aut_idp_type").Value));
    oRs.Close();
    Response.Redirect(url); // die stuurt ons wel terug
    Response.End;
}

// idp_data is inclusief include idpmappings
// Er is elders vastgesteld dat 'claim' geldige informatie over een gebruiker bevat.
// Verwerk dat nu: probeer in te loggen als de gebruiker en/of maak hem eventueel aan
function process_claim(claim, idp_data, params)
{
    __Log("Entering process_claim")
    __Log(claim);

    params = params || {};
    var hasIdentify = false;
    var hasIdentifyVal = false;
    var isFACFACinternal = idp_data.internal != 0;
    var auditfails = [];
    for (var i =0; i < idp_data.idpmappings.length; i++)
    {
        var idpm = idp_data.idpmappings[i];
        if (typeof idpm.identify == "object" && "id" in idpm.identify)
            idpm.identify = idpm.identify.id;

        if (idpm.identify == 0)
            continue;

        hasIdentify = true;
        if (!claim[idpm.from]) // niet meegegeven
            continue;

        if (idpm.name.id == 21) // afdeling doen we apart
            continue;
        hasIdentifyVal = true;

        switch (idpm.name.id) // zie model_aut_idp_map.inc voor codering
        {
            case 1: // login
                tryLogin(claim[idpm.from], null, { noPassword: true, idp_code: idp_data.code, stateless: params.by_bearer, isFACFACinternal: isFACFACinternal });
                break;
            case 9: // email
                tryLogin(claim[idpm.from], null, { noPassword: true, idp_code: idp_data.code, stateless: params.by_bearer, isFACFACinternal: isFACFACinternal });
                break;
           case 12: // externalid, geen break
           case 13: // externalid (case insensitve)
                var sql = "SELECT pp.prs_perslid_key"
                        + "  FROM prs_perslid pp"
                        + " WHERE prs_perslid_verwijder IS NULL";
                if (idpm.name.id == 12)
                    sql += "   AND prs_perslid_externid = " + safe.quoted_sql(claim[idpm.from]);
                else
                    sql += "   AND UPPER(prs_perslid_externid) = " + safe.quoted_sql_upper(claim[idpm.from]);
                var oRs = Oracle.Execute(sql);
                if (!oRs.Eof)
                {
                    doLogin(oRs("prs_perslid_key").Value, { stateless: params.by_bearer, idp_code: idp_data.code, isFACFACinternal: isFACFACinternal });
                }
                oRs.Close();
                break;
            case 99: // internal, prs_perslid_key
                doLogin(parseInt(claim[idpm.from], 10), { stateless: params.by_bearer, idp_code: idp_data.code, isFACFACinternal: isFACFACinternal });
                break;
            default:
                if (idpm.name.id > 1000) // Flexkenmerk
                {
                    var kenmerk_key = idpm.name.id - 1000;
                    var sql = "SELECT pp.prs_perslid_key"
                            + "  FROM prs_perslid pp"
                            + "     , prs_kenmerklink pkl"
                            + " WHERE pp.prs_perslid_key = pkl.prs_link_key"
                            + "   AND prs_perslid_verwijder IS NULL"
                            + "   AND pkl.prs_kenmerklink_niveau = 'P'"
                            + "   AND pkl.prs_kenmerk_key = " + kenmerk_key
                    if (idpm.identify & 2)
                    {
                        sql += "   AND UPPER(pkl.prs_kenmerklink_waarde) = " + safe.quoted_sql_upper(claim[idpm.from]);
                    }
                    else
                    {
                        sql += "   AND pkl.prs_kenmerklink_waarde = " + safe.quoted_sql(claim[idpm.from]);
                    }
                    var oRs = Oracle.Execute(sql);
                    if (!oRs.Eof)
                    {
                        doLogin(oRs("prs_perslid_key").Value, { stateless: params.by_bearer, idp_code: idp_data.code, isFACFACinternal: isFACFACinternal });
                    }
                    oRs.Close();
                }
                else
                    __DoLog("Unsupported identifying claim '{0}'".format(idpm.name.name));
        }
        if (user_key < 0) // niet gelukt met deze identifying claim. Zijn er nog meer misschien
        {
            auditfails.push(L("lcl_autfai_loginnotfound").format(idpm.name.name,  idpm.from, claim[idpm.from]));
        }
        else
        {
            __Log("Authenticated because claim '{0}' maps to '{1}'".format(idpm.from, idpm.name.name));
            break; // ingelogd, niet verder zoeken
        }
    }
    if (user_key < 0 && auditfails.length)
    {
        shared.auditfail(auditfails.join("\n"));
    }

    // We zijn nu in principe ingelogd maar er kunnen nog extra voorwaarden zijn waarom het toch niet mag
    // Dan wordt je alsnog uitgelogd
    var alles_ok = user_key > 0 || (idp_data.autocreate.id & 1);
    if (user_key > 0 && idp_data.authorization && !user.has(idp_data.authorization.id)) // gebruiker moet deze autorisatie hebben
        alles_ok = false;

    if (alles_ok)
    {
        for (var i = 0; i < idp_data.idpmappings.length; i++)
        {
            var idpm = idp_data.idpmappings[i];
            // 21=afdeling, deze is van zichzelf niet identificerend genoeg,
            // we controleren dat je er *ook* onder moet vallen
            if (idpm.name.id == 21 && idpm.identify == 1)
            {
                var IdentifyDept = claim[idpm.from];
                // Zoek afdeling eerst op
                var sql = "SELECT prs_afdeling_key"
                        + "  FROM prs_v_afdeling"
                        + " WHERE prs_afdeling_verwijder IS NULL"
                        + "   AND prs_afdeling_upper = " + safe.quoted_sql_upper(IdentifyDept);
                if  (idp_data.company)
                    sql += " AND prs_bedrijf_key = " + idp_data.company.id;
                var oRs = Oracle.Execute(sql);
                if (oRs.EOF)
                {
                    __DoLog("Claimed department {0} not found".format(IdentifyDept), "#ff8800");
                    alles_ok = false;
                }
                else
                {
                    var afd_key = oRs("prs_afdeling_key").Value;
                    oRs.MoveNext();
                    if (!oRs.EOF)
                    {
                        __DoLog("Claimed department {0} not unique".format(IdentifyDept), "#ff8800");
                        alles_ok = false;
                    }
                    else
                    {
                      var sql = "SELECT ab.prs_bedrijf_key"
                              + "  FROM prs_v_afdeling_boom ab"
                              + " WHERE ab.prs_afdeling_key = " + user.prs_afdeling_key()
                              + "   AND ab.prs_afdeling_key1 = " + afd_key;
                        var oRs2 = Oracle.Execute(sql);
                        if (oRs2.EOF)
                        {
                            __DoLog("User is not a member of department {0}".format(IdentifyDept), "#ff8800");
                            alles_ok = false;
                        }
                        oRs2.close();
                    }
                }
                oRs.Close();
            }
        }
    }
    if (!alles_ok)
    {
        doLogoff(); // Toch maar niet
    }

    if (!hasIdentify)
        shared.internal_error(L("aut_idp_no_ident").format(idp_data.code || idp_data.name));
    if (!hasIdentifyVal)
        shared.internal_error(L("aut_idp_no_ident_attr").format(idp_data.code || idp_data.name));

    if (  user_key < 0 && idp_data.autocreate.id & 1   // Misschien dan maar aanmaken?
       || user_key > 0 && idp_data.autocreate.id & 2   // en/ of bijwerken
       )
    {
        process_claim_update(claim, idp_data, params)
    }
}

function process_claim_update(claim, idp_data, params)
{
    var isFACFACinternal = idp_data.internal != 0;
    var persdata = {  };
    for (var i =0; i < idp_data.idpmappings.length; i++)
    {
        var idpm = idp_data.idpmappings[i];
        var val = idpm["default"];
        if (idpm.from in claim)
        {
            val = claim[idpm.from];
        }

        switch (idpm.name.id) // zie model_aut_idp_map.inc voor codering
        {
            case  1: persdata["login"]         = val; break;
            case  2: persdata["lastname"]      = val; break;
            case  3: persdata["firstname"]     = val; break;
            case  4: persdata["middlename"]    = val; break;
            case  5: persdata["initials"]      = val; break;
            case  6: persdata["gender"]        = val; break;
            case  7: persdata["phone"]         = val; break;
            case  8: persdata["title"]         = val; break;
            case  9: persdata["email"]         = val; break;
            case 10: persdata["phone"]         = val; break;
            case 11: persdata["mobile"]        = val; break;
            case 12: // externalid, geen break
            case 13: persdata["externalid"]    = val; break;
            case 14: persdata["employeenumber"]= val; break;
            // de foreigns
            case 20: if (val) // functie (=srtperslid)
            {   // Bestaat de functie
                var sql = "SELECT prs_srtperslid_omschrijving"
                        + "  FROM prs_srtperslid"
                        + " WHERE prs_srtperslid_verwijder IS NULL"
                        + "   AND prs_srtperslid_upper = " + safe.quoted_sql_upper(val);
                var oRs = Oracle.Execute(sql);
                if (oRs.eof)
                {   // Niet gevonden dus toevoegen
                    var sql_ins = "INSERT INTO prs_srtperslid"
                                + " (prs_srtperslid_omschrijving)"
                                + " VALUES (" + safe.quoted_sql(val) + ")";
                    Oracle.Execute(sql_ins);
                }
                oRs.Close();
                persdata["function"] = { name: val };
                break; // Kan omdat fields.function.foreign.desc_is_unique is gezet
            }
            case 21: if (val) // afdeling
            {   // Zoek afdeling
                var sql = "SELECT prs_afdeling_key"
                        + "  FROM prs_v_afdeling"
                        + " WHERE prs_afdeling_verwijder IS NULL"
                        + "   AND prs_afdeling_upper = " + safe.quoted_sql_upper(val);
                if (idp_data.department)
                {
                    sql += " AND prs_afdeling_parentkey = " + idp_data.department.id;
                }
                else
                {
                    if  (idp_data.company)
                        sql += " AND prs_bedrijf_key = " + idp_data.company.id;
                }
                var oRs = Oracle.Execute(sql);
                if (oRs.eof)
                {   // Niet gevonden dus toevoegen
                    // department of company moet bekend zijn (uit idp_data)
                    var afd_fld = "";
                    var afd_val = "";
                    if (idp_data.department && idp_data.department.id)
                    {
                        afd_fld += ", prs_afdeling_parentkey";
                        afd_val += ", " + idp_data.department.id;
                    }
                    else if (idp_data.company && idp_data.company.id)
                    {
                        afd_fld += ", prs_bedrijf_key";
                        afd_val += ", " + idp_data.company.id;
                    }

                    if (afd_fld != "")
                    {
                        var sql = "SELECT prs_s_prs_afdeling_key.nextval FROM DUAL";
                        var oRs_seq = Oracle.Execute(sql);
                        var afd_key = oRs_seq(0).Value;
                        oRs_seq.Close();
                        var sql_ins = "INSERT INTO prs_afdeling"
                                    + "( prs_afdeling_key, prs_afdeling_naam" + afd_fld + ")"
                                    + "VALUES"
                                    + "( " + afd_key
                                    + ", " + safe.quoted_sql(val)
                                           + afd_val
                                    + ")";
                        Oracle.Execute(sql_ins);
                        persdata["department"] = afd_key;
                    }
                    else
                    {   // Geen bedrijf en parent-afdeling bekend
                        __Log("Claimed department {0} not inserted: no company and parent department".format(val));
                    }
                }
                else
                {   // Bestaande afdeling
                    var afd_key = oRs("prs_afdeling_key").Value;
                    oRs.MoveNext();
                    if (!oRs.eof)
                        shared.internal_error("Claimed department {0} not unique".format(val));

                    persdata["department"] = afd_key;
                }
                oRs.Close();
                break;
            }
            case 22: if (val)
                        persdata["profile"]   = { name: val }; break; // Kan omdat fields.function.desc_is_unique is gezet
            // De 1-n
            case 100: persdata.authorisation   = val; break;
            case 101: persdata.workplace       = val; break;
            // case 102: persdata.workplacevirtual       = val; break;
            // case 103: reserved voor mandatering?
            default:
                if (idpm.name.id > 1000)
                    set_custom_field(persdata, idpm.name.id - 1000, val, "C");
                break;
        }
    }
    // Klantspecifieke check functie (hookfunction) voor de invoer
    var pResult = new HookResult();
    if (!custfunc.aut_process_claim(user_key, persdata, claim, idp_data, pResult))
    {
      shared.simpel_page(pResult.errmsg);
    }

    if (user_key < 0)
        __Log("User automatically created with data:");
    else
        __Log("User automatically updated with data:");
    __Log(persdata);

    var persparams = {};
    var person = new model_prs_perslid({ internal: true }); // Internal: true om dit (nog) anoniem te mogen doen
    if (user_key > 0) // bijwerken
    {
        person.REST_PUT( persparams, persdata, user_key );
    }
    else // nieuwe
    {
        if (!("department" in persdata))
        {
            if (!idp_data.department)
                shared.internal_error("Department is not configured for Identity Provider {0} ({1})".format(idp_data.code, idp_data.name));

            persdata["department"] = idp_data.department.id; // dan moet die ingevuld zijn
        }

        var prs = person.REST_POST( persparams, persdata );
        __Log("Created user '{0} {1}' with key {2} for idp '{3}'".format(persdata["firstname"], persdata["lastname"], prs.key, idp_data.code || idp_data.code));
        if (claim.password_reset)
        {
            var sql = "UPDATE prs_perslid SET prs_perslid_wachtwoord_exp = SYSDATE WHERE prs_perslid_key = " + prs.key;
            Oracle.Execute(sql);
        }
        // De nieuw aangemaakte gebruiker inloggen:
        doLogin(prs.key, { idp_code: idp_data.code, isFACFACinternal: isFACFACinternal });
        // En nu pas kunnen we tracken
        shared.trackaction("PRSUPD", prs.key, L("lcl_prsupd_createduser").format(persdata["firstname"], persdata["lastname"], prs.key, idp_data.code || idp_data.code));
    }
    // Nu authorisatie groepen nog bijwerken
    // Via het model was me even iets te hoog gegrepen: ik zou toch (nog) de id's er bij moeten halen
    if ("authorisation" in persdata)
    {   // authorisation bevat gebruikersgroepen gescheiden door '|' of ';'
        /* SHIB: Within each CGI variable or header (see below), multiple attribute values
           are separated by a semicolon, and semicolons in values are escaped with a backslash.
           The data should be interpreted as UTF-8, which is a superset of ASCII.
         */

        var autharr = persdata["authorisation"].toLowerCase().split(/[;\|]/); // lowerCase, insensitive dus
        var sql = "DELETE FROM fac_gebruikersgroep"
                + " WHERE prs_perslid_key = " + user_key
                + "   AND fac_groep_key NOT IN (SELECT fac_groep_key "
                + "                               FROM fac_groep"
                + "                              WHERE LOWER(fac_groep_omschrijving) IN ({0})".format(safe.quoted_sql_join(autharr))
                + "                                 OR SUBSTR(fac_groep_omschrijving, 1, 1) = '_'" // Die blijven altijd
                + "                             )";
        Oracle.Execute(sql);
        var sql = "INSERT INTO fac_gebruikersgroep(prs_perslid_key, fac_groep_key)"
                + "  SELECT " + user_key + ", fac_groep_key"
                + "    FROM fac_groep fg"
                + "   WHERE LOWER(fac_groep_omschrijving) IN ({0})".format(safe.quoted_sql_join(autharr))
                + "     AND SUBSTR(fac_groep_omschrijving, 1, 1) <> '_'" // Die nooit
                + "     AND NOT EXISTS (SELECT 1"
                + "                       FROM fac_gebruikersgroep fg2"
                + "                      WHERE fg2.fac_groep_key = fg.fac_groep_key"
                + "                        AND fg2.prs_perslid_key = " + user_key + ")";
        Oracle.Execute(sql);
    }

    if ("workplace" in persdata)
    {
        // Eerst oude werkplekken ophalen
        var sql = "SELECT pw.prs_werkplek_key, UPPER(alg_plaatsaanduiding) alg_plaatsaanduiding"
                + "  FROM prs_perslidwerkplek pw, prs_werkplek wp, alg_v_plaatsaanduiding"
                + " WHERE pw.prs_perslid_key = " + user_key
                + "   AND pw.prs_werkplek_key = wp.prs_werkplek_key"
                + "   AND wp.prs_werkplek_type = 0" // alleen vaste plekken
                + "   AND alg_onroerendgoed_keys = wp.prs_alg_ruimte_key"
                + "   AND alg_onroerendgoed_type = 'R'";

        var oRs = Oracle.Execute(sql);
        var oldWP = {};
        while (!oRs.Eof)
        {
            oldWP[oRs("alg_plaatsaanduiding").Value] = oRs("prs_werkplek_key").Value;
            oRs.MoveNext();
        }
        oRs.Close();

        // workplace bevat ruimtes gescheiden door '|' of ';'
        // (we ondersteunen alleen impliciete werkplekken, geen 'named')
        // Codering moet volgens alg_v_plaatsaanduiding zijn (locatiecode-gebouwcode-verdiepingcode-ruimtenr)
        // Als er een '@' voor staat is het een virtuele werkplek
        persdata["workplace"] = persdata["workplace"] || "";
        var workplacearr = persdata["workplace"].split(/[;\|]/);
        for (var i = 0; i < workplacearr.length; i++)
        {
            var wpcode = workplacearr[i];
            var virtual = 0;
            if (wpcode.substr(0, 1) == '@')
            {
                virtual = 1;
                wpcode = wpcode.substr(1);
            }

            if (wpcode in oldWP)
            {
                delete oldWP[wpcode]; // Hoeven we straks niet te wissen
            }
            else // Toevoegen
            {
                var sql = "SELECT alg_onroerendgoed_keys"
                        + "  FROM alg_v_plaatsaanduiding"
                        + " WHERE alg_onroerendgoed_type = 'R'"
                        + "   AND UPPER(alg_plaatsaanduiding) = " + safe.quoted_sql_upper(wpcode);
                var oRs = Oracle.Execute(sql);
                if (!oRs.Eof)
                {
                    delete oldWP[wpcode.toUpperCase()]; // Die zal hergebruikt worden
                    var okey = oRs("alg_onroerendgoed_keys").Value;
                    sql = "BEGIN"
                        + "  prs.movetoruimte ({0}, {1}, '{2}', {3}); ".format(user_key, okey, 'G', virtual) // G want maar één werkplek per gebouw
                        + "END;";
                    Oracle.Execute(sql);
                }
                else
                    __Log("Workplace '{0}' not found".format(workplacearr[i]));
                oRs.Close();
            }
        }
        for (wpcode in oldWP) // restant opruimen
        {
            __Log("Persoon verwijderen van WP {0}, wp-key {1}".format(wpcode, oldWP[wpcode]));
            if (S("prs_werkplek_implicit") == 1)
            {
                var sql = "DELETE FROM prs_werkplek"
                        + " WHERE prs_werkplek_key = " + oldWP[wpcode];
            }
            else
            {
                var sql = "DELETE FROM prs_perslid_werkplek"
                        + " WHERE prs_perslid_key = " + user_key
                        + "   AND prs_werkplek_key = " + oldWP[wpcode];
            }
            Oracle.Execute(sql);
        }
    }
}
function expireCookie(cookiename, interval, number)
{
  var datum = new Date();
  if (interval == "d")
    datum.setDate(datum.getDate() + number);
  else if (interval == "yyyy")
    datum.setYear(datum.getYear() + number);
  Response.Cookies(cookiename).expires=datum.getVarDate();
}
%>
