Подтвердить что ты не робот

Построить подписанный запрос SAML2 LogOut

Моя цель - реализовать протокол Single Log Out. Сначала я понимаю, как работает стандарт и как я могу поместить его в свой сценарий: ADFS 2.0 как IdP, для меня это как "черный ящик"

То, что я делаю в данный момент, следующее:

  • Отправьте <AuthnRequest> на мой IdP

  • IdP запрашивает у меня учетные данные, я предоставляю их и получаю успешную регистрацию.

  • Получите значение SessionIndex и создайте a <LogoutRequest>

<samlp:LogoutRequest xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="_135ad2fd-b275-4428-b5d6-3ac3361c3a7f" Version="2.0" Destination="https://idphost/adfs/ls/" IssueInstant="2008-06-03T12:59:57Z"><saml:Issuer>myhost</saml:Issuer><NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress" NameQualifier="https://idphost/adfs/ls/">[email protected]</NameID<samlp:SessionIndex>_0628125f-7f95-42cc-ad8e-fde86ae90bbe</samlp:SessionIndex></samlp:LogoutRequest>

  • Возьмите вышеуказанный <LogoutRequest> и закодируйте его в Base64

  • Конструирует следующую строку: SAMLRequest=base64encodedRequest&SigAlg=http%3A%2F%2Fwww.w3.org%2F2000%2F09%2Fxmldsig%23rsa-sha1

  • С помощью указанной выше строки создается подпись

  • Кодировать подпись в base64

  • Отправить запрос: https://"https://idphost/adfs/ls/?SAMLRequest=base64encodedRequest&SigAlg=http%3A%2F%2Fwww.w3.org%2F2000%2F09%2Fxmldsig%23rsa-sha1&Signature=base64EncodedSignature

Но IdP отвечает мне: Ошибка подтверждения сигнатуры сообщения SAML.

Для подписания я использую свой закрытый ключ (2048 байт), и для проверки предполагается, что IdP использует мой открытый ключ (тот, который я отправил, когда я зарегистрировал свой хост)

Код для подписания запроса выглядит так:

// Retrieve the private key
KeyStore keyStore = KeyStore.getInstance("JKS", "SUN");
FileInputStream stream;
stream = new FileInputStream("/path/to/my/keystore.jks");
keyStore.load(stream, "storepass".toCharArray());
PrivateKey key = (PrivateKey) keyStore.getKey("keyAlias","keyPass".toCharArray());

// Create the signature
Signature signature = Signature.getInstance("SHA1withRSA");
signature.initSign(key);
signature.update("SAMLRequest=jVJda8IwFH2e4H8ofW%2BbVmvboGWCDApusDn2sBdJm1sNtEmXmw7x1y92KDrY2Ov5uueEzJG1TUfXaqd68wIfPaBxDm0jkQ7Mwu21pIqhQCpZC0hNRTfLxzWNfEI7rYyqVONeWf52METQRijpOsVq4W7JoSzjJJnWAEAmwLMMpmRG0jCrYJICIcR13kCjdSxcG%2BA6K9tQSGYGZG9MhzQIGrUT0uPw6VegpV%2FtA8ZrDBq0ZxB7KCQaJo2NICT1yMwjk9cwonFG4%2BTdzceju%2FmpOx3EOu8qYThgGJ3j5sE1fZE%2F2X3FynlQumXm9%2BGhHw6I4F49SCm0TDRLzjWgrXiKee5ZI2oB%2Bj%2Bj8qYX6GvFtdj1cPRryzPJ4Xh%2F2%2Fe736VvRzf2nn24wmoP%2BZbMojSM4tpL6iz2plFVeYyn4NUc0hmDjJQlfCf9cI5HZ%2Fjm4%2BRf&RelayState=null&SigAlg=http%3A%2F%2Fwww.w3.org%2F2000%2F09%2Fxmldsig%23rsa-sha1".getBytes());

String signatureBase64encodedString = (new BASE64Encoder()).encodeBuffer(signature.sign());
4b9b3361

Ответ 1

