#xpages #lotus-domino #xpages-ssjs
#xpages #lotus-domino #xpages-ssjs
Вопрос:
В моем приложении XPages я хочу программно создать сеанс для веб-пользователя и вернуть браузеру SessionID и DomAuthSessId в виде файлов cookie.
Насколько я знаю, документы person для веб-пользователей содержат только хэш HTTPPassword, и этот хэш нельзя использовать для создания сеанса.
Есть ли способ создать сеанс, не зная пароля пользователя? Например, подписав приложение идентификатором администратора, а затем используя sessionAsSignerWithFullAccess или что-то в этом роде…
Комментарии:
1. Чего вы хотите достичь?
2. @PerHenrikLausten: Я хочу реализовать единый вход (SSO) для пользователей, приходящих из другого (не-domino / не-xpages) веб-приложения. Если пользователь успешно прошел аутентификацию в этом приложении (oauth2) Я могу использовать accesstoken, чтобы получить его / ее почтовый адрес и использовать его для автоматического создания сеанса для этого пользователя на сервере Domino, на котором работает мое приложение XPages.
Ответ №1:
Возможно, вы не захотите этого делать. Если вам нужен доступ к защищенным ресурсам, вы можете использовать клиентские библиотеки http в Domino и выполнить запрос прокси. Или используйте sessionAsSigner для сбора информации и ее возврата.
Однако, если вам необходимо: — в OpenNTF Domino API есть метод создания сеанса пользователя только на основе имени. — sessionAsSigner предоставляет вам сеанс подписывающего
Затем вы можете использовать session.getSessionToken() для получения токена. Ни один из методов не даст вам доступа к зашифрованному контенту
Комментарии:
1. Спасибо за ваш ответ. Как только у меня будет время проверить, работает ли ваш подход, я сообщу об этом здесь.
Ответ №2:
Если вы хотите иметь внешнюю систему, которая проверяет пользователя и выполняет авторизацию, возможно, этоOpenNTF.org xsnippet поможет вам. Потому что с помощью этого вы можете создать токен входа для пользователя. https://openntf.org/XSnippets.nsf/snippet.xsp?id=ltpatoken-generator-for-multi-server-sso-configurations
Комментарии:
1.Спасибо за ваш ответ. Как только у меня будет время проверить, работает ли ваш подход, я сообщу об этом здесь.
Ответ №3:
Несколько лет назад ребята из nil.com написал об этом сообщение в блоге и включил пакет, который поможет вам работать с токенами LTPA. С тех пор оригинальное сообщение в блоге отключилось, но я включу их код ниже — полная благодарность ребятам из Nil, конечно.
Пакет позволяет вам выполнять следующие действия на вашей xpage:
var ltpaSecret=LtpaLibrary.getLtpaSecret(names,siteName);
var token=LtpaLibrary.createLtpaToken(username, ltpaSecret.tokenExpiration,ltpaSecret.ltpaSecret);
response.setHeader("Set-Cookie", "LtpaToken=" token "; domain=" ltpaSecret.tokenDomain "; path=/; HttpOnly; secure");
Что это делает, так это:
- Получите секрет токена ltpa для указанного интернет-сайта в указанном каталоге Domino
- Создайте новый ltpatoken для пользователя и используйте секрет, чтобы подписать его и установить срок действия
- Верните новый ltpatoken в браузер в виде файла cookie
Излишне говорить — вам нужно быть очень осторожным с этим и быть абсолютно уверенным, что никто не сможет злоупотребить этим. Одним из способов может быть поместить это в выделенный NSF, к которому имеют доступ только сервер и один пользователь — пользователь, являющийся учетной записью службы для использования системой единого входа. Даже администраторы сервера не должны получать обычный доступ к NSF.
Система единого входа вызовет вашу xpage, передаст ей имя пользователя, примет возвращенный файл cookie и передаст его пользователю, прежде чем перенаправить их в приложение, к которому они запрашивали доступ.
Оригинальный пакет от nil.com:
package com.nil.ltpa;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.Vector;
import lotus.domino.Database;
import lotus.domino.Document;
import lotus.domino.Item;
import lotus.domino.NotesError;
import lotus.domino.NotesException;
import lotus.domino.View;
import com.nil.exception.Base64DecodeException;
import com.nil.helpers.HttpUtils;
public class LtpaLibrary {
private static final byte[] ltpaTokenVersion = { 0, 1, 2, 3 };
private static final int dateStringLength = 8;
private static final String dateStringFiller = "00000000";
private static final int creationDatePosition = ltpaTokenVersion.length;
private static final int expirationDatePosition = creationDatePosition dateStringLength;
private static final int preUserDataLength = ltpaTokenVersion.length dateStringLength dateStringLength;
private static final int hashLength = 20;
/** This method parses the LtpaToken cookie received from the web browser and returns the information in the <tt>TokenData</tt>
* class.
* @param ltpaToken - the cookie (base64 encoded).
* @param ltpaSecretStr - the contents of the <tt>LTPA_DominoSecret</tt> field from the SSO configuration document.
* @return The contents of the cookie. If the cookie is invalid (the hash - or some other - test fails), this method returns
* <tt>null</tt>.
* @throws NoSuchAlgorithmException
* @throws Base64DecodeException
*/
public static TokenData parseLtpaToken( String ltpaToken, String ltpaSecretStr ) throws NoSuchAlgorithmException,
Base64DecodeException {
byte[] data = HttpUtils.base64Decode( ltpaToken );
int variableLength = data.length - hashLength;
/* Compare to 20 to since variableLength must be at least (preUserDataLength 1) [21] character long:
* Token version: 4 bytes
* Token creation: 8 bytes
* Token expiration: 8 bytes
* User name: variable length > 0
*/
if( variableLength <= preUserDataLength ) return null;
byte[] ltpaSecret = HttpUtils.base64Decode( ltpaSecretStr );
if( !validateSHA( data, variableLength, ltpaSecret ) ) return null;
if( !compareBytes( data, 0, ltpaTokenVersion, 0, ltpaTokenVersion.length ) ) return null;
TokenData ret = new TokenData();
ret.tokenCreated.setTimeInMillis( (long)Integer.parseInt( getString( data, creationDatePosition, dateStringLength ), 16 ) * 1000 );
ret.tokenExpiration
.setTimeInMillis( (long)Integer.parseInt( getString( data, expirationDatePosition, dateStringLength ), 16 ) * 1000 );
byte[] nameBuffer = new byte[ data.length - ( preUserDataLength hashLength ) ];
System.arraycopy( data, preUserDataLength, nameBuffer, 0, variableLength - preUserDataLength );
ret.username = new String( nameBuffer );
return ret;
}
private static boolean validateSHA( byte[] ltpaTokenData, int variableLength, byte[] ltpaSecret ) throws NoSuchAlgorithmException {
MessageDigest sha1 = MessageDigest.getInstance( "SHA-1" );
byte[] digestData = new byte[ variableLength ltpaSecret.length ];
System.arraycopy( ltpaTokenData, 0, digestData, 0, variableLength );
System.arraycopy( ltpaSecret, 0, digestData, variableLength, ltpaSecret.length );
byte[] digest = sha1.digest( digestData );
if( digest.length > ltpaTokenData.length - variableLength ) return false;
int bytesToCompare = ( digest.length <= ltpaTokenData.length - variableLength ) ? digest.length : ltpaTokenData.length
- variableLength;
return compareBytes( digest, 0, ltpaTokenData, variableLength, bytesToCompare );
}
private static boolean compareBytes( byte[] b1, int offset1, byte[] b2, int offset2, int len ) {
if( len < 0 ) return false;
// offset must be positive, and the compare chunk must be contained the buffer
if( ( offset1 < 0 ) || ( offset1 len > b1.length ) ) return false;
if( ( offset2 < 0 ) || ( offset2 len > b2.length ) ) return false;
for( int i = 0; i < len; i )
if( b1[ offset1 i ] != b2[ offset2 i ] ) return false;
return true;
}
/** Convert bytes from the buffer into a String.
* @param buffer - the buffer to take the bytes from.
* @param offset - the offset in the buffer to start at.
* @param len - the number of bytes to convert into a String.
* @return - A String representation of specified bytes.
*/
private static String getString( byte[] buffer, int offset, int len ) {
if( len < 0 ) return "";
if( ( offset len ) > buffer.length ) return "";
byte[] str = new byte[ len ];
System.arraycopy( buffer, offset, str, 0, len );
return new String( str );
}
/** Create a valid LTPA token for the specified user. The creation time is <tt>now</tt>.
* @param username - the user to create the LTPA token for.
* @param durationMinutes - the duration of the token validity in minutes.
* @param ltpaSecretStr - the LTPA Domino Secret to use to create the token.
* @return - base64 encoded LTPA token, ready for the cookie.
* @throws NoSuchAlgorithmException
* @throws Base64DecodeException
*/
public static String createLtpaToken( String username, int durationMinutes, String ltpaSecretStr ) throws NoSuchAlgorithmException,
Base64DecodeException {
return createLtpaToken( username, new GregorianCalendar(), durationMinutes, ltpaSecretStr );
}
/** Create a valid LTPA token for the specified user.
* @param username - the user to create the LTPA token for.
* @param creationTime - the time the token becomes valid.
* @param durationMinutes - the duration of the token validity in minutes.
* @param ltpaSecretStr - the LTPA Domino Secret to use to create the token.
* @return - base64 encoded LTPA token, ready for the cookie.
* @throws NoSuchAlgorithmException
* @throws Base64DecodeException
*/
public static String createLtpaToken( String username, GregorianCalendar creationTime, int durationMinutes, String ltpaSecretStr )
throws NoSuchAlgorithmException, Base64DecodeException {
// create byte array buffers for both strings
byte[] ltpaSecret = HttpUtils.base64Decode( ltpaSecretStr );
byte[] usernameArray = username.getBytes();
byte[] workingBuffer = new byte[ preUserDataLength usernameArray.length ltpaSecret.length ];
// copy version into workingBuffer
System.arraycopy( ltpaTokenVersion, 0, workingBuffer, 0, ltpaTokenVersion.length );
GregorianCalendar expirationDate = (GregorianCalendar)creationTime.clone();
expirationDate.add( Calendar.MINUTE, durationMinutes );
// copy creation date into workingBuffer
String hex = dateStringFiller Integer.toHexString( (int)( creationTime.getTimeInMillis() / 1000 ) ).toUpperCase();
System
.arraycopy( hex.getBytes(), hex.getBytes().length - dateStringLength, workingBuffer, creationDatePosition,
dateStringLength );
// copy expiration date into workingBuffer
hex = dateStringFiller Integer.toHexString( (int)( expirationDate.getTimeInMillis() / 1000 ) ).toUpperCase();
System.arraycopy( hex.getBytes(), hex.getBytes().length - dateStringLength, workingBuffer, expirationDatePosition,
dateStringLength );
// copy user name into workingBuffer
System.arraycopy( usernameArray, 0, workingBuffer, preUserDataLength, usernameArray.length );
// copy the ltpaSecret into the workingBuffer
System.arraycopy( ltpaSecret, 0, workingBuffer, preUserDataLength usernameArray.length, ltpaSecret.length );
byte[] hash = createHash( workingBuffer );
// put the public data and the hash into the outputBuffer
byte[] outputBuffer = new byte[ preUserDataLength usernameArray.length hashLength ];
System.arraycopy( workingBuffer, 0, outputBuffer, 0, preUserDataLength usernameArray.length );
System.arraycopy( hash, 0, outputBuffer, preUserDataLength usernameArray.length, hashLength );
return HttpUtils.base64Encode( outputBuffer );
}
private static byte[] createHash( byte[] buffer ) throws NoSuchAlgorithmException {
MessageDigest sha1 = MessageDigest.getInstance( "SHA-1" );
return sha1.digest( buffer );
}
private static boolean fieldContainsValue( Vector values, Item item ) throws NotesException {
for( int i = 0; i < values.size(); i ) {
if( item.containsValue( values.get( i ) ) ) return true;
}
return false;
}
/** Get the contents of the LTPA_Secret field of the correct SSO configuration document based on the Internet site host name.
* @param names - the <strong>names.nsf</strong> database.
* @param siteName - the Internet host name of the site to generate/verify Domino cookie for.
* @return A class containing LTPA data, <tt>null</tt> if no matching SSO configuration document was found.
* @throws NotesException
* @throws UnknownHostException
*/
public static LtpaData getLtpaSecret( Database names, String siteName ) throws NotesException {
Vector searchValues = new Vector();
searchValues.add( siteName );
try {
InetAddress addr = InetAddress.getByName( siteName );
searchValues.add( addr.getHostAddress() );
} catch( UnknownHostException e ) {
// do nothing
}
View internetSites = names.getView( "($InternetSites)" );
String ssoQuery = "LtpaToken";
Vector vRecycle = new Vector( internetSites.getEntryCount() );
Document webSite = internetSites.getFirstDocument();
while( webSite != null ) {
vRecycle.add( webSite );
if( webSite.getItemValueString( "Form" ).equalsIgnoreCase( "WebSite" )
amp;amp; webSite.getItemValueString( "Type" ).equalsIgnoreCase( "WebSite" ) ) {
// The correct type of document
if( fieldContainsValue( searchValues, webSite.getFirstItem( "ISiteAdrs" ) ) ) {
ssoQuery = webSite.getItemValueString( "ISiteOrg" ) ":" webSite.getItemValueString( "HTTP_SSOCfg" );
break;
}
}
webSite = internetSites.getNextDocument( webSite );
}
internetSites.recycle( vRecycle ); // recycle all the collected documents
internetSites.recycle();
View ssoConfigs = names.getView( "($WebSSOConfigs)" );
Document ssoConfigDoc = ssoConfigs.getDocumentByKey( ssoQuery );
ssoConfigs.recycle();
if( ssoConfigDoc == null )
throw new NotesException( NotesError.NOTES_ERR_SSOCONFIG, "Site "" siteName "" SSO config document not found." );
LtpaData ret = new LtpaData( ssoConfigDoc.getItemValueString( "LTPA_DominoSecret" ), ssoConfigDoc
.getItemValueInteger( "LTPA_TokenExpiration" ), ssoConfigDoc.getItemValueString( "LTPA_TokenDomain" ) );
ssoConfigDoc.recycle();
return ret;
}
}
package com.nil.ltpa;
public class LtpaData {
public String ltpaSecret;
public String tokenDomain;
public int tokenExpiration;
public LtpaData() {
ltpaSecret = "";
tokenDomain = "";
tokenExpiration = 0;
}
public LtpaData( String ltpaSecret, int tokenExpiration, String tokenDomain ) {
super();
this.ltpaSecret = ltpaSecret;
this.tokenExpiration = tokenExpiration;
this.tokenDomain = tokenDomain;
}
/* (non-Javadoc)
* @see java.lang.Object#toString()
*/
public String toString() {
return "LTPAData {Secret=" ltpaSecret ", expiration=" tokenExpiration ", domain=" tokenDomain "}";
}
}
package com.nil.ltpa;
import java.util.GregorianCalendar;
import java.util.SimpleTimeZone;
public class TokenData {
public static final SimpleTimeZone utcTimeZone = new SimpleTimeZone( 0, "UTC" );
public String username;
public GregorianCalendar tokenCreated;
public GregorianCalendar tokenExpiration;
public TokenData() {
username = "";
tokenCreated = new GregorianCalendar( utcTimeZone );
tokenCreated.setTimeInMillis( 0 );
tokenExpiration = new GregorianCalendar( utcTimeZone );
tokenExpiration.setTimeInMillis( 0 );
}
public String toString() {
StringBuffer buf = new StringBuffer();
buf.append( "[ username:" ).append( username ).append( ", tokenCreated: " ).append( tokenCreated.getTime().toString() );
buf.append( ", tokenExpiration: " ).append( tokenExpiration.getTime().toString() ).append( " ]" );
return buf.toString();
}
}
package com.nil.helpers;
import java.util.Vector;
import com.nil.exception.Base64DecodeException;
public class HttpUtils {
private static final String base64Chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789 /";
public static final String base64Encode( byte[] bytes ) {
if( bytes == null ) return null;
StringBuffer ret = new StringBuffer();
for( int sidx = 0, didx = 0; sidx < bytes.length; sidx = 3, didx = 4 ) {
ret.append( base64Chars.charAt( ( bytes[ sidx ] >>> 2 ) amp; 077 ) );
if( sidx 1 < bytes.length ) {
ret.append( base64Chars.charAt( ( bytes[ sidx 1 ] >>> 4 ) amp; 017 | ( bytes[ sidx ] << 4 ) amp; 077 ) );
if( sidx 2 < bytes.length )
ret.append( base64Chars.charAt( ( bytes[ sidx 2 ] >>> 6 ) amp; 003 | ( bytes[ sidx 1 ] << 2 ) amp; 077 ) );
else
ret.append( base64Chars.charAt( ( bytes[ sidx 1 ] << 2 ) amp; 077 ) );
if( sidx 2 < bytes.length ) ret.append( base64Chars.charAt( bytes[ sidx 2 ] amp; 077 ) );
} else
ret.append( base64Chars.charAt( ( bytes[ sidx ] << 4 ) amp; 077 ) );
}
int mod = ret.length() % 4;
for( int i = 0; ( mod > 0 ) amp;amp; ( i < 4 - mod ); i )
ret.append( '=' );
return ret.toString();
} // public static final String base64Encode( byte[] bytes )
public static final byte[] base64Decode( String data ) throws Base64DecodeException {
if( data.length() == 0 ) return new byte[ 0 ];
Vector dest = new Vector( data.length() );
// ASCII printable to 0-63 conversion
int prevBits = 0; // stores the bits left over from the previous step
int modAdjust = 0; // stores the start of the current line.
for( int i = 0; i < data.length(); i ) {
char ch = data.charAt( i ); // get the character
if( ch == '=' ) break; // is it the padding character, no check for correct position
int mod = ( i - modAdjust ) % 4; // what is the index modulo 4 in the current line
if( mod == 0 ) {
// the line can only be broken on modulo 0 (e.g. 72, 76 character per line. MIME specifies 76 as max).
if( ( ch == 'r' ) || ( ch == 'n' ) ) { // we handle the encoders that use 'n' only as well
modAdjust = i 1; // skip the [CR/]LF sequence. The new line probably starts at i 1;
continue;
}
}
// if we came to here, there was no special character
int x = base64Chars.indexOf( ch ); // search for the character in the table
if( x < 0 ) throw new Base64DecodeException(); // if the character was not found raise an exception
switch( mod ) {
case 0:
prevBits = x << 2; // just store the bits and continue
break;
case 1:
dest.add( new Byte( (byte)( prevBits | x >>> 4 ) ) ); // previous 6 bits OR 2 new ones
prevBits = ( x amp; 017 ) << 4; // store 4 bits
break;
case 2:
dest.add( new Byte( (byte)( prevBits | x >>> 2 ) ) ); // previous 4 bits OR 4 new ones
prevBits = ( x amp; 003 ) << 6; // store 2 bits
break;
case 3:
dest.add( new Byte( (byte)( prevBits | x ) ) ); // previous 2 bits OR 6 new ones
break;
}
}
byte[] ret = new byte[ dest.size() ]; // convert the Vector into an array
for( int i = 0; i < ret.length; i )
ret[ i ] = ( (Byte)dest.get( i ) ).byteValue();
return ret;
}
public static final boolean isBase64Encoded( String sBase64 ) {
int len = sBase64.length();
if( len % 4 != 0 ) return false;
for( int i = 0; i < len; i ) {
char c = sBase64.charAt( i );
if( ( c >= 'a' ) amp;amp; ( c <= 'z' ) ) continue;
if( ( c >= 'A' ) amp;amp; ( c <= 'Z' ) ) continue;
if( ( c >= '0' ) amp;amp; ( c <= '9' ) ) continue;
if( ( c == ' ' ) || ( c == '/' ) || ( c == '=' ) ) continue;
return false;
}
return true;
}
}
package com.nil.exception;
public class Base64DecodeException extends Exception {
/**
*
*/
private static final long serialVersionUID = -5600202677007235761L;
/**
*
*/
public Base64DecodeException() {
// Auto-generated constructor stub
}
/**
* @param argMessage
*/
public Base64DecodeException( String argMessage ) {
super( argMessage );
}
/**
* @param argCause
*/
public Base64DecodeException( Throwable argCause ) {
super( argCause );
}
/**
* @param argMessage
* @param argCause
*/
public Base64DecodeException( String argMessage, Throwable argCause ) {
super( argMessage, argCause );
}
}
Комментарии:
1. Большое вам спасибо за подробное объяснение и код. Как только у меня будет время проверить, работает ли это, я сообщу здесь.