由于我们需要采取许多步骤才能最终在 Domino 9.0.1 上成功实现 SLO,因此我决定编写代码,允许使用任何(未来的)IdP 配置在我们的 Domino 服务器上运行。我实施了以下策略:
- 尽可能多地使用传入 SAML 注销请求中的可用信息
- 识别 idpcat.nsf 中的 IdP 配置以查找有关要发送到 IdP 服务提供商(SAML 服务器)的 IdP SLO 响应的相应信息
- 在 idpcat.nsf 中的相应 IdP 配置中定义 SAML 注销响应,以便在 SAML 配置更改时动态适应新要求。
结果,代码将传入的 SAML 注销请求的所有字段读取到参数映射中,并解码和扩展查询字符串以将请求的 XML 参数提取到参数映射中。由于 domino 服务器上的不同网站可以为不同的 IdP 服务提供商配置以允许 SSO 连接,因此我使用相应的“主机名”来识别 IdP 配置,并在同一个 Parameter Map 中读取其所有字段。为了定义适用的 XML 响应,我决定将所有需要的定义写入 IdP 配置的 Comment 中,这允许调整单个 IdP 配置以对不同的 IdP 提供者使用相同的代码,即使使用不同的 SAML 版本。idpcat.nsf 中 IdP 配置的 Comment 字段中的定义如下所示:
SLO 响应:/idp/SLO.saml2;
SLO 响应 XML:"<"urn:LogoutResponse ID="@UUID" Version="#Version" IssueInstant="@ACTUAL_TIME" Destination="SLO_Response" InResponseTo="#ID" xmlns:urn="#xmlns:urn"> " "<"urn1:Issuer xmlns:urn1="XML_Parameter1"">"HTTP_HSP_LISTENERURI"<"/urn1:Issuer">" "<"urn:Status">" "<"urn:StatusCode Value="XML_Parameter2"/" >" "<"/urn:Status">" "<"/urn:LogoutResponse">";
XML 值:#xmlns:urn=protocol -> 断言&#xmlns:urn=protocol -> status:Success;
响应参数:RelayState&SigAlg&Signature;
签名类型:SHA256withRSA;
密钥库类型:PKCS12;
密钥库文件:D:\saml_cert.pfx;
密钥库密码:**********;
证书:{xxxxxxxxx}
此定义中的键与值用“:”分隔,值的结尾用“;”指定 (不是新行)这允许根据 IdP 服务提供商的要求在用于 SSO 连接的相应 IdP 配置中设置 SAML 响应的完整参数化。定义如下:
• SLO 响应:这是相对地址,SLO 响应必须发送到相应的 IdP 服务器上。
• SLO 响应 XML:这是定义以 XML 格式结构化的 SLO 响应的文本字符串(使用“<”和“>”,不带“)。在参数映射中找到的标识参数的字符串被交换为它们各自的值。为了确保类似的参数被正确识别 Cookie 参数有一个前导“$”,请求查询的 XML 参数有一个前导“#”。另外提供了 2 个公式,其中“@UUID”将计算一个具有正确格式的随机 UUID XML 响应的 ID 参数和“@ACTUAL_TIME”将为 XML 响应的 IssueInstant 参数计算即时格式的正确时间戳。
• XML 值:此文本字符串标识附加参数,其中基本上使用已知参数,但需要交换部分参数值以匹配所需文本。参数由字符串“XML_Paramater”标识,后跟字符串中的位置,在 SLO 响应 XML 文本中用“&”分隔每个值。XML 值的文本结构如下:参数标识后跟“=”,要替换的文本后跟“->”和新文本。
• 响应参数:响应参数用“&”分隔,并将按定义添加到 SLO 响应中。如果需要签名,则此字符串中需要参数 SigAlg 和 Signature,并且应放在末尾。
• 签名类型:如果需要签名,则在此处指定用于计算签名的算法类型。
• KeyStore 类型:这是用于证书的 KeyStore 的类型。
• KeyStore 文件:这是保存 KeyStore 的文件,包括 Lotus Notes 服务器上的驱动器和路径。我们在测试服务器上使用了 D:\saml_cert.pfx。
• KeyStore 密码:这是打开 KeyStore 文件和存储在其中的证书所需的密码。
• 证书:这是在密钥库文件中标识证书的证书的别名。如果将证书存储在新的 KeyStore 文件中以将多个证书组合在一个位置,则别名始终更改为新值,必须在此处进行调整。
我实现的代码是 domcfg.nsf 中名称为“Logout”的 Java 代理,但它基本上可以在 SSO 用户可用的任何数据库中实现,它作为服务器运行以允许保护 idpcat 中的 IdP 配置.nsf 具有最高的安全性。在 IdP 服务提供商上,您必须分别将相应网站的 Domino 服务器的 SLO 请求配置为“ https://WEBSITE/domcfg.nsf/Logout?Open& ”,然后是 SAML 请求。如果 IdP 服务提供商要求签名,您必须存储一个带有证书的 KeyStore 文件,其中包括签名所需的 PrivateKey。可以使用 MMC 管理单元功能管理 KeyStore 文件(请参阅https://msdn.microsoft.com/en-us/library/ms788967(v=vs.110).aspx)。可以通过导出功能将多个证书合并到一个文件中,但您必须确保通过导出向导中的相应设置将私钥导出到文件中。
这是“注销”代理的代码,它将用户从 domino 服务器注销并将 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();
}
}
您可能已经认识到,如果定义不正确并且不会导致成功注销,则可以使用几行注释行来调试代理。您可以通过删除开始这些行的“//”轻松更改这些行,并打印出您希望在屏幕上看到的参数或将它们发送到日志。
为了在 domino 服务器上启动 SLO,我使用相同的概念编写了另一个 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();
}
}
}