Наконец, я получил правильный рецепт:

  • Сгенерировать значение SAMLRequest
  • Кодировать значение SAMLRequest в Base64
  • URL-кодирование значения SAMLRequest
  • URL-кодирование значения SigAlg: http://www.w3.org/2000/09/xmldsig#rsa-sha1
  • Подайте подпись алгоритма (SHA1withRSA) с помощью SAMLRequest = значение & SigAlg = значение
  • URL-кодирование сгенерированной подписи

Мы можем выполнить шаги 2 и 3 с помощью отладчика SAML 2.0 (https://rnd.feide.no/simplesaml/module.php/saml2debug/debug.php). И для URL-кодирование использует классические w3schools (http://www.w3schools.com/tags/ref_urlencode.asp)

Внимание! Убедитесь, что алгоритм вашей полагающейся стороны в ADFS2 настроен на SHA1!

С уважением,

Луис

ps: теперь мне нужно немного кода...

pps: код можно найти здесь: https://github.com/cerndb/wls-cern-sso/tree/master/saml2slo

Ответ 2

Там ошибка в реализации ADFS, где сообщение об ошибке, которое оно дает, обратное. Когда он говорит:

Запрос SAML не подписан с ожидаемым алгоритмом подписи. Запрос SAML подписан с алгоритмом подписи http://www.w3.org/2001/04/xmldsig-more#rsa-sha256. Ожидаемый алгоритм подписи http://www.w3.org/2000/09/xmldsig#rsa-sha1

это на самом деле означает, что вы используете SHA1, и он ожидал SHA256.

Ответ 3

Поскольку у нас было много шагов, чтобы, наконец, добиться успешной реализации SLO на Domino 9.0.1, я решил написать код, который позволит использовать любую (будущую) конфигурацию IdP для работы с нашими серверами Domino. Я реализовал следующую стратегию:

  • Используйте как можно больше информации из входящего запроса на выход SAML
  • Определите конфигурацию IdP в idpcat.nsf, чтобы найти соответствующую информацию об IdP SLO Response, которая будет отправлена ​​поставщику услуг IdP (сервер SAML).
  • Определите ответ на выход SAML в соответствующей конфигурации IdP в idpcat.nsf, чтобы разрешить динамическую адаптацию к новым требованиям при изменении конфигурации SAML.

В результате код считывает все поля входящего запроса на выход SAML в карту параметров и декодирует и раздувает строку запроса, чтобы извлечь XML-параметры запроса на карту параметров. Поскольку различные веб-сайты на сервере domino могут быть настроены для разных поставщиков услуг IdP для разрешения соединения SSO, я определяю конфигурацию IdP с соответствующим "именем хоста" и читаю все свои поля на той же карте параметров. Для определения применимого ответа XML я решил написать все необходимые определения в комментарии конфигурации IdP, что позволяет адаптировать отдельные конфигурации IdP для использования одного и того же кода для разных провайдеров IdP, даже если они используют разные версии SAML. Определения в поле "Комментарий" конфигурации IdP в idpcat.nsf выглядят следующим образом:

Ответ SLO:/idp/SLO.saml2;

SLO Response XML: "<" urn: LogoutResponse ID = "@UUID" Версия = "# Версия" IssueInstant = "@ACTUAL_TIME" Destination = "SLO_Response" InResponseTo = "# ID" xmlns: urn = "# xmlns: урна" > "    "<" urn1: Эмитент xmlns: urn1 = "XML_Parameter1 " > " HTTP_HSP_LISTENERURI "<" /urn 1: Эмитент" > "  " & Л;" Урна: Статус " > "        "<" urn: Значение StatusCode = "XML_Parameter2" /" > "  " & Л;"/мкм: Статус " > " "& Л;" /мкм: LogoutResponse " > " ;

Значения XML: #xmlns: urn = protocol → assertion & #xmlns: urn = protocol → status: Success;

Параметры ответа: RelayState & SigAlg & Signature;

Тип подписи: SHA256withRSA;

KeyStore Type: PKCS12;

Файл KeyStore: D:\saml_cert.pfx;

Пароль KeyStore: **********;

Сертификат: {xxxxxxxxxx}

Ключи в этих определениях отделяются от значений ":", а конец значений указан с помощью ";" (а не новая строка). Это позволяет настроить полную параметризацию ответа SAML в соответствии с требованиями поставщика услуг IdP в соответствующей конфигурации IdP, используемой для подключения SSO. Определения определяются следующим образом:

• SLO Response: Это относительный адрес, в котором ответ SLO должен быть отправлен на соответствующий IdP-сервер.

• SLO Response XML: это текстовая строка, определяющая ответ SLO, структурированный в формате XML (используйте "<" и " > " без "). Строки, идентифицирующие параметры, найденные в карте параметров, обмениваются на их соответствующее значение. Чтобы убедиться, что аналогичные параметры правильно идентифицированы, параметры Cookie имеют ведущие" $"и XML-параметры запроса Request" #". Кроме того, предоставляются 2 формулы, где "@UUID" будет вычислять случайный UUID с помощью правильный формат для параметра идентификатора XML-ответа и "@ACTUAL_TIME" рассчитает правильную отметку времени в формате "Мгновенный" для параметра IssueInstant ответа XML.

• Значения XML: эта текстовая строка идентифицирует дополнительные параметры, где в основном используется известный параметр, но часть значения параметра необходимо обменять, чтобы соответствовать требуемому тексту. Параметры идентифицируются строкой "XML_Paramater", а затем позицией в строке, разделяющей каждое значение с помощью "&" в тексте XML-ответа SLO. Текст для значений XML структурирован с помощью идентификатора параметра, за которым следует "=", и текст, подлежащий замене, а затем "- > " и новый текст.

• Параметры ответа: параметры ответа разделяются "&" и будет добавлен в ответ SLO, как определено. Если требуется подпись, параметры SigAlg и Signature необходимы в этой строке и должны быть помещены в конец.

• Тип подписи: если требуется подпись, тип алгоритма, используемый для вычисления сигнатуры, указан здесь.

• KeyStore Type: тип ключа, используемый для сертификата.

• Файл KeyStore: это файл, в котором хранился KeyStore, включая диск и путь на сервере Lotus Notes. Мы использовали D:\saml_cert.pfx на тестовом сервере.

• Пароль KeyStore: это пароль, необходимый для открытия файла KeyStore и хранимых там сертификатов.

• Сертификат: это псевдоним сертификата, идентифицирующего сертификат в файле KeyStore. Если сертификат хранится в новом файле KeyStore для объединения нескольких сертификатов в одном месте, псевдоним всегда изменяется на новое значение, которое должно быть адаптировано здесь.

Введенный мной код - это агент Java с именем "Выход" в domcfg.nsf, но он может быть реализован в любой базе данных, доступной для пользователей SSO, и он работает как сервер, чтобы обеспечить защиту конфигураций IdP в idpcat.nsf с максимальной безопасностью. На поставщике услуг IdP вам необходимо настроить запрос SLO для Domino Server соответственно на соответствующем веб-сайте как " https://WEBSITE/domcfg.nsf/Logout?Open&", за которым следует SAML Запрос. Если подпись запрашивается поставщиком услуг IdP, вам необходимо сохранить файл KeyStore с сертификатом, включая PrivateKey, который требуется для подписания. Файл KeyStore можно управлять с помощью функции оснастки MMC (см. https://msdn.microsoft.com/en-us/library/ms788967(v=vs.110).aspx). Можно объединить несколько сертификатов в один файл с помощью функции экспорта, но вы должны убедиться, что вы экспортируете личные ключи в файл по соответствующей настройке в мастере экспорта.

Это код агента "Выход" , который выводит пользователя с сервера домино и отправляет ответ выхода SAML поставщику услуг IdP:

import lotus.domino.*;
import java.io.*;
import java.util.*;
import java.text.*;
import com.ibm.xml.crypto.util.Base64;
import java.util.zip.*;
import java.net.URLEncoder;
import java.security.*;

public class JavaAgent extends AgentBase {
    public void NotesMain() {
        try {
            Session ASession = getSession();
            AgentContext AContext = ASession.getAgentContext();
            DateTime date = ASession.createDateTime("Today 06:00");
            int timezone = date.getTimeZone();

            Database DB = AContext.getCurrentDatabase();
            String DBName = DB.getFileName();
            DBName = DBName.replace("\\", "/").replace(" ", "+");

            //Load PrintWriter to printout values for checking (only to debug)
            //PrintWriter pwdebug = getAgentOutput();
            //pwdebug.flush();

            //Load Data from Logout Request
            Document Doc = AContext.getDocumentContext();
            Vector<?> items = Doc.getItems();
            Map<String, String> Params = new LinkedHashMap<String, String>();
            for (int j=0; j<items.size(); j++) {
                Item item = (Item)items.elementAt(j);
                if (!item.getValueString().isEmpty()) Params.put(item.getName(), item.getValueString());
            }
            String ServerName = Params.get("HTTP_HSP_HTTPS_HOST");
            int pos = ServerName.indexOf(":");
            ServerName = pos > 0 ? ServerName.substring(0, ServerName.indexOf(":")) : ServerName;
            Params.put("ServerName", ServerName);
            Doc.recycle();
            DB.recycle();

            //Load Cookie Variables
            Params = map(Params, Params.get("HTTP_COOKIE"), "$", "; ", "=", false, false);
            //Load Query Variables
            Params = map(Params, Params.get("QUERY_STRING_DECODED"), "", "&", "=", false, false);
            //Decode and Infalte SAML Request
            String RequestUnziped = decode_inflate(Params.get("SAMLRequest"), true);
            //pwdebug.println("Request unziped: " + RequestUnziped);
            //System.out.println("Request unziped: " + RequestUnziped);
            String RequestXMLParams = RequestUnziped.substring(19, RequestUnziped.indexOf("\">"));
            //Load XML Parameters from Request
            Params = map(Params, RequestXMLParams, "#", "\" ", "=\"", false, false);
            //for (Map.Entry<String, String> entry : Params.entrySet()) pwdebug.println(entry.getKey() + " value: " + entry.getValue());
            //for (Map.Entry<String, String> entry : Params.entrySet()) System.out.println(entry.getKey() + " value: " + entry.getValue());
            String Issuer = RequestUnziped.substring(RequestUnziped.indexOf(":Issuer"), RequestUnziped.indexOf("Issuer>"));
            Issuer = Issuer.substring(Issuer.indexOf(">") + 1, Issuer.indexOf("<"));
            Params.put("SLO_Issuer", Issuer);

            //Load Parameters for the Response
            DbDirectory Dir = ASession.getDbDirectory(null);
            Database idpcat = Dir.openDatabase("idpcat.nsf");
            View idpView = idpcat.getView("($IdPConfigs)");
            Document idpDoc = idpView.getDocumentByKey(ServerName, false);
            items = idpDoc.getItems();
            for (int j=0; j<items.size(); j++) {
                Item item = (Item)items.elementAt(j);
                if (!item.getValueString().isEmpty()) Params.put(item.getName(), item.getValueString());
            }
            Params = map(Params, idpDoc.getItemValueString("Comments"), "", ";", ": ", false, false);
            Params.put("SLO_Response", Issuer + Params.get("SLO Response"));
            Params.put("@UUID", "_" + UUID.randomUUID().toString());
            Params.put("@ACTUAL_TIME", actualTime(Params.get("#IssueInstant"), Params.get("#NotOnOrAfter"), timezone));
            //for (Map.Entry<String, String> entry : Params.entrySet()) pwdebug.println(entry.getKey() + " value: " + entry.getValue());
            //for (Map.Entry<String, String> entry : Params.entrySet()) System.out.println(entry.getKey() + " value: " + entry.getValue());
            idpDoc.recycle();
            idpView.recycle();
            idpcat.recycle();
            Dir.recycle();

            //Setup XML Response as defined
            String ResponseString = Params.get("SLO Response XML");
            for (Iterator<String> itRq = Params.keySet().iterator(); itRq.hasNext();) {
                String Key = (String) itRq.next();
                ResponseString = ResponseString.replace(Key, Params.get(Key));
            }
            //pwdebug.println("Response String replaced: " + ResponseString);
            //System.out.println("Response String replaced: " + ResponseString);
            //Load Values to be exchanged in the defined Response
            Map<String, String> RsXMLValues = map(new LinkedHashMap<String, String>(), Params.get("XML Values"), "", "&", "=", true, false);
            //for (Map.Entry<String, String> entry : RsXMLValues.entrySet()) pwdebug.println(entry.getKey() + " value: " + entry.getValue());
            //for (Map.Entry<String, String> entry : RsXMLValues.entrySet()) System.out.println(entry.getKey() + " value: " + entry.getValue());
            //Exchange defined Strings with Values from the Request
            int itc = 0;
            for (Iterator<String> itRXV = RsXMLValues.keySet().iterator(); itRXV.hasNext();) {
                itc = itc + 1;
                String Key = (String) itRXV.next();
                int lock = Key.indexOf(" -> ");
                String KeyRq = lock > 0 ? Key.substring(0, lock) : Key;
                int lockRq = KeyRq.indexOf(" ");
                KeyRq = lockRq > 0 ? KeyRq.substring(0, lockRq) : KeyRq;
                String Parameter = Params.get(KeyRq);
                String Value = RsXMLValues.get(Key);
                if (!Value.isEmpty()) {
                    int locv = Value.indexOf(" -> ");
                    String ValueS = locv > 0 ? Value.substring(0, locv) : Value;
                    String ValueR = locv > 0 && Value.length() > locv + 4 ? Value.substring(locv + 4) : ValueS;
                    Parameter = Parameter.replace(ValueS, ValueR);
                }
                ResponseString = ResponseString.replace(("XML_Parameter" + itc), Parameter);
            }
            //pwdebug.println("Final XML Response String: " + ResponseString);
            //System.out.println("Final XML Response String: " + ResponseString);
            //Deflate and Encode the XML Response
            String ResponseZiped = deflate_encode(ResponseString, Deflater.DEFAULT_COMPRESSION, true);
            //pwdebug.println("Response Ziped: " + ResponseZiped);
            //System.out.println("Response Ziped: " + ResponseZiped);
            //Setup Response URLQuery as defined
            String ResponseEncoded = "SAMLResponse=" + URLEncoder.encode(ResponseZiped, "UTF-8");
            //pwdebug.println("Response to Sign: " + ResponseEncoded);
            //System.out.println("Response to Sign: " + ResponseEncoded);
            //Load Parameters to be added to the Response
            Map<String, String> ResponseParams = map(new LinkedHashMap<String, String>(), Params.get("Response Parameters"), "", "&", "=", false, true);
            //for (Map.Entry<String, String> entry : ResponseParams.entrySet()) pwdebug.println(entry.getKey() + " value: " + entry.getValue());
            //for (Map.Entry<String, String> entry : ResponseParams.entrySet()) System.out.println(entry.getKey() + " value: " + entry.getValue());
            //Add defined Parameters with Values from the Request
            for (Iterator<String> itRP = ResponseParams.keySet().iterator(); itRP.hasNext();) {
                String Key = (String) itRP.next();
                if (Key.contains("Signature")) {
                    //pwdebug.println("Response to Sign: " + ResponseEncoded);
                    //System.out.println("Response to Sign: " + ResponseEncoded);
                    Signature signature = Signature.getInstance(Params.get("Signature Type"));
                    //pwdebug.println("Signature: Initiated");
                    //System.out.println("Signature: Initiated");
                    KeyStore keyStore = KeyStore.getInstance(Params.get("KeyStore Type"));
                    //pwdebug.println("Key Store: Initiated");
                    //System.out.println("Key Store: Initiated");
                    keyStore.load(new FileInputStream(Params.get("KeyStore File")), Params.get("KeyStore Password").toCharArray());
                    //pwdebug.println("Key Store: Loaded");
                    //System.out.println("Key Store: Loaded");
                    PrivateKey key = (PrivateKey) keyStore.getKey (Params.get("Certificate"), Params.get("KeyStore Password").toCharArray());
                    //pwdebug.println("Key Store: Private Key Loaded");
                    //System.out.println("Key Store: Private Key Loaded");
                    signature.initSign(key);
                    //pwdebug.println("Signature: Private Key Initiated");
                    //System.out.println("Signature: Private Key Initiated");
                    signature.update(ResponseEncoded.getBytes("UTF-8"));
                    //pwdebug.println("Signature: Signed");
                    //System.out.println("Signature: Signed");
                    String ResponseSignature = URLEncoder.encode(Base64.encode(signature.sign()), "UTF-8"); 
                    //pwdebug.println("Signature: Signed");
                    //System.out.println("Signature: Signed");
                    ResponseEncoded = ResponseEncoded.concat("&").concat(Key).concat("=").concat(ResponseSignature);
                }
                else ResponseEncoded = ResponseEncoded.concat("&").concat(Key).concat("=").concat(URLEncoder.encode(Params.get(Key), "UTF-8"));
            }
            String ResponseURL = Params.get("SLO_Response").concat("?").concat(ResponseEncoded);
            //pwdebug.println("Final Response URL: " + ResponseURL);
            //pwdebug.close();
            //System.out.println("Final Response URL: " + ResponseURL);

            //Send Logout to Server and redirect to Response to defined Destination
            PrintWriter pwsaml = getAgentOutput();
            pwsaml.flush();
            pwsaml.println("[" + Params.get("HTTP_HSP_LISTENERURI") + "/" + DBName + "?logout&redirectto=" + URLEncoder.encode(ResponseURL, "UTF-8") + "]");
            pwsaml.close();

            //Recycle Agent and Session
            AContext.recycle();
            ASession.recycle();

        } catch(Exception e) {
            PrintWriter pwerror = getAgentOutput();
            pwerror.flush();
            pwerror.println(e);
            System.out.println(e);
            pwerror.close();
        } 
    }

    //Load Maps from Strings to identify Paramteres and Values
    private static Map<String, String> map(Map<String, String> map, String input, String keys, String spliting, String pairing, Boolean keycount, Boolean empty) {
        Map<String, String> output = map.isEmpty() ? new LinkedHashMap<String, String>() : map;
        String[] Pairs = input.split(spliting);
        int kc = 0;
        for (String Pair : Pairs) {
            kc = kc + 1;
            int pos = Pair.indexOf(pairing);
            String Key = pos > 0 ? Pair.substring(0, pos) : Pair;
            if (keycount) Key = Key + " " + kc;
            String Value = pos > 0 && Pair.length() > (pos + pairing.length()) ? Pair.substring(pos + pairing.length()) : "";
            if (!output.containsKey(Key) && (empty || !Value.trim().isEmpty())) output.put((keys + Key).trim(), Value.trim());
        }
        return output;
    }

    //Decode and Inflate to XML
    private static String decode_inflate(String input, Boolean infflag) throws IOException, DataFormatException {
        byte[] inputDecoded = Base64.decode(input.getBytes("UTF-8"));
        Inflater inflater = new Inflater(infflag);
        inflater.setInput(inputDecoded);
        byte[] outputBytes = new byte[1024];
        int infLength = inflater.inflate(outputBytes);
        inflater.end();
        String output = new String(outputBytes, 0, infLength, "UTF-8");
        return output;
    }

    //Deflate and Encode XML
    private static String deflate_encode(String input, int level , Boolean infflag) throws IOException {
        byte[] inputBytes = input.getBytes("UTF-8");
        Deflater deflater = new Deflater(level, infflag);
        deflater.setInput(inputBytes);
        deflater.finish();
        byte[] outputBytes = new byte[1024];
        int defLength = deflater.deflate(outputBytes);
        deflater.end();
        byte[] outputDeflated = new byte[defLength];
        System.arraycopy(outputBytes, 0, outputDeflated, 0, defLength);
        String output = Base64.encode(outputDeflated);
        return output;
    }

    //Define Date and Time Formats
    private static SimpleDateFormat DateFormat = new SimpleDateFormat("yyyy-MM-dd");
    private static SimpleDateFormat TimeFormat = new SimpleDateFormat("HH:mm:ss.SSS");

    //Formated Actual Time
    private static String actualTime(String minTime, String maxTime, int localZone) throws ParseException {
        Date actualtime = new Date();
        long acttime = actualtime.getTime();
        long mintime = resetTime(minTime, localZone);
        long maxtime = resetTime(maxTime, localZone);
        acttime = (acttime > mintime) && (acttime < maxtime) ? acttime: mintime + 1000;
        return formatTime(acttime);
    }

    //Reset timemillis from String as defined
    private static long resetTime(String givenTime, int localZone) throws ParseException {
        Date date = DateFormat.parse(givenTime.substring(0, givenTime.indexOf("T")));
        long days = date.getTime();
        Date time = TimeFormat.parse(givenTime.substring(givenTime.indexOf("T") + 1, givenTime.indexOf("Z")));
        long hours = time.getTime();
        long zonecorr = localZone * 3600000;
        return days + hours - zonecorr;
    }

    //Format timemillis into a String as required
    private static String formatTime(long totalmilliSeconds) {
        long date = 86400000 * (totalmilliSeconds / 86400000);
        long time = totalmilliSeconds % 86400000;
        String dateString = DateFormat.format(date).concat("T");
        String timeString = TimeFormat.format(time).concat("Z");
        return dateString.concat(timeString);
    }

    public static String noCRLF(String input) { 
        String lf = "%0D";
        String cr = "%0A";
        String find = lf;
        int pos = input.indexOf(find);
        StringBuffer output = new StringBuffer();
        while (pos != -1) {
            output.append(input.substring(0, pos));
            input = input.substring(pos + 3, input.length());
            if (find.equals(lf)) find = cr;
            else find = lf;
            pos = input.indexOf(find);
        }
        if (output.toString().equals("")) return input;
        else return output.toString();
    }
}

Как вы могли заметить, для отладки агента можно использовать несколько строк с комментариями, если определения неверны и не приводят к успешному завершению работы. Вы можете легко изменить эти строки, удалив "//", начиная с этих строк, и распечатайте параметры, которые вы хотели бы видеть на экране, или отправите их в журналы.

Чтобы инициировать SLO на сервере domino, я написал еще один Java-агент, используя ту же концепцию. Агент называется startSLO и находится в той же базе данных, что и агент "Выход" . Использование этого агента можно легко реализовать в любом из ваших приложений, создав кнопки, открывающие относительный URL-адрес "/domcfg.nsf/startSLO? Open". Агент "startSLO" имеет следующий код.:

import lotus.domino.*;
import java.io.*;

public class JavaAgent extends AgentBase {
    public void NotesMain() {
        try {
            Session ASession = getSession();
            AgentContext AContext = ASession.getAgentContext();

            Database DB = AContext.getCurrentDatabase();
            String DBName = DB.getFileName();
            DBName = DBName.replace("\\", "/").replace(" ", "+");

            //Load Data from Logout Request
            Document Doc = AContext.getDocumentContext();
            String ServerName = Doc.getItemValueString("HTTP_HSP_HTTPS_HOST");
            int pos = ServerName.indexOf(":");
            ServerName = pos > 0 ? ServerName.substring(0, ServerName.indexOf(":")) : ServerName;
            String Query = Doc.getItemValueString("Query_String");
            pos = Query.indexOf("?Open&");
            Query = pos > 0 ? "?" + Query.substring(Query.indexOf("?Open") + 6) : "";
            Doc.recycle();
            DB.recycle();

            //Load Parameters for the Response
            DbDirectory Dir = ASession.getDbDirectory(null);
            Database idpcat = Dir.openDatabase("idpcat.nsf");
            View idpView = idpcat.getView("($IdPConfigs)");
            Document idpDoc = idpView.getDocumentByKey(ServerName, false);
            String SAMLSLO = idpDoc.getItemValueString("SAMLSloUrl");
            idpDoc.recycle();
            idpView.recycle();
            idpcat.recycle();
            Dir.recycle();

            //Send Logout to Server and redirect to Response to defined Destination
            PrintWriter pwsaml = getAgentOutput();
            pwsaml.flush();
            pwsaml.println("[" + SAMLSLO + Query + "]");
            pwsaml.close();

            //Recycle Agent and Session
            AContext.recycle();
            ASession.recycle();

        } catch(Exception e) {
            PrintWriter pwerror = getAgentOutput();
            pwerror.flush();
            pwerror.println(e);
            System.out.println(e);
            pwerror.close();
        } 
    }
}