ColdFusion 2021 — Как обрабатывать SAML/SSO с несколькими приложениями на одном сервере
У нас есть сервер с дюжиной небольших приложений, каждое из которых находится в своей подпапке сервера (//URL/app1, //URL/app2 и т. д.).
У меня работает базовая проверка подлинности SSO в оба конца. Я настроил свою учетную запись с помощью своего поставщика идентификационной информации и установил ответ для перехода на общую целевую страницу (URL-адрес ACS). Поскольку целевая страница в настоящее время используется всеми приложениями, она находится в отдельной папке, отличной от приложений (//URL/sso/acsLandingPage.cfm).
Сейчас я работаю над своим первым приложением. Я могу обнаружить, что пользователь не вошел в систему, поэтому я делаю
Но как мне перенаправить обратно в мое целевое приложение и сообщить ему, что пользователь аутентифицирован?
Если я просто сделаю
Есть ли функция, которую я могу вызвать в исходном приложении, которая сообщит, есть ли у текущего браузера/пользователя открытая сессия?
Нужно ли мне настраивать отдельный SP для каждого приложения, чтобы вместо одной общей целевой страницы каждое приложение имело свою собственную целевую страницу, чтобы оно могло устанавливать переменные сеанса для передачи обратно в основное приложение? (IDP рассматривает наши приложения как «один сервер», я могу получить отдельные ключи, если это лучший способ справиться с этим).
Моя текущая рабочая идея для целевой страницы ACS состоит в том, чтобы проанализировать URL-адрес relayState, чтобы узнать, какое приложение запустило запрос инициализации, а затем сделать что-то вроде этого:
ACS LandingPage.cfm
<cfset response = processSAMLResponse(idp, sp) />
<cfif find(response.relaystate, 'app1')>
<cfapplication name="app1" sessionmanagement="true" />
<cfelseif find(response.relaystate, 'app2')>
<cfapplication name="app2" sessionmanagement="true" />
</cfif>
<cfset session.authenticated_username = response.nameid />
<cflocation url="#response.relaystate#" />
Не очень идеально, но я думаю, что это может сработать.
Я надеялся, что просто упускаю из виду что-то простое и очень ценю любую помощь, которую могу получить.
Изменить: моя вышеприведенная идея использования <cfapplication в ACSLandingPage не работает, потому что <cfapplication продолжает пытаться назначить его новому сеансу, поэтому, когда я перенаправляю обратно в исходное приложение, он думает, что находится в другом сеансе, поэтому не иметь доступ к исходному имени пользователя session.authenticated.
1 ответ
Хорошо, вот как я решил эту проблему. Вероятно, это не «правильное» решение, но оно работает для меня.
Решение с полным кодом было бы слишком длинным и сложным и основывалось бы на слишком большом количестве локальных вызовов, которые не имели бы смысла, поэтому я пытаюсь свести это к некоторым фрагментам кода, которые будут иметь смысл, чтобы показать, как работает мое решение.
В каждом приложении Application.cfc выглядит примерно так. Имя каждого приложения соответствует пути Application.cfc. Мы делаем это, потому что мы часто запускаем «обучающие экземпляры» кодовой базы на том же сервере, которые указывают на альтернативную схему БД, чтобы пользователи могли поиграть, не повреждая производственные данные.
component {
this.name = hash(getCurrentTemplatePath());
...
В функции приложения onRequestStart это выглядит примерно так:
cfparam(session.is_authenticated, false);
cfparam(session.auth_username, '');
cfparam(application._auth_struct, {}); // will be important later
// part 1
// there will be code in this block later in the description
// part 2
if (NOT session.is_authenticated OR session.auth_username EQ '') {
var returnURL = '#getPageContext().getRequest().getScheme()#://#cgi.server_name#/#cgi.http_url#'; // points back to this calling page
// start the call
InitSAMLAuthRequest({
'idp' : 'IDP_NAME',
'sp' : 'SP_NAME',
'relayState': returnURL
});
}
// log them in
if (session.is_authenticated AND session.auth_username NEQ '' AND NOT isUserLoggedIn()) {
... do cflogin stuff here ...
}
// throw problems if we are not logged in by this point
if (NOT isUserLoggedIn()) {
... if we don't have a logged in user by this point do error handling and redirect them somewhere safe ...
}
Это инициирует соединение SAML с нашим поставщиком идентификаторов. Провайдер делает свое дело и возвращает пользователя в файл https://myserver/sso/ProcessSAMLResponse.cfm.
processSAMLResponse использует returnURL, установленный в relayState, чтобы определить, какое приложение инициировало запрос, чтобы получить путь к файлу Application.cfc приложения.
<cfset response = ProcessSAMLResponse(idpname:"IDP_NAME", spname:"SP_NAME") />
<cfset returnURL = response.RELAYSTATE />
<cfif findNoCase("/app1", returnURL)>
<cfset appPath = "PHYSICAL_PATH_TO_APP1s_APPLICATION.CFC" />
<cfelseif findNoCase("/app2", returnURL)>
<cfset appPath = "PHYSICAL_PATH_TO_APP2s_APPLICATION.CFC" />
<cfelseif findNoCase("/app3", returnURL)>
<cfset appPath = "PHYSICAL_PATH_TO_APP3s_APPLICATION.CFC" />
...
</cfif>
<!--- initiate application --->
<cfapplication name="#hash(appPath)#" sessionmanagement="true"></cfapplication>
<!--- create a token (little more than a random string and a bit prettier than a UUID) --->
<cfset auth_token = hash(response.NAMEID & dateTimeFormat(now(), 'YYYYmmddHHnnssL'))/>
<cfset application._auth_struct[auth_token] = {
"nameid": lcase(response.NAMEID),
"expires": dateAdd('n', 5, now())
} />
<!--- append token (can also be done with a ?: if you are inclined) --->
<cfif NOT find("?", returnURL)>
<cfset returnURL &= "?auth_token=" & encodeForURL(auth_token) />
<cfelse>
<cfset returnURL &= "&auth_token=" & encodeForURL(auth_token) />
</cfif>
<!--- return to the calling page --->
<cflocation url="#returnURL#" addToken="No"/>
Это возвращает его обратно в приложение. Итак, мы возвращаемся к onRequestStart приложения, чтобы заполнить этот блок части 1 сверху:
cfparam(session.is_authenticated, false);
cfparam(session.auth_username, '');
// part 1
// look for an auth token
if (NOT session.is_authenticated AND session.auth_username EQ '' AND structKeyExists(URL, 'auth_token')) {
var auth_token = URL.auth_token;
// see if it exists in our auth struct (and has all fields)
if ( structKeyExists(application, "_auth_struct")
AND structKeyExists(application._auth_struct, auth_token)
AND isStruct(application._auth_struct[auth_token])
AND structKeyExists(application._auth_struct[auth_token], 'nameid')
AND structKeyExists(application._auth_struct[auth_token], 'expires')) {
// only load if not expired
if (application._auth_struct[auth_token].expires GT now()) {
session.is_authenticated = true;
session.auth_username = application._auth_struct[auth_token].nameid;
}
// remove token from struct to prevent replays
structDelete(application._auth_struct, auth_token);
} // token in auth struct?
// remove expired tokens
application._auth_struct = structFilter(application._auth_struct, function(key, value) {
return value.expires GT now();
});
} // auth_token?
// part 2
// .... from earlier
Так я решил проблему нескольких приложений, пытающихся использовать одну комбинацию IDP/SP.
Важные предостережения:
- Все это делается на сервере интрасети, поэтому моя безопасность намного слабее, чем на общедоступном сервере. (в частности, использование переменной приложения для хранения токенов аутентификации может быть уязвимо для массивной атаки типа DDOS, которая приведет к переполнению новых сеансов и заполнению доступной памяти).
- Подмножество 1 — эти приложения получают несколько сотен пользователей в день во всех приложениях, если у вас есть сайт, который получает тысячи посещений в день, хранение токенов в приложении, как у меня, может быть недостаточно эффективным для вас.
Мой IDP очень ограничен. Было бы намного лучше, если бы я мог просто создавать отдельные настройки SP для каждого приложения и направлять обратные вызовы непосредственно в вызывающее приложение.
Я пропустил несколько проверок и обработки ошибок, чтобы сделать пример простым. Вы должны сделать гораздо больше тестов со значениями, особенно чтобы убедиться, что nameID является действительным пользователем перед фактическим вызовом cflogin.
Перед вызовом initSAMLAuthRequest вы можете добавить счетчик сеансов, чтобы предотвратить бесконечный цикл вызовов аутентификации, если что-то пойдет не так (узнал это на собственном горьком опыте).