The default behavior for LDAP is to perform a case-insensitive search for username. And the default behavior for the IdP is to accept the username in exactly the form as entered by the user. Which can lead to inconsistent behavior of the IdP if the user changes the form how the username is entered (all lower-case, all upper-case, various forms of mixed-case…).
The consequence is that some operations on the IdP (like storing a session) can fail, because the storage layer detects the key (username) is different from the previous case. This gets quickly complex, as the database search operations are also case insensitive (at least on MySQL in the default configuration) - returning the existing session for the user, but confusing the storage layer with a different key than what the storage layer requested.
The real impact is that besides logging errors and exceptions in the log file, the IdP can also break the login flow, displaying the exception with a cryptic error message to the user. (This happens when a user logs into a new service for the first time, and uses a case combination different from what the main key for the user is - what was used for the very first time the user used the IdP).
The IdP 3.x installation manual has been updated (as of 2017-03-01) to force the IdP to normalize to lower-case by setting shibboleth.authn.Password.Lowercase
to TRUE
in /opt/shibboleth-idp/conf/authn/password-authn-config.xml
:
<util:constant id="shibboleth.authn.Password.Lowercase" static-field="java.lang.Boolean.TRUE"/>
However, on IdPs that already have been operating in production, it is necessary to make database changes at the same time to change all stored sessions to lowercase.
The tables affected are:
tb_st
- where the sharedToken values are storedshibpid
- where persistentId values are storedStorageRecords
- where the IdP stores several different types of recordsThe recommended sequence is:
service tomcat stop
) - outage starts hereservice tomcat start
) - outage ends hereThe following functions define string transformations that are needed by the code below:
MySQL helper functions
CREATE FUNCTION fixValue (value LONGTEXT)
RETURNS LONGTEXT
RETURN CONCAT( LOWER(SUBSTRING_INDEX(SUBSTRING_INDEX(value, ':', 1), ':', -1)), SUBSTRING(value, LOCATE(':', value), LENGTH(value)));
DELIMITER $$
CREATE FUNCTION fixList (list LONGTEXT)
RETURNS LONGTEXT
BEGIN
DECLARE todo LONGTEXT;
DECLARE done LONGTEXT;
SET done = '';
SET todo = list;
iter: LOOP
IF NOT LOCATE(',',todo)>0 THEN
LEAVE iter;
END IF;
SET done = CONCAT(done,fixValue(SUBSTR(todo,1,LOCATE(',',todo))));
SET todo = SUBSTR(todo,LOCATE(',',todo)+1);
END LOOP iter;
RETURN CONCAT(done,fixValue(todo));
END$$
DELIMITER ;
DELIMITER $$
CREATE FUNCTION fixEntry (data LONGTEXT, entryName LONGTEXT)
RETURNS LONGTEXT
BEGIN
DECLARE searchFor LONGTEXT;
DECLARE foundAt INT;
DECLARE found2 INT;
SET searchFor = CONCAT('"', entryName, '":"');
SET foundAt = LOCATE(searchFor,data);
IF foundAt > 0 THEN
SET found2 = LOCATE('"',data,foundAt + LENGTH(searchFor));
IF found2 > 0 THEN
RETURN CONCAT( SUBSTR(data,1,foundAt + LENGTH(searchFor) - 1), LOWER(SUBSTR(data,foundAt + LENGTH(searchFor), found2 - foundAt - LENGTH(searchFor))), SUBSTR(data,found2));
ELSE
RETURN data;
END IF;
ELSE
RETURN data;
END IF;
END$$
DELIMITER ;
The following checks:
tb_st - the two numbers returned by the second query should be the same
SELECT * FROM tb_st WHERE lower(uid) NOT LIKE BINARY uid;
SELECT COUNT(DISTINCT(uid)), COUNT(DISTINCT(lower(uid))) FROM tb_st;
SELECT localId, principalName, lower(principalName) FROM shibpid WHERE lower(principalName) NOT LIKE BINARY principalName;
SELECT id, CONCAT( LOWER(SUBSTRING_INDEX(SUBSTRING_INDEX(id, ':', 1), ':', -1)), SUBSTRING(id, LOCATE(':', id), LENGTH(id))) AS new_id FROM StorageRecords WHERE LOCATE(':',id)>0 HAVING id != BINARY new_id;
SELECT value, fixEntry(value,"nam") AS new_value FROM StorageRecords WHERE id ='_session' HAVING value != BINARY new_value;
SELECT value, fixEntry(fixEntry(value,"LDAPN"),"U") AS new_value FROM StorageRecords WHERE id ='authn/Password' HAVING value != BINARY new_value;
SELECT value, fixList(value) AS new_value FROM StorageRecords WHERE id LIKE '%:_key_idx' HAVING value != BINARY new_value;
The following code:
tb_st.uid
and shibpid.principalName
to lowercaseid
is of the form <username>:<keyword>
, turn the username part to lowercaseid "
_session"
, change the “nam” field in the JSON data to lowercaseid
"Authn/Password"
, change the “LDAPN” field in the JSON data to lowercaseid
of the form <username>:_key_idx
, change the username to lowercase in each tuple inside the array this field holdsUPDATE tb_st SET uid = LOWER(uid);
UPDATE shibpid SET principalName = LOWER(principalName);
UPDATE StorageRecords SET id = CONCAT( LOWER(SUBSTRING_INDEX(SUBSTRING_INDEX(id, ':', 1), ':', -1)), SUBSTRING(id, LOCATE(':', id), LENGTH(id))) WHERE LOCATE(':',id)>0;
UPDATE StorageRecords SET value = fixEntry(value,"nam") WHERE id ='_session';
UPDATE StorageRecords SET value = fixEntry(fixEntry(value,"LDAPN"),"U") WHERE id ='authn/Password';
UPDATE StorageRecords SET value = fixList(value) WHERE id LIKE '%:_key_idx';
Remove the helper functions once this operation is complete:
DROP FUNCTION fixValue;
DROP FUNCTION fixList;
DROP FUNCTION fixEntry;