Поскольку у нас было много шагов, чтобы наконец успешно реализовать SLO на Domino 9.0.1, я решил написать код, который позволит использовать любую (будущую) конфигурацию IdP для работы с нашими серверами Domino. Я реализовал следующую стратегию:
- Используйте как можно больше информации из входящего запроса на выход из SAML
- Определите конфигурацию IdP в idpcat.nsf, чтобы найти соответствующую информацию об ответе IdP SLO, который должен быть отправлен поставщику услуг IdP (сервер SAML)
- Определите Ответ выхода из SAML в соответствующей конфигурации IdP в idpcat.nsf, чтобы разрешить динамическую адаптацию к новым требованиям в случае изменения конфигурации SAML.
В результате код считывает все поля входящего запроса на выход из SAML в карту параметров, декодирует и раздувает строку запроса для извлечения XML-параметров запроса в карту параметров. Поскольку разные веб-сайты на сервере домино могут быть настроены для разных поставщиков услуг IdP, чтобы разрешить SSO-соединение, я идентифицирую конфигурацию IdP с соответствующим «именем хоста» и считываю все ее поля в одной и той же карте параметров. Для определения применимого XML-ответа я решил записать все необходимые определения в комментарий конфигурации IdP, который позволяет адаптировать отдельные конфигурации IdP для использования одного и того же кода для разных поставщиков IdP, даже если используются разные версии SAML. Определения в поле «Комментарии» конфигурации IdP в idpcat.nsf выглядят следующим образом:
Ответ SLO: /idp/SLO.saml2;
XML ответа SLO: "<" urn: LogoutResponse ID = "@ UUID" Version = "# Version" IssueInstant = "@ ACTUAL_TIME" Destination = "SLO_Response" InResponseTo = "# ID" xmlns: urn = "# xmlns: урна «>»
"<" urn1: Issuer xmlns: urn1 = "XML_Parameter1" ">« HTTP_HSP_LISTENERURI "<" / urn1: Issuer ">"
"<" Урна: Status ">"
"<" urn: StatusCode Value = "XML_Parameter2" / ">"
"<" / Урна: Status ">"
"<" / Урна: LogoutResponse ">";
Значения XML: #xmlns: urn = протокол -> утверждение & # xmlns: urn = протокол -> статус: успех;
Параметры ответа: RelayState & SigAlg & Signature;
Тип подписи: SHA256 с RSA;
Тип хранилища ключей: PKCS12;
Файл хранилища ключей: D: \ saml_cert.pfx;
Пароль хранилища ключей: **********;
Сертификат: {xxxxxxxxxx}
Ключи в этих определениях отделяются от значений с помощью «:», а конец значений указывается с помощью «;» (не новая строка) Это позволяет настроить полную параметризацию ответа SAML в соответствии с требованиями поставщика услуг IdP в соответствующей конфигурации IdP, используемой для соединения SSO.
Определения указаны следующим образом:
• Ответ SLO: это относительный адрес, на который необходимо отправить ответ SLO на соответствующем сервере IdP.
• XML ответа SLO: это текстовая строка, определяющая ответ SLO, структурированный в формате XML (используйте «<» и «>« без »). Строки, идентифицирующие параметры, найденные в карте параметров, заменяются их соответствующими значениями. Убедитесь, что аналогичные параметры определены правильно. Параметры Cookie имеют начальный «$», а параметры XML запроса-запроса - «#». Дополнительно предоставляются 2 формулы, где «@UUID» будет вычислять случайный UUID с правильным формат для параметра ID ответа XML и «@ACTUAL_TIME» вычислит правильную метку времени в формате Instant для параметра IssueInstant ответа XML.
• Значения XML: эта текстовая строка идентифицирует дополнительные параметры, где в основном используется известный параметр, но часть значения параметра необходимо заменить для соответствия требуемому тексту.Параметры обозначаются строкой «XML_Paramater», за которой следует позиция в строке, разделяющей каждое значение символом «&» в тексте XML ответа SLO.Текст для значений XML структурирован таким образом, что за идентификатором параметра следует «=», за заменяемым текстом следует «->» и новый текст.
• Параметры ответа: параметры ответа разделяютсяс "&" и будет добавлен в ответ SLO, как определено.Если требуется подпись, параметры SigAlg и Signature необходимы в этой строке и должны быть помещены в конце.
• Тип подписи: Если требуется подпись, здесь указывается тип алгоритма, используемого для вычисления подписи.
• Тип хранилища ключей: это тип хранилища ключей, используемого для сертификата.
• Файл хранилища ключей: это файл, в котором сохранено хранилище ключей, включая диск и путь к Lotus.Сервер Notes.Мы использовали D: \ saml_cert.pfx на тестовом сервере.
• Пароль хранилища ключей: это пароль, необходимый для открытия файла хранилища ключей и сохраненных в нем сертификатов.
• Сертификат: этоПсевдоним сертификата, идентифицирующий сертификат в файле хранилища ключей.Если сертификат хранится в новом файле хранилища ключей для объединения нескольких сертификатов в одном месте, псевдоним всегда изменяется на новое значение, которое необходимо здесь адаптировать.
Код, который я реализовал, - это агент Java симя «Logout» в domcfg.nsf, но оно может быть в основном реализовано в любой базе данных, доступной для пользователей единого входа, и работает как сервер, чтобы обеспечить защиту конфигураций IdP в idpcat.nsf с максимальной безопасностью.В сервис-провайдере IdP вы должны настроить SLO-запрос для сервера Domino и соответствующего веб-сайта как «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 Logout для поставщика услуг 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 на сервере домино, я написал другой Java-агентиспользуя ту же концепцию.Агент называется startSLO и находится в той же базе данных, что и агент «Выход из системы».Использование этого агента может быть легко реализовано в любом приложении, создавая кнопки, открывающие соответствующий URL-адрес «/domcfg.nsf/startSLO?Open».Агент "startSLO" имеет следующий код:
1063 *