ColdFusion 2021 - 如何在同一服务器上处理 SAML/SSO 多个应用程序
ColdFusion 2021 - How to handle SAML/SSO with multiple applications on same server
我们有一个服务器,其中包含大约十几个小应用程序,每个应用程序都位于服务器自己的子文件夹中(//URL/app1、//URL/app2 等)。
我的基本 SSO 身份验证往返工作正常。我用我的 IDP 设置了我的帐户,并将响应设置为转到一个公共登录页面 (ACS URL)。由于登录页面当前与所有应用程序共享,因此它位于与应用程序不同的单独文件夹中 (//URL/sso/acsLandingPage.cfm)
我现在正在开发我的第一个应用程序。我可以检测到用户没有登录,所以我做了一个 initSAMLAuthRequest(idp, sp, relayState: "CALLING_PAGE_URL")
然后它熄灭,进行身份验证,然后 returns 到登录页面。
但是我如何重定向回我的目标应用程序并告诉它用户已通过身份验证?
如果我只是执行 <cflocation url="CALLING_PAGE_URL" />
原始应用不知道 SAML 请求。
我可以在原始应用程序中调用一个函数来判断当前 browser/user 是否有打开的会话吗?
我是否需要为每个应用程序设置单独的 SP,而不是一个公共登录页面,每个应用程序都有自己的登录页面,以便它可以设置会话变量以传回主应用程序? (IDP 将我们的应用程序视为“一个服务器”,如果这是处理此问题的最佳方式,我可以获得单独的密钥)。
我目前对 ACS 登录页面的工作想法是解析 relayState URL 以找出哪个应用程序启动了 init 请求,然后执行如下操作:
ACSLandingPage.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#" />
不太理想,但我认为它可能有效。
我希望我只是忽略了一些简单的事情,非常感谢我能得到的任何帮助。
编辑:
我上面在 ACSLandingPage 中使用
好的,这就是我最终解决这个问题的方法。可能不是“正确”的解决方案,但对我有用。
完整的代码解决方案太长太复杂,并且依赖太多没有意义的本地调用,所以我试图将其简化为一些代码片段,这些代码片段将有意义地展示如何我的解决方案有效。
在每个应用程序中,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 ...
}
这将启动与我们的 ID 提供商的 SAML 连接。提供商执行其任务,然后 return 将用户发送到文件 'https://myserver/sso/ProcessSAMLResponse.cfm'.
processSAMLResponse 使用在 relayState 中设置的 returnURL 来确定哪个应用程序发起了请求,因此它可以获得应用程序 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 组合的问题。
重要注意事项:
- 这一切都是在 Intranet 服务器上完成的,因此我的安全性比在面向 public 的服务器上要宽松得多。 (特别是,使用应用程序变量存储 auth-tokens 可能容易受到大规模 DDOS 类型的攻击,这种攻击会淹没新会话并填满可用内存)。
- 1 的子集 - 这些应用程序每天在所有应用程序中获得数百名用户,如果您的网站每天获得数千次点击,像我这样将令牌存储在应用程序中可能内存效率不够给你。
我的 IDP 非常受限。如果我可以为每个应用程序创建不同的 SP 设置并让 return 调用直接返回调用应用程序,那就更好了。
我跳过了一些检查和错误处理以保持示例简单。您应该对值进行更多测试,尤其是在实际调用 cflogin 之前确保 nameID 是有效用户。
在调用 initSAMLAuthRequest 之前,您可能需要添加一个会话计数器,以防止出现问题时身份验证调用的无限循环(通过艰难的方式了解到)。
我们有一个服务器,其中包含大约十几个小应用程序,每个应用程序都位于服务器自己的子文件夹中(//URL/app1、//URL/app2 等)。
我的基本 SSO 身份验证往返工作正常。我用我的 IDP 设置了我的帐户,并将响应设置为转到一个公共登录页面 (ACS URL)。由于登录页面当前与所有应用程序共享,因此它位于与应用程序不同的单独文件夹中 (//URL/sso/acsLandingPage.cfm)
我现在正在开发我的第一个应用程序。我可以检测到用户没有登录,所以我做了一个 initSAMLAuthRequest(idp, sp, relayState: "CALLING_PAGE_URL")
然后它熄灭,进行身份验证,然后 returns 到登录页面。
但是我如何重定向回我的目标应用程序并告诉它用户已通过身份验证?
如果我只是执行 <cflocation url="CALLING_PAGE_URL" />
原始应用不知道 SAML 请求。
我可以在原始应用程序中调用一个函数来判断当前 browser/user 是否有打开的会话吗?
我是否需要为每个应用程序设置单独的 SP,而不是一个公共登录页面,每个应用程序都有自己的登录页面,以便它可以设置会话变量以传回主应用程序? (IDP 将我们的应用程序视为“一个服务器”,如果这是处理此问题的最佳方式,我可以获得单独的密钥)。
我目前对 ACS 登录页面的工作想法是解析 relayState URL 以找出哪个应用程序启动了 init 请求,然后执行如下操作:
ACSLandingPage.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#" />
不太理想,但我认为它可能有效。
我希望我只是忽略了一些简单的事情,非常感谢我能得到的任何帮助。
编辑:
我上面在 ACSLandingPage 中使用
好的,这就是我最终解决这个问题的方法。可能不是“正确”的解决方案,但对我有用。
完整的代码解决方案太长太复杂,并且依赖太多没有意义的本地调用,所以我试图将其简化为一些代码片段,这些代码片段将有意义地展示如何我的解决方案有效。
在每个应用程序中,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 ...
}
这将启动与我们的 ID 提供商的 SAML 连接。提供商执行其任务,然后 return 将用户发送到文件 'https://myserver/sso/ProcessSAMLResponse.cfm'.
processSAMLResponse 使用在 relayState 中设置的 returnURL 来确定哪个应用程序发起了请求,因此它可以获得应用程序 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 组合的问题。
重要注意事项:
- 这一切都是在 Intranet 服务器上完成的,因此我的安全性比在面向 public 的服务器上要宽松得多。 (特别是,使用应用程序变量存储 auth-tokens 可能容易受到大规模 DDOS 类型的攻击,这种攻击会淹没新会话并填满可用内存)。
- 1 的子集 - 这些应用程序每天在所有应用程序中获得数百名用户,如果您的网站每天获得数千次点击,像我这样将令牌存储在应用程序中可能内存效率不够给你。
我的 IDP 非常受限。如果我可以为每个应用程序创建不同的 SP 设置并让 return 调用直接返回调用应用程序,那就更好了。
我跳过了一些检查和错误处理以保持示例简单。您应该对值进行更多测试,尤其是在实际调用 cflogin 之前确保 nameID 是有效用户。
在调用 initSAMLAuthRequest 之前,您可能需要添加一个会话计数器,以防止出现问题时身份验证调用的无限循环(通过艰难的方式了解到)。