OpenAM - 将会话属性获取到 OpenID Connect 声明中
OpenAM - Getting a Session attribute into an OpenID Connect claim
我正在使用配置为具有 SAML 信任圈的 OpenAM 13.5,以将登录到我们的应用程序与第三方 IdP 联合起来。第三方收到的一些 SAML 断言被映射为会话级属性。 SAML 部分工作正常,但我需要连接到 OpenAM 一个可以与 OpenID Connect 对话的应用程序。我创建了一个 OpenID Connect 服务,相应地配置了客户端,我可以使用流程 "App -> OpenAM UI -> 3rd party IDP -> OpenAM OIDC -> App".
成功登录
问题是我只能检索映射到数据存储的属性 - 会话属性(例如 AuthLevel、IDP 名称等)不包含在映射的声明中。
我试图编辑 OIDC 声明默认脚本,它有一个似乎包含我需要的会话变量,但不幸的是会话变量始终为空。
这是正确的做法吗?为什么会话为空?我需要启用什么才能阅读它吗?
在此先感谢您的帮助。
您无法在 OIDC 声明脚本中检索 SSO 会话 属性,因为 OAuth2 客户端不会在令牌请求中发送 SSO 跟踪 cookie。
只有使用 AM 专有功能才有可能 'always include claims in ID token'。
正如 Bernhard 在评论中所描述的那样,一旦请求到达 /userinfo 端点,OpenAM 就无法将访问令牌与实时会话协调(并且该会话也不再存在)。
然而,当通过激活专有 AM 功能访问 ID 令牌内的声明时 "Always include claims in ID Token" 会话对象可用,我们可以轮询其属性!
对于未来的读者,这是我修改 OIDC 脚本的方式:
/*
* The contents of this file are subject to the terms of the Common Development and
* Distribution License (the License). You may not use this file except in compliance with the
* License.
*
* You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
* specific language governing permission and limitations under the License.
*
* When distributing Covered Software, include this CDDL Header Notice in each file and include
* the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
* Header, with the fields enclosed by brackets [] replaced by your own identifying
* information: "Portions copyright [year] [name of copyright owner]".
*
* Copyright 2014-2016 ForgeRock AS.
*/
import com.iplanet.sso.SSOException
import com.sun.identity.idm.IdRepoException
import org.forgerock.oauth2.core.UserInfoClaims
/*
* Defined variables:
* logger - always presents, the "OAuth2Provider" debug logger instance
* claims - always present, default server provided claims
* session - present if the request contains the session cookie, the user's session object
* identity - always present, the identity of the resource owner
* scopes - always present, the requested scopes
* requestedClaims - Map<String, Set<String>>
* always present, not empty if the request contains a claims parameter and server has enabled
* claims_parameter_supported, map of requested claims to possible values, otherwise empty,
* requested claims with no requested values will have a key but no value in the map. A key with
* a single value in its Set indicates this is the only value that should be returned.
* Required to return a Map of claims to be added to the id_token claims
*
* Expected return value structure:
* UserInfoClaims {
* Map<String, Object> values; // The values of the claims for the user information
* Map<String, List<String>> compositeScopes; // Mapping of scope name to a list of claim names.
* }
*/
// user session not guaranteed to be present
boolean sessionPresent = session != null
def fromSet = { claim, attr ->
if (attr != null && attr.size() == 1){
attr.iterator().next()
} else if (attr != null && attr.size() > 1){
attr
} else if (logger.warningEnabled()) {
logger.warning("OpenAMScopeValidator.getUserInfo(): Got an empty result for claim=$claim");
}
}
attributeRetriever = { attribute, claim, identity, session, requested ->
if (requested == null || requested.isEmpty()) {
fromSet(claim, identity.getAttribute(attribute))
} else if (requested.size() == 1) {
requested.iterator().next()
} else {
throw new RuntimeException("No selection logic for $claim defined. Values: $requested")
}
}
sessionAttributeRetriever = { attribute, claim, identity, session, requested ->
if (requested == null || requested.isEmpty()) {
if (session != null) {
fromSet(claim, session.getProperty(attribute))
} else {
null
}
} else if (requested.size() == 1) {
requested.iterator().next()
} else {
throw new RuntimeException("No selection logic for $claim defined. Values: $requested")
}
}
// [ {claim}: {attribute retriever}, ... ]
claimAttributes = [
"email": attributeRetriever.curry("mail"),
"address": { claim, identity, session, requested -> [ "formatted" : attributeRetriever("postaladdress", claim, identity, session, requested) ] },
"phone_number": attributeRetriever.curry("telephonenumber"),
"given_name": attributeRetriever.curry("givenname"),
"zoneinfo": attributeRetriever.curry("preferredtimezone"),
"family_name": attributeRetriever.curry("sn"),
"locale": attributeRetriever.curry("preferredlocale"),
"name": attributeRetriever.curry("cn"),
"spid_uid": attributeRetriever.curry("employeeNumber"),
"spid_idp": attributeRetriever.curry("idpEntityId"),
"spid_gender": attributeRetriever.curry("description"),
"spid_authType": sessionAttributeRetriever.curry("AuthType"),
"spid_authLevel": sessionAttributeRetriever.curry("AuthLevel"),
]
// {scope}: [ {claim}, ... ]
scopeClaimsMap = [
"email": [ "email" ],
"address": [ "address" ],
"phone": [ "phone_number" ],
"profile": [ "given_name", "zoneinfo", "family_name", "locale", "name" ],
"spid": [ "spid_uid", "spid_idp", "spid_authType", "spid_authLevel", "spid_gender" ],
]
if (logger.messageEnabled()) {
scopes.findAll { s -> !("openid".equals(s) || scopeClaimsMap.containsKey(s)) }.each { s ->
logger.message("OpenAMScopeValidator.getUserInfo()::Message: scope not bound to claims: $s")
}
}
def computeClaim = { claim, requestedValues ->
try {
[ claim, claimAttributes.get(claim)(claim, identity, session, requestedValues) ]
} catch (IdRepoException e) {
if (logger.warningEnabled()) {
logger.warning("OpenAMScopeValidator.getUserInfo(): Unable to retrieve attribute=$attribute", e);
}
} catch (SSOException e) {
if (logger.warningEnabled()) {
logger.warning("OpenAMScopeValidator.getUserInfo(): Unable to retrieve attribute=$attribute", e);
}
}
}
def computedClaims = scopes.findAll { s -> !"openid".equals(s) && scopeClaimsMap.containsKey(s) }.inject(claims) { map, s ->
scopeClaims = scopeClaimsMap.get(s)
map << scopeClaims.findAll { c -> !requestedClaims.containsKey(c) }.collectEntries([:]) { claim -> computeClaim(claim, null) }
}.findAll { map -> map.value != null } << requestedClaims.collectEntries([:]) { claim, requestedValue ->
computeClaim(claim, requestedValue)
}
def compositeScopes = scopeClaimsMap.findAll { scope ->
scopes.contains(scope.key)
}
return new UserInfoClaims((Map)computedClaims, (Map)compositeScopes)
我还必须将 java.util.ArrayList$Itr
class 添加到脚本 classes 白名单中。
感谢您的帮助!
我正在使用配置为具有 SAML 信任圈的 OpenAM 13.5,以将登录到我们的应用程序与第三方 IdP 联合起来。第三方收到的一些 SAML 断言被映射为会话级属性。 SAML 部分工作正常,但我需要连接到 OpenAM 一个可以与 OpenID Connect 对话的应用程序。我创建了一个 OpenID Connect 服务,相应地配置了客户端,我可以使用流程 "App -> OpenAM UI -> 3rd party IDP -> OpenAM OIDC -> App".
成功登录问题是我只能检索映射到数据存储的属性 - 会话属性(例如 AuthLevel、IDP 名称等)不包含在映射的声明中。
我试图编辑 OIDC 声明默认脚本,它有一个似乎包含我需要的会话变量,但不幸的是会话变量始终为空。
这是正确的做法吗?为什么会话为空?我需要启用什么才能阅读它吗?
在此先感谢您的帮助。
您无法在 OIDC 声明脚本中检索 SSO 会话 属性,因为 OAuth2 客户端不会在令牌请求中发送 SSO 跟踪 cookie。
只有使用 AM 专有功能才有可能 'always include claims in ID token'。
正如 Bernhard 在评论中所描述的那样,一旦请求到达 /userinfo 端点,OpenAM 就无法将访问令牌与实时会话协调(并且该会话也不再存在)。
然而,当通过激活专有 AM 功能访问 ID 令牌内的声明时 "Always include claims in ID Token" 会话对象可用,我们可以轮询其属性!
对于未来的读者,这是我修改 OIDC 脚本的方式:
/*
* The contents of this file are subject to the terms of the Common Development and
* Distribution License (the License). You may not use this file except in compliance with the
* License.
*
* You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
* specific language governing permission and limitations under the License.
*
* When distributing Covered Software, include this CDDL Header Notice in each file and include
* the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
* Header, with the fields enclosed by brackets [] replaced by your own identifying
* information: "Portions copyright [year] [name of copyright owner]".
*
* Copyright 2014-2016 ForgeRock AS.
*/
import com.iplanet.sso.SSOException
import com.sun.identity.idm.IdRepoException
import org.forgerock.oauth2.core.UserInfoClaims
/*
* Defined variables:
* logger - always presents, the "OAuth2Provider" debug logger instance
* claims - always present, default server provided claims
* session - present if the request contains the session cookie, the user's session object
* identity - always present, the identity of the resource owner
* scopes - always present, the requested scopes
* requestedClaims - Map<String, Set<String>>
* always present, not empty if the request contains a claims parameter and server has enabled
* claims_parameter_supported, map of requested claims to possible values, otherwise empty,
* requested claims with no requested values will have a key but no value in the map. A key with
* a single value in its Set indicates this is the only value that should be returned.
* Required to return a Map of claims to be added to the id_token claims
*
* Expected return value structure:
* UserInfoClaims {
* Map<String, Object> values; // The values of the claims for the user information
* Map<String, List<String>> compositeScopes; // Mapping of scope name to a list of claim names.
* }
*/
// user session not guaranteed to be present
boolean sessionPresent = session != null
def fromSet = { claim, attr ->
if (attr != null && attr.size() == 1){
attr.iterator().next()
} else if (attr != null && attr.size() > 1){
attr
} else if (logger.warningEnabled()) {
logger.warning("OpenAMScopeValidator.getUserInfo(): Got an empty result for claim=$claim");
}
}
attributeRetriever = { attribute, claim, identity, session, requested ->
if (requested == null || requested.isEmpty()) {
fromSet(claim, identity.getAttribute(attribute))
} else if (requested.size() == 1) {
requested.iterator().next()
} else {
throw new RuntimeException("No selection logic for $claim defined. Values: $requested")
}
}
sessionAttributeRetriever = { attribute, claim, identity, session, requested ->
if (requested == null || requested.isEmpty()) {
if (session != null) {
fromSet(claim, session.getProperty(attribute))
} else {
null
}
} else if (requested.size() == 1) {
requested.iterator().next()
} else {
throw new RuntimeException("No selection logic for $claim defined. Values: $requested")
}
}
// [ {claim}: {attribute retriever}, ... ]
claimAttributes = [
"email": attributeRetriever.curry("mail"),
"address": { claim, identity, session, requested -> [ "formatted" : attributeRetriever("postaladdress", claim, identity, session, requested) ] },
"phone_number": attributeRetriever.curry("telephonenumber"),
"given_name": attributeRetriever.curry("givenname"),
"zoneinfo": attributeRetriever.curry("preferredtimezone"),
"family_name": attributeRetriever.curry("sn"),
"locale": attributeRetriever.curry("preferredlocale"),
"name": attributeRetriever.curry("cn"),
"spid_uid": attributeRetriever.curry("employeeNumber"),
"spid_idp": attributeRetriever.curry("idpEntityId"),
"spid_gender": attributeRetriever.curry("description"),
"spid_authType": sessionAttributeRetriever.curry("AuthType"),
"spid_authLevel": sessionAttributeRetriever.curry("AuthLevel"),
]
// {scope}: [ {claim}, ... ]
scopeClaimsMap = [
"email": [ "email" ],
"address": [ "address" ],
"phone": [ "phone_number" ],
"profile": [ "given_name", "zoneinfo", "family_name", "locale", "name" ],
"spid": [ "spid_uid", "spid_idp", "spid_authType", "spid_authLevel", "spid_gender" ],
]
if (logger.messageEnabled()) {
scopes.findAll { s -> !("openid".equals(s) || scopeClaimsMap.containsKey(s)) }.each { s ->
logger.message("OpenAMScopeValidator.getUserInfo()::Message: scope not bound to claims: $s")
}
}
def computeClaim = { claim, requestedValues ->
try {
[ claim, claimAttributes.get(claim)(claim, identity, session, requestedValues) ]
} catch (IdRepoException e) {
if (logger.warningEnabled()) {
logger.warning("OpenAMScopeValidator.getUserInfo(): Unable to retrieve attribute=$attribute", e);
}
} catch (SSOException e) {
if (logger.warningEnabled()) {
logger.warning("OpenAMScopeValidator.getUserInfo(): Unable to retrieve attribute=$attribute", e);
}
}
}
def computedClaims = scopes.findAll { s -> !"openid".equals(s) && scopeClaimsMap.containsKey(s) }.inject(claims) { map, s ->
scopeClaims = scopeClaimsMap.get(s)
map << scopeClaims.findAll { c -> !requestedClaims.containsKey(c) }.collectEntries([:]) { claim -> computeClaim(claim, null) }
}.findAll { map -> map.value != null } << requestedClaims.collectEntries([:]) { claim, requestedValue ->
computeClaim(claim, requestedValue)
}
def compositeScopes = scopeClaimsMap.findAll { scope ->
scopes.contains(scope.key)
}
return new UserInfoClaims((Map)computedClaims, (Map)compositeScopes)
我还必须将 java.util.ArrayList$Itr
class 添加到脚本 classes 白名单中。
感谢您的帮助!