Моя цель - реализовать протокол Single Log Out. Сначала я понимаю, как работает стандарт и как я могу поместить его в свой сценарий: ADFS 2.0 как IdP, для меня это как "черный ящик"
Но IdP отвечает мне: Ошибка подтверждения сигнатуры сообщения SAML.
Для подписания я использую свой закрытый ключ (2048 байт), и для проверки предполагается, что IdP использует мой открытый ключ (тот, который я отправил, когда я зарегистрировал свой хост)
Ответ 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();
}
}
}