Merge pull request #1035 from patriot1burke/master

clean and docs for mapper
This commit is contained in:
Bill Burke 2015-03-11 12:07:03 -04:00
commit 3af3dd78e5
20 changed files with 296 additions and 131 deletions

View file

@ -40,6 +40,8 @@
<!ENTITY ApplicationClustering SYSTEM "modules/application-clustering.xml"> <!ENTITY ApplicationClustering SYSTEM "modules/application-clustering.xml">
<!ENTITY MultiTenancy SYSTEM "modules/multi-tenancy.xml"> <!ENTITY MultiTenancy SYSTEM "modules/multi-tenancy.xml">
<!ENTITY Proxy SYSTEM "modules/proxy.xml"> <!ENTITY Proxy SYSTEM "modules/proxy.xml">
<!ENTITY CustomAttributes SYSTEM "modules/custom-attributes.xml">
<!ENTITY ProtocolMappers SYSTEM "modules/protocol-mappers.xml">
]> ]>
<book> <book>
@ -128,6 +130,8 @@ This one is short
&Clustering; &Clustering;
&ApplicationClustering; &ApplicationClustering;
&Proxy; &Proxy;
&CustomAttributes;
&ProtocolMappers;
&Migration; &Migration;
</book> </book>

View file

@ -94,6 +94,14 @@
Multitenancy support. You can host and manage multiple realms for multiple organizations. In the same auth server Multitenancy support. You can host and manage multiple realms for multiple organizations. In the same auth server
and even within the same deployed application. and even within the same deployed application.
</listitem> </listitem>
<listitem>
Identity brokering/chaining. You can make the Keycloak server a child IDP to another SAML 2.0 or OpenID Connect IDP.
</listitem>
<listitem>
Token claim, assertion, and attribute mappings. You can map user attributes, roles, and role names however you want
into a OIDC ID Token, Access Token, SAML attribute statements, etc. This feature allows you to basically
tailor however you want auth responses to look.
</listitem>
<listitem> <listitem>
Supports JBoss AS7, EAP 6.x, Wildfly, Tomcat 7, Tomcat 8, Jetty 9.1.x, Jetty 9.2.x, Jetty 8.1.x, and Pure JavaScript applications. Plans to support Node.js, RAILS, GRAILS, and other non-Java deployments Supports JBoss AS7, EAP 6.x, Wildfly, Tomcat 7, Tomcat 8, Jetty 9.1.x, Jetty 9.2.x, Jetty 8.1.x, and Pure JavaScript applications. Plans to support Node.js, RAILS, GRAILS, and other non-Java deployments
</listitem> </listitem>

View file

@ -0,0 +1,150 @@
<chapter id="custom-user-attributes">
<title>Custom User Attributes</title>
<para>If you have custom user data you want to store and manage in the admin console, registration page, and user account service, you can easily add
support for it by extending and modifying various Keycloak <link linkend="themes">themes</link>.</para>
<section>
<title>In admin console</title>
<para>To be able to enter custom attributes in the admin console, take the following steps</para>
<para>
<orderedlist>
<listitem>
Create a new theme within the <literal>themes/admin/mytheme</literal> directory in your distribution.
Where <literal>mytheme</literal> is whatever you want to name your theme.
</listitem>
<listitem>
Create a <literal>theme.properties</literal> file in this directory that extends the main admin console
theme.
<programlisting><![CDATA[parent=keycloak
import=common/keycloak
]]></programlisting>
</listitem>
<listitem>
Copy the file <literal>themes/admin/base/resources/partials/user-attribute-entry.html</literal> into the
a mirror directory in your theme: <literal>themes/admin/mytheme/resources/partials/user-attribute-entry.html</literal>.
What you are doing here is overriding the user attribute entry page in the admin console and putting in
what attributes you want. This file already contains an example of entering address data. You can remove
this if you want and replace it with something else. Also, if you want to edit this file directly instead
of creating a new theme, you can.
</listitem>
<listitem>
In the <literal>user-attribute-entry.html</literal> file add your custom user attribute entry form item. For example
<programlisting><![CDATA[ <div class="form-group clearfix block">
<label class="col-sm-2 control-label" for="mobile">Mobile</label>
<div class="col-sm-6">
<input ng-model="user.attributes.mobile" class="form-control" type="text" name="mobile" id="mobile" />
</div>
<span tooltip-placement="right" tooltip="Mobile number." class="fa fa-info-circle"></span>
</div>
]]></programlisting>
The <literal>ng-model</literal> names the user attribute you will store in the database and must have the
form of <literal>user.attributes.ATTR_NAME</literal>.
</listitem>
<listitem>
Change the theme for the admin console. Save it, then refresh your browser, and you should
now see these fields in the User detail page for any user.
</listitem>
</orderedlist>
</para>
</section>
<section>
<title>In registration page</title>
<para>To be able to enter custom attributes in the registration page, take the following steps</para>
<para>
<orderedlist>
<listitem>
Create a new theme within the <literal>themes/login/mytheme</literal> directory in your distribution.
Where <literal>mytheme</literal> is whatever you want to name your theme.
</listitem>
<listitem>
Create a <literal>theme.properties</literal> file in this directory that extends the main admin console
theme.
<programlisting><![CDATA[parent=keycloak
import=common/keycloak
styles= ../patternfly/lib/patternfly/css/patternfly.css ../patternfly/css/login.css ../patternfly/lib/zocial/zocial.css css/login.css]]></programlisting>
</listitem>
<listitem>
Copy the file <literal>themes/login/base/register.ftl</literal> into the
a mirror directory in your theme: <literal>themes/login/mytheme/register.ftl</literal>.
What you are doing here is overriding the registration page and adding
what attributes you want. This file already contains an example of entering address data. You can remove
this if you want and replace it with something else. Also, if you want to edit this file directly instead
of creating a new theme, you can.
</listitem>
<listitem>
In the <literal>register.ftl</literal> file add your custom user attribute entry form item. For example
<programlisting><![CDATA[
<div class="form-group">
<div class="${properties.kcLabelWrapperClass!}">
<label for="user.attributes.mobile" class="${properties.kcLabelClass!}">Mobile number</label>
</div>
<div class="col-sm-10 col-md-10">
<input type="text" class="${properties.kcInputClass!}" id="user.attributes.mobile" name="user.attributes.mobile"/>
</div>
</div>
]]></programlisting>
Make sure the input field id ane name match the user attribute you want to store in the database.
This must have the
form of <literal>user.attributes.ATTR_NAME</literal>. You might also want to replace the label text
with a message property. This will help later if you want to internationalize your pages.
</listitem>
<listitem>
Change the theme for the login to your new theme. Save it, then refresh your browser, and you should
now see these fields in the registration.
</listitem>
</orderedlist>
</para>
</section>
<section>
<title>In user account profile page</title>
<para>To be able to manage custom attributes in the user account profile page, take the following steps</para>
<para>
<orderedlist>
<listitem>
Create a new theme within the <literal>themes/account/mytheme</literal> directory in your distribution.
Where <literal>mytheme</literal> is whatever you want to name your theme.
</listitem>
<listitem>
Create a <literal>theme.properties</literal> file in this directory that extends the main admin console
theme.
<programlisting><![CDATA[parent=patternfly
import=common/keycloak
styles= ../patternfly/lib/patternfly/css/patternfly.css ../patternfly/css/account.css css/account.css]]></programlisting>
</listitem>
<listitem>
Copy the file <literal>themes/account/base/account.ftl</literal> into the
a mirror directory in your theme: <literal>themes/account/mytheme/account.ftl</literal>.
What you are doing here is overriding the profile page and adding
what attributes you want to manage. This file already contains an example of entering address data. You can remove
this if you want and replace it with something else. Also, if you want to edit this file directly instead
of creating a new theme, you can.
</listitem>
<listitem>
In the <literal>account.ftl</literal> file add your custom user attribute entry form item. For example
<programlisting><![CDATA[
<div class="form-group">
<div class="col-sm-2 col-md-2">
<label for="user.attributes.mobile" class="control-label">Mobile number</label>
</div>
<div class="col-sm-10 col-md-10">
<input type="text" class="form-control" id="user.attributes.mobile" name="user.attributes.mobile" value="${(account.attributes.mobile!'')?html}"/>
</div>
</div>]]></programlisting>
Make sure the input field id ane name match the user attribute you want to store in the database.
This must have the
form of <literal>user.attributes.ATTR_NAME</literal>. You might also want to replace the label text
with a message property. This will help later if you want to internationalize your pages.
</listitem>
<listitem>
Change the theme for the account to your new theme. Save it, then refresh your browser, and you should
now see these fields in the account profile page.
</listitem>
</orderedlist>
</para>
</section>
</chapter>

View file

@ -0,0 +1,17 @@
<chapter id="mappers">
<title>OIDC Token and SAML Assertion Mappings</title>
<para>
Applications that receive ID Tokens, Access Tokens, or SAML assertions may need or want different user metadata
and roles. Keycloak allows you to define what exactly is transferred. You can hardcode roles, claims and custom
attributes. You can pull user metadata into a token or assertion. You can rename roles. Basicall you have
a lot of control of what exactly goes back to the client.
</para>
<para>
Within the admin console, if you go to an application you've registered, you'll see a "Mappers" sub-menu item.
This is the place where you can control how a OIDC ID Token, Access Token, and SAML login response assertions look
like. When you click on this you'll see some default mappers that have been set up for you. Clicking the
"Add Builtin" button gives you the option to add other preconfigured mappers. Clicking on "Create" allows
you to define your own protocol mappers. The tooltips are very helpful to learn exactly what you can do
to tailor your tokens and assertions. They should be enough to guide you through the process.
</para>
</chapter>

View file

@ -64,13 +64,6 @@
</div> </div>
<span tooltip-placement="right" tooltip="Should a statement specifying the method and timestamp be included in login responses?" class="fa fa-info-circle"></span> <span tooltip-placement="right" tooltip="Should a statement specifying the method and timestamp be included in login responses?" class="fa fa-info-circle"></span>
</div> </div>
<div class="form-group clearfix block" data-ng-show="protocol == 'saml'">
<label class="col-sm-2 control-label" for="samlMultiValuedRoles">Multi-valued Roles</label>
<div class="col-sm-6">
<input ng-model="samlMultiValuedRoles" ng-click="switchChange()" name="samlMultiValuedRoles" id="samlMultiValuedRoles" onoffswitch />
</div>
<span tooltip-placement="right" tooltip="'On' means that there will be one role attribute with multiple values for each role in SAML response. 'Off' means that there will be an attribute defined for each role." class="fa fa-info-circle"></span>
</div>
<div class="form-group clearfix block" data-ng-show="protocol == 'saml'"> <div class="form-group clearfix block" data-ng-show="protocol == 'saml'">
<label class="col-sm-2 control-label" for="samlServerSignature">Sign Documents</label> <label class="col-sm-2 control-label" for="samlServerSignature">Sign Documents</label>
<div class="col-sm-6"> <div class="col-sm-6">

View file

@ -1,6 +1,7 @@
package org.keycloak.login; package org.keycloak.login;
import org.keycloak.models.ClientModel; import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel; import org.keycloak.models.RoleModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
@ -35,7 +36,7 @@ public interface LoginFormsProvider extends Provider {
public Response createErrorPage(); public Response createErrorPage();
public Response createOAuthGrant(); public Response createOAuthGrant(ClientSessionModel clientSessionModel);
public Response createCode(); public Response createCode();

View file

@ -24,6 +24,7 @@ import org.keycloak.login.freemarker.model.IdentityProviderBean;
import org.keycloak.login.freemarker.model.TotpBean; import org.keycloak.login.freemarker.model.TotpBean;
import org.keycloak.login.freemarker.model.UrlBean; import org.keycloak.login.freemarker.model.UrlBean;
import org.keycloak.models.ClientModel; import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel; import org.keycloak.models.RoleModel;
@ -75,6 +76,7 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
private UserModel user; private UserModel user;
private ClientModel client; private ClientModel client;
private ClientSessionModel clientSession;
private UriInfo uriInfo; private UriInfo uriInfo;
@ -213,7 +215,7 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
attributes.put("register", new RegisterBean(formData)); attributes.put("register", new RegisterBean(formData));
break; break;
case OAUTH_GRANT: case OAUTH_GRANT:
attributes.put("oauth", new OAuthGrantBean(accessCode, client, realmRolesRequested, resourceRolesRequested, this.accessRequestMessage)); attributes.put("oauth", new OAuthGrantBean(accessCode, clientSession, client, realmRolesRequested, resourceRolesRequested, this.accessRequestMessage));
break; break;
case CODE: case CODE:
attributes.put(OAuth2Constants.CODE, new CodeBean(accessCode, messageType == MessageType.ERROR ? message : null)); attributes.put(OAuth2Constants.CODE, new CodeBean(accessCode, messageType == MessageType.ERROR ? message : null));
@ -265,7 +267,8 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider {
return createResponse(LoginFormsPages.ERROR); return createResponse(LoginFormsPages.ERROR);
} }
public Response createOAuthGrant() { public Response createOAuthGrant(ClientSessionModel clientSession) {
this.clientSession = clientSession;
return createResponse(LoginFormsPages.OAUTH_GRANT); return createResponse(LoginFormsPages.OAUTH_GRANT);
} }

View file

@ -21,8 +21,9 @@
*/ */
package org.keycloak.login.freemarker.model; package org.keycloak.login.freemarker.model;
import org.keycloak.models.ClaimMask;
import org.keycloak.models.ClientModel; import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.RoleModel; import org.keycloak.models.RoleModel;
import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.MultivaluedMap;
@ -41,7 +42,7 @@ public class OAuthGrantBean {
private ClientModel client; private ClientModel client;
private List<String> claimsRequested; private List<String> claimsRequested;
public OAuthGrantBean(String code, ClientModel client, List<RoleModel> realmRolesRequested, MultivaluedMap<String, RoleModel> resourceRolesRequested, String accessRequestMessage) { public OAuthGrantBean(String code, ClientSessionModel clientSession, ClientModel client, List<RoleModel> realmRolesRequested, MultivaluedMap<String, RoleModel> resourceRolesRequested, String accessRequestMessage) {
this.code = code; this.code = code;
this.client = client; this.client = client;
this.realmRolesRequested = realmRolesRequested; this.realmRolesRequested = realmRolesRequested;
@ -50,36 +51,12 @@ public class OAuthGrantBean {
// todo support locale // todo support locale
List<String> claims = new LinkedList<String>(); List<String> claims = new LinkedList<String>();
long mask = client.getAllowedClaimsMask(); if (clientSession != null) {
if (ClaimMask.hasEmail(mask)) { for (ProtocolMapperModel model : client.getProtocolMappers()) {
claims.add("email"); if (model.isConsentRequired() && model.getProtocol().equals(clientSession.getAuthMethod()) && model.getConsentText() != null) {
} claims.add(model.getConsentText());
if (ClaimMask.hasUsername(mask)) { }
claims.add("username"); }
}
if (ClaimMask.hasName(mask)) {
claims.add("name");
}
if (ClaimMask.hasGender(mask)) {
claims.add("gender");
}
if (ClaimMask.hasAddress(mask)) {
claims.add("address");
}
if (ClaimMask.hasPhone(mask)) {
claims.add("phone");
}
if (ClaimMask.hasPicture(mask)) {
claims.add("picture");
}
if (ClaimMask.hasProfile(mask)) {
claims.add("profile page");
}
if (ClaimMask.hasLocale(mask)) {
claims.add("locale");
}
if (ClaimMask.hasWebsite(mask)) {
claims.add("website");
} }
if (claims.size() > 0) this.claimsRequested = claims; if (claims.size() > 0) this.claimsRequested = claims;
} }

View file

@ -24,11 +24,6 @@ public class HardcodedAttributeMapper extends AbstractSAMLProtocolMapper impleme
static { static {
ConfigProperty property; ConfigProperty property;
property = new ConfigProperty();
property.setName(ProtocolMapperUtils.USER_ATTRIBUTE);
property.setLabel(ProtocolMapperUtils.USER_MODEL_ATTRIBUTE_LABEL);
property.setHelpText(ProtocolMapperUtils.USER_MODEL_ATTRIBUTE_HELP_TEXT);
configProperties.add(property);
AttributeStatementHelper.setConfigProperties(configProperties); AttributeStatementHelper.setConfigProperties(configProperties);
property = new ConfigProperty(); property = new ConfigProperty();
property.setName(ATTRIBUTE_VALUE); property.setName(ATTRIBUTE_VALUE);

View file

@ -3,6 +3,7 @@ package org.keycloak.protocol;
import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.ProtocolMapperModel; import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.representations.AccessToken;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.util.List; import java.util.List;
@ -41,4 +42,18 @@ public class ProtocolMapperUtils {
} }
return null; return null;
} }
public static String[] parseRole(String role) {
int scopeIndex = role.indexOf('.');
if (scopeIndex > -1) {
String appName = role.substring(0, scopeIndex);
role = role.substring(scopeIndex + 1);
String[] rtn = {appName, role};
return rtn;
} else {
String[] rtn = {null, role};
return rtn;
}
}
} }

View file

@ -5,6 +5,7 @@ import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ProtocolMapperModel; import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel; import org.keycloak.models.UserSessionModel;
import org.keycloak.protocol.ProtocolMapper;
import org.keycloak.protocol.ProtocolMapperUtils; import org.keycloak.protocol.ProtocolMapperUtils;
import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.representations.AccessToken; import org.keycloak.representations.AccessToken;
@ -44,8 +45,13 @@ public class HardcodedClaim extends AbstractOIDCProtocolMapper implements OIDCAc
property = new ConfigProperty(); property = new ConfigProperty();
property.setName(OIDCAttributeMapperHelper.JSON_TYPE); property.setName(OIDCAttributeMapperHelper.JSON_TYPE);
property.setLabel(OIDCAttributeMapperHelper.JSON_TYPE); property.setLabel(OIDCAttributeMapperHelper.JSON_TYPE);
property.setType(ConfigProperty.STRING_TYPE); List<String> types = new ArrayList(3);
property.setDefaultValue(ConfigProperty.STRING_TYPE); types.add("String");
types.add("long");
types.add("int");
types.add("boolean");
property.setType(ProtocolMapper.ConfigProperty.LIST_TYPE);
property.setDefaultValue(types);
property.setHelpText("JSON type that should be used for the value of the claim. long, int, boolean, and String are valid values."); property.setHelpText("JSON type that should be used for the value of the claim. long, int, boolean, and String are valid values.");
configProperties.add(property); configProperties.add(property);
property = new ConfigProperty(); property = new ConfigProperty();

View file

@ -4,6 +4,7 @@ import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ProtocolMapperModel; import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.UserSessionModel; import org.keycloak.models.UserSessionModel;
import org.keycloak.protocol.ProtocolMapperUtils;
import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.representations.AccessToken; import org.keycloak.representations.AccessToken;
@ -48,7 +49,7 @@ public class HardcodedRole extends AbstractOIDCProtocolMapper implements OIDCAcc
@Override @Override
public String getDisplayType() { public String getDisplayType() {
return "Add Role"; return "Hardcoded Role";
} }
@Override @Override
@ -58,19 +59,18 @@ public class HardcodedRole extends AbstractOIDCProtocolMapper implements OIDCAcc
@Override @Override
public String getHelpText() { public String getHelpText() {
return "Hardcode any role specify into the token."; return "Hardcode a role into the access token.";
} }
@Override @Override
public AccessToken transformAccessToken(AccessToken token, ProtocolMapperModel mappingModel, KeycloakSession session, public AccessToken transformAccessToken(AccessToken token, ProtocolMapperModel mappingModel, KeycloakSession session,
UserSessionModel userSession, ClientSessionModel clientSession) { UserSessionModel userSession, ClientSessionModel clientSession) {
String role = mappingModel.getConfig().get(ROLE_CONFIG); String role = mappingModel.getConfig().get(ROLE_CONFIG);
String appName = null; String[] scopedRole = ProtocolMapperUtils.parseRole(role);
int scopeIndex = role.indexOf('.'); String appName = scopedRole[0];
if (scopeIndex > -1) { String roleName = scopedRole[1];
appName = role.substring(0, scopeIndex); if (appName != null) {
role = role.substring(scopeIndex + 1); token.addAccess(appName).addRole(roleName);
token.addAccess(appName).addRole(role);
} else { } else {
AccessToken.Access access = token.getRealmAccess(); AccessToken.Access access = token.getRealmAccess();
if (access == null) { if (access == null) {

View file

@ -2,12 +2,15 @@ package org.keycloak.protocol.oidc.mappers;
import org.keycloak.models.ProtocolMapperModel; import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.protocol.ProtocolMapper;
import org.keycloak.protocol.ProtocolMapperUtils; import org.keycloak.protocol.ProtocolMapperUtils;
import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.representations.AccessToken; import org.keycloak.representations.AccessToken;
import org.keycloak.representations.IDToken; import org.keycloak.representations.IDToken;
import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.List;
import java.util.Map; import java.util.Map;
/** /**
@ -97,4 +100,40 @@ public class OIDCAttributeMapperHelper {
public static boolean includeInAccessToken(ProtocolMapperModel mappingModel) { public static boolean includeInAccessToken(ProtocolMapperModel mappingModel) {
return "true".equals(mappingModel.getConfig().get(INCLUDE_IN_ACCESS_TOKEN)); return "true".equals(mappingModel.getConfig().get(INCLUDE_IN_ACCESS_TOKEN));
} }
public static void addAttributeConfig(List<ProtocolMapper.ConfigProperty> configProperties) {
ProtocolMapper.ConfigProperty property;
property = new ProtocolMapper.ConfigProperty();
property.setName(TOKEN_CLAIM_NAME);
property.setLabel(TOKEN_CLAIM_NAME_LABEL);
property.setType(ProtocolMapper.ConfigProperty.STRING_TYPE);
property.setHelpText("Name of the claim to insert into the token. This can be a fully qualified name like 'address.street'. In this case, a nested json object will be created.");
configProperties.add(property);
property = new ProtocolMapper.ConfigProperty();
property.setName(JSON_TYPE);
property.setLabel(JSON_TYPE);
List<String> types = new ArrayList(3);
types.add("String");
types.add("long");
types.add("int");
types.add("boolean");
property.setType(ProtocolMapper.ConfigProperty.LIST_TYPE);
property.setDefaultValue(types);
property.setHelpText("JSON type that should be used to populate the json claim in the token. long, int, boolean, and String are valid values.");
configProperties.add(property);
property = new ProtocolMapper.ConfigProperty();
property.setName(INCLUDE_IN_ID_TOKEN);
property.setLabel(INCLUDE_IN_ID_TOKEN_LABEL);
property.setType(ProtocolMapper.ConfigProperty.BOOLEAN_TYPE);
property.setDefaultValue("true");
property.setHelpText(INCLUDE_IN_ID_TOKEN_HELP_TEXT);
configProperties.add(property);
property = new ProtocolMapper.ConfigProperty();
property.setName(INCLUDE_IN_ACCESS_TOKEN);
property.setLabel(INCLUDE_IN_ACCESS_TOKEN_LABEL);
property.setType(ProtocolMapper.ConfigProperty.BOOLEAN_TYPE);
property.setDefaultValue("true");
property.setHelpText(INCLUDE_IN_ACCESS_TOKEN_HELP_TEXT);
configProperties.add(property);
}
} }

View file

@ -58,7 +58,7 @@ public class RoleNameMapper extends AbstractOIDCProtocolMapper implements OIDCAc
@Override @Override
public String getDisplayType() { public String getDisplayType() {
return "Role Mapper"; return "Role Name Mapper";
} }
@Override @Override
@ -76,29 +76,35 @@ public class RoleNameMapper extends AbstractOIDCProtocolMapper implements OIDCAc
UserSessionModel userSession, ClientSessionModel clientSession) { UserSessionModel userSession, ClientSessionModel clientSession) {
String role = mappingModel.getConfig().get(ROLE_CONFIG); String role = mappingModel.getConfig().get(ROLE_CONFIG);
String newName = mappingModel.getConfig().get(NEW_ROLE_NAME); String newName = mappingModel.getConfig().get(NEW_ROLE_NAME);
String appName = null;
int scopeIndex = role.indexOf('.'); String[] scopedRole = ProtocolMapperUtils.parseRole(role);
if (scopeIndex > -1) { String[] newScopedRole = ProtocolMapperUtils.parseRole(newName);
appName = role.substring(0, scopeIndex); String appName = scopedRole[0];
String roleName = scopedRole[1];
if (appName != null) {
AccessToken.Access access = token.getResourceAccess(appName); AccessToken.Access access = token.getResourceAccess(appName);
if (access == null) return token; if (access == null) return token;
if (!access.getRoles().contains(roleName)) return token;
role = role.substring(scopeIndex + 1); access.getRoles().remove(roleName);
if (!access.getRoles().contains(role)) return token;
access.getRoles().remove(role);
} else { } else {
AccessToken.Access access = token.getRealmAccess(); AccessToken.Access access = token.getRealmAccess();
if (access == null) return token; if (access == null) return token;
access.getRoles().remove(role); access.getRoles().remove(roleName);
} }
String newAppName = null; String newAppName = newScopedRole[0];
scopeIndex = newName.indexOf('.'); String newRoleName = newScopedRole[1];
if (scopeIndex > -1) { AccessToken.Access access = null;
newAppName = role.substring(0, scopeIndex); if (newAppName == null) {
newName = role.substring(scopeIndex + 1); access = token.getRealmAccess();
token.addAccess(newAppName).addRole(newName); if (access == null) {
access = new AccessToken.Access();
token.setRealmAccess(access);
}
} else {
access = token.addAccess(newAppName);
} }
access.addRole(newRoleName);
return token; return token;
} }

View file

@ -33,33 +33,7 @@ public class UserAttributeMapper extends AbstractOIDCProtocolMapper implements O
property.setHelpText(ProtocolMapperUtils.USER_MODEL_ATTRIBUTE_HELP_TEXT); property.setHelpText(ProtocolMapperUtils.USER_MODEL_ATTRIBUTE_HELP_TEXT);
property.setType(ConfigProperty.STRING_TYPE); property.setType(ConfigProperty.STRING_TYPE);
configProperties.add(property); configProperties.add(property);
property = new ConfigProperty(); OIDCAttributeMapperHelper.addAttributeConfig(configProperties);
property.setName(OIDCAttributeMapperHelper.TOKEN_CLAIM_NAME);
property.setLabel(OIDCAttributeMapperHelper.TOKEN_CLAIM_NAME_LABEL);
property.setType(ConfigProperty.STRING_TYPE);
property.setHelpText("Name of the claim to insert into the token. This can be a fully qualified name like 'address.street'. In this case, a nested json object will be created.");
configProperties.add(property);
property = new ConfigProperty();
property.setName(OIDCAttributeMapperHelper.JSON_TYPE);
property.setLabel(OIDCAttributeMapperHelper.JSON_TYPE);
property.setType(ConfigProperty.STRING_TYPE);
property.setDefaultValue(ConfigProperty.STRING_TYPE);
property.setHelpText("JSON type that should be used to populate the json claim in the token. long, int, boolean, and String are valid values.");
configProperties.add(property);
property = new ConfigProperty();
property.setName(OIDCAttributeMapperHelper.INCLUDE_IN_ID_TOKEN);
property.setLabel(OIDCAttributeMapperHelper.INCLUDE_IN_ID_TOKEN_LABEL);
property.setType(ConfigProperty.BOOLEAN_TYPE);
property.setDefaultValue("true");
property.setHelpText(OIDCAttributeMapperHelper.INCLUDE_IN_ID_TOKEN_HELP_TEXT);
configProperties.add(property);
property = new ConfigProperty();
property.setName(OIDCAttributeMapperHelper.INCLUDE_IN_ACCESS_TOKEN);
property.setLabel(OIDCAttributeMapperHelper.INCLUDE_IN_ACCESS_TOKEN_LABEL);
property.setType(ConfigProperty.BOOLEAN_TYPE);
property.setDefaultValue("true");
property.setHelpText(OIDCAttributeMapperHelper.INCLUDE_IN_ACCESS_TOKEN_HELP_TEXT);
configProperties.add(property);
} }

View file

@ -3,7 +3,6 @@ package org.keycloak.protocol.oidc.mappers;
import org.keycloak.models.ClientSessionModel; import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ProtocolMapperModel; import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel; import org.keycloak.models.UserSessionModel;
import org.keycloak.protocol.ProtocolMapperUtils; import org.keycloak.protocol.ProtocolMapperUtils;
@ -32,33 +31,7 @@ public class UserPropertyMapper extends AbstractOIDCProtocolMapper implements OI
property.setType(ConfigProperty.STRING_TYPE); property.setType(ConfigProperty.STRING_TYPE);
property.setHelpText(ProtocolMapperUtils.USER_MODEL_PROPERTY_HELP_TEXT); property.setHelpText(ProtocolMapperUtils.USER_MODEL_PROPERTY_HELP_TEXT);
configProperties.add(property); configProperties.add(property);
property = new ConfigProperty(); OIDCAttributeMapperHelper.addAttributeConfig(configProperties);
property.setName(OIDCAttributeMapperHelper.TOKEN_CLAIM_NAME);
property.setLabel(OIDCAttributeMapperHelper.TOKEN_CLAIM_NAME_LABEL);
property.setType(ConfigProperty.STRING_TYPE);
property.setHelpText("Name of the claim to insert into the token. This can be a fully qualified name like 'address.street'. In this case, a nested json object will be created.");
configProperties.add(property);
property = new ConfigProperty();
property.setName(OIDCAttributeMapperHelper.JSON_TYPE);
property.setLabel(OIDCAttributeMapperHelper.JSON_TYPE);
property.setType(ConfigProperty.STRING_TYPE);
property.setDefaultValue(ConfigProperty.STRING_TYPE);
property.setHelpText("JSON type that should be used to populate the json claim in the token. long, int, boolean, and String are valid values.");
configProperties.add(property);
property = new ConfigProperty();
property.setName(OIDCAttributeMapperHelper.INCLUDE_IN_ID_TOKEN);
property.setLabel(OIDCAttributeMapperHelper.INCLUDE_IN_ID_TOKEN_LABEL);
property.setType(ConfigProperty.BOOLEAN_TYPE);
property.setDefaultValue("true");
property.setHelpText(OIDCAttributeMapperHelper.INCLUDE_IN_ID_TOKEN_HELP_TEXT);
configProperties.add(property);
property = new ConfigProperty();
property.setName(OIDCAttributeMapperHelper.INCLUDE_IN_ACCESS_TOKEN);
property.setLabel(OIDCAttributeMapperHelper.INCLUDE_IN_ACCESS_TOKEN_LABEL);
property.setType(ConfigProperty.BOOLEAN_TYPE);
property.setDefaultValue("true");
property.setHelpText(OIDCAttributeMapperHelper.INCLUDE_IN_ACCESS_TOKEN_HELP_TEXT);
configProperties.add(property);
} }
public static final String PROVIDER_ID = "oidc-usermodel-property-mapper"; public static final String PROVIDER_ID = "oidc-usermodel-property-mapper";

View file

@ -33,7 +33,7 @@ public class UserSessionNoteMapper extends AbstractOIDCProtocolMapper implements
configProperties.add(property); configProperties.add(property);
property = new ConfigProperty(); property = new ConfigProperty();
property.setName(OIDCAttributeMapperHelper.TOKEN_CLAIM_NAME); property.setName(OIDCAttributeMapperHelper.TOKEN_CLAIM_NAME);
property.setLabel(OIDCAttributeMapperHelper.TOKEN_CLAIM_NAME); property.setLabel(OIDCAttributeMapperHelper.TOKEN_CLAIM_NAME_LABEL);
property.setType(ConfigProperty.STRING_TYPE); property.setType(ConfigProperty.STRING_TYPE);
property.setHelpText("Name of the claim to insert into the token. This can be a fully qualified name like 'address.street'. In this case, a nested json object will be created."); property.setHelpText("Name of the claim to insert into the token. This can be a fully qualified name like 'address.street'. In this case, a nested json object will be created.");
configProperties.add(property); configProperties.add(property);

View file

@ -391,7 +391,7 @@ public class AuthenticationManager {
.setClientSessionCode(accessCode.getCode()) .setClientSessionCode(accessCode.getCode())
.setAccessRequest(realmRoles, resourceRoles) .setAccessRequest(realmRoles, resourceRoles)
.setClient(client) .setClient(client)
.createOAuthGrant(); .createOAuthGrant(clientSession);
} }
event.success(); event.success();

View file

@ -197,7 +197,7 @@ public class IdentityBrokerService {
.setClient(clientModel) .setClient(clientModel)
.setUriInfo(this.uriInfo) .setUriInfo(this.uriInfo)
.setActionUri(this.uriInfo.getRequestUri()) .setActionUri(this.uriInfo.getRequestUri())
.createOAuthGrant(), clientModel); .createOAuthGrant(null), clientModel);
} }
IdentityProvider identityProvider = getIdentityProvider(providerId); IdentityProvider identityProvider = getIdentityProvider(providerId);

View file

@ -607,6 +607,7 @@ public class AccessTokenTest {
app.addProtocolMapper(UserAttributeMapper.createClaimMapper("nested phone", "phone", "home.phone", "String", true, "", true, true)); app.addProtocolMapper(UserAttributeMapper.createClaimMapper("nested phone", "phone", "home.phone", "String", true, "", true, true));
app.addProtocolMapper(HardcodedRole.create("hard-realm", "hardcoded")); app.addProtocolMapper(HardcodedRole.create("hard-realm", "hardcoded"));
app.addProtocolMapper(HardcodedRole.create("hard-app", "app.hardcoded")); app.addProtocolMapper(HardcodedRole.create("hard-app", "app.hardcoded"));
app.addProtocolMapper(RoleNameMapper.create("rename-app-role", "test-app.customer-user", "realm-user"));
session.getTransaction().commit(); session.getTransaction().commit();
session.close(); session.close();
} }
@ -647,6 +648,8 @@ public class AccessTokenTest {
nested = (Map)accessToken.getOtherClaims().get("home"); nested = (Map)accessToken.getOtherClaims().get("home");
Assert.assertEquals("617-777-6666", nested.get("phone")); Assert.assertEquals("617-777-6666", nested.get("phone"));
Assert.assertTrue(accessToken.getRealmAccess().getRoles().contains("hardcoded")); Assert.assertTrue(accessToken.getRealmAccess().getRoles().contains("hardcoded"));
Assert.assertTrue(accessToken.getRealmAccess().getRoles().contains("realm-user"));
Assert.assertFalse(accessToken.getResourceAccess("test-app").getRoles().contains("customer-user"));
Assert.assertTrue(accessToken.getResourceAccess("app").getRoles().contains("hardcoded")); Assert.assertTrue(accessToken.getResourceAccess("app").getRoles().contains("hardcoded"));
@ -665,6 +668,7 @@ public class AccessTokenTest {
|| model.getName().equals("hard-nested") || model.getName().equals("hard-nested")
|| model.getName().equals("custom phone") || model.getName().equals("custom phone")
|| model.getName().equals("nested phone") || model.getName().equals("nested phone")
|| model.getName().equals("rename-app-role")
|| model.getName().equals("hard-realm") || model.getName().equals("hard-realm")
|| model.getName().equals("hard-app") || model.getName().equals("hard-app")
) { ) {