Como teníamos que seguir muchos pasos para finalmente lograr la implementación correcta de SLO en Domino 9.0.1, decidí escribir un código que permitiera utilizar cualquier configuración IdP (futura) para ejecutar con nuestros servidores Domino. He implementado la siguiente estrategia:
- uso tanto de la información disponible a partir de una solicitud SAML Salir de entrada como sea posible
- Identificar la configuración en el IdP idpcat.nsf para encontrar la información correspondiente sobre el IdP SLO Respuesta a ser enviado al proveedor de servicios IdP (servidor SAML)
- Defina la respuesta de cierre de sesión de SAML en la configuración de IdP correspondiente en idpcat.nsf para permitir la adaptación dinámica a nuevos requisitos si la configuración de SAML cambia.
Como resultado, el código lee todos los campos de la solicitud SAML Salir entrante en un mapa de parámetros y decodifica e infla la cadena de consulta para extraer los parámetros XML de la solicitud en el mapa de parámetros. Como se pueden configurar diferentes sitios web en el servidor de domino para diferentes proveedores de servicios IdP para permitir la conexión de SSO, identifico la configuración de IdP con el "nombre de host" correspondiente y leo todos sus campos en el mismo Mapa de parámetros. Para definir una Respuesta XML aplicable, decidí escribir todas las definiciones necesarias en la configuración de Comment of the IdP, que permite adaptar configuraciones individuales de IdP para usar el mismo código para diferentes proveedores de IdP, incluso si usan diferentes versiones de SAML. Las definiciones en el campo Comentario de la configuración de IdP en idpcat.nsf son como las siguientes:
Respuesta SLO:/idp/SLO.saml2;
XML
SLO Respuesta: "< "urn: LogoutResponse ID =" @ UUID" versión = "# Versión" IssueInstant = Destino "@ ACTUAL_TIME" = "SLO_Response" InResponseTo = xmlns "# ID": Urna = "# xmlns : urna ">" "< "urn1: Emisor xmlns: urn1 =" XML_Parameter1" ">" HTTP_HSP_LISTENERURI "< "/ urn1: Emisor">" "< "urn: Status">" "<" urn: StatusCode value = "XML_Parameter2"/">" "< "/ urn: Status">" "< "/ urn: LogoutResponse">";
valores XML: #xmlns: urna = Protocolo -> aseveración & #xmlns: urn = protocolo -> estado: éxito;
Parámetros de respuesta: RelayState & SigAlg & Signature;
Tipo de firma: SHA256withRSA;
KeyStore Tipo: PKCS12;
Archivo de KeyStore: D: \ saml_cert.pfx;
KeyStore Contraseña: **********;
Certificado: {xxxxxxxxxx}
las claves de este definiciones se separan de los Valores con ":" y el final de los valores se especifica con ";" (no la nueva línea) Esto permite configurar una parametrización completa de la respuesta SAML según lo requiera el proveedor de servicios IdP en la configuración IdP correspondiente utilizada para la conexión SSO. Las definiciones se especifican de la siguiente manera:
• Respuesta SLO: esta es la dirección relativa a la cual debe enviarse la respuesta SLO en el servidor IdP respectivo.
• SLO Response XML: cadena de texto que define SLO Response estructurado en formato XML (Use "<" y ">" sin "). Las cadenas que identifican los parámetros encontrados en el Mapa de parámetros se intercambian a sus respectivos valores. Para asegurarse de que los parámetros similares se identifican correctamente, los parámetros de Cookie tienen un "$" inicial y los parámetros XML de la consulta de solicitud un "#" inicial. Además, se proporcionan 2 fórmulas, donde "@UUID" calculará un UUID aleatorio con el el formato correcto para el parámetro ID de la respuesta XML y "@ACTUAL_TIME" calculará la marca de tiempo correcta en el formato Instant para el parámetro IssueInstant de la respuesta XML.
• Valores XML: esta cadena de texto identifica parámetros adicionales, donde básicamente se usa un parámetro conocido, pero una parte del valor del parámetro debe intercambiarse para que coincida con el texto requerido. Los parámetros se identifican mediante la cadena "XML_Paramater" seguida de la posición en la cadena que separa cada valor con "&" en el texto XML de Respuesta SLO. El texto para los valores XML se estructura teniendo la identificación del parámetro seguida por "=" y el texto que se reemplazará seguido de "->" y el nuevo texto.
• Parámetros de respuesta: los parámetros de respuesta se separan con "&" y se agregarán a la Respuesta SLO como se define. Si se requiere una firma, los parámetros SigAlg y Firma son necesarios en esta cadena y deben colocarse al final.
• Tipo de firma: si se requiere una firma, aquí se especifica el tipo de algoritmo utilizado para calcular la firma.
• KeyStore Type: este es el tipo de KeyStore utilizado para el certificado.
• Archivo de KeyStore: este es el archivo donde KeyStore se ha guardado, incluida la unidad y la ruta en el servidor de Lotus Notes. Usamos D: \ saml_cert.pfx en el servidor de prueba.
• Contraseña de KeyStore: esta es la contraseña requerida para abrir el archivo KeyStore y los certificados almacenados en el mismo.
• Certificado: este es el alias del certificado que identifica el certificado en el archivo KeyStore. Si un Certificado se almacena en un nuevo Archivo KeyStore para combinar varios Certificados en una ubicación, el Alias siempre se cambia a un nuevo valor, que debe adaptarse aquí.
El código que implementé es un Agente de Java con el nombre "Cerrar sesión" en el archivo domcfg.nsf, pero básicamente podría implementarse en cualquier base de datos disponible para los usuarios de SSO y se ejecuta como el servidor para permitir la protección del IdP configuraciones en idpcat.nsf con la mayor seguridad. En el proveedor de servicios IdP, debe configurar la solicitud de SLO para el servidor de Domino, respectivamente, el sitio web correspondiente, como "https://WEBSITE/domcfg.nsf/Logout?Open&", seguido de la solicitud de SAML. Si el proveedor de servicios IdP solicita la firma, debe almacenar un archivo KeyStore con el certificado, incluida la clave privada requerida para firmar. El archivo KeyStore se puede administrar utilizando la función del complemento MMC (consulte https://msdn.microsoft.com/en-us/library/ms788967(v=vs.110).aspx). Es posible combinar varios certificados en un archivo mediante la función de exportación, pero debe asegurarse de exportar las claves privadas al archivo mediante la configuración correspondiente en el asistente de exportación.
Este es el código para el agente "Salir", que cierra la sesión del usuario del servidor Domino y envía el SAML Salir Respuesta al proveedor de servicios 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();
}
}
Como ya habrán reconocido, varios Las líneas comentadas se pueden usar para depurar el agente, si las definiciones no son correctas y no dan como resultado un cierre de sesión exitoso. Puede cambiar fácilmente esas líneas eliminando el "//" que comienza esas líneas e imprime los parámetros que le gustaría ver en su pantalla o los envía a los registros.
Para iniciar SLO en el servidor de domino, escribí otro agente de Java utilizando el mismo concepto. El agente se llama startSLO y se encuentra en la misma base de datos que el agente de "Cerrar sesión". El uso de este agente se puede implementar fácilmente en cualquiera de sus aplicaciones creando botones que abren la URL relativa "/domcfg.nsf/startSLO?Open". El agente "startSLO" tiene el siguiente código .:
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();
}
}
}
Ahora recibo esta excepción: ** Longitud no válida para una matriz de caracteres Base-64 **. Básicamente eliminé el atributo NameQualifier de la solicitud y estoy usando rsa-sha256 en lugar del anterior, paso a paso ... – Gaucho
Último registro de ADFS2: ADFS2: ** La solicitud de SAML no está firmada con el algoritmo de firma esperado. \t Firmado con: http://www.w3.org/2001/04/xmldsig-more#rsa-sha256 \t Esperado: http://www.w3.org/2000/09/xmldsig#rsa-sha1* * – Gaucho
Bien, el problema es el algoritmo. Estoy generando la firma usando ** SHA1withRSA **. Necesitaría usar ** http: //www.w3.org/2000/09/xmldsig#rsa-sha1 ". Usar la [Java XML API] (http://java.sun.com/developer/technicalArticles/ xml/dig_signature_api /) Puedo generar un xml SignedAuthnRequest, pero necesitaría aplicar HTTP-Redirect (SAMLRequest = value & SigAlg = value & Signature = value) ... – Gaucho