OIDCLoginProtocolFactory.java
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.protocol.oidc;
import org.jboss.logging.Logger;
import org.keycloak.Config;
import org.keycloak.OAuth2Constants;
import org.keycloak.common.Profile;
import org.keycloak.common.constants.KerberosConstants;
import org.keycloak.common.util.UriUtils;
import org.keycloak.events.EventBuilder;
import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientScopeModel;
import org.keycloak.models.Constants;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel;
import org.keycloak.models.utils.DefaultClientScopes;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.protocol.AbstractLoginProtocolFactory;
import org.keycloak.protocol.LoginProtocol;
import org.keycloak.protocol.oidc.mappers.AcrProtocolMapper;
import org.keycloak.protocol.oidc.mappers.AddressMapper;
import org.keycloak.protocol.oidc.mappers.AllowedWebOriginsProtocolMapper;
import org.keycloak.protocol.oidc.mappers.AudienceResolveProtocolMapper;
import org.keycloak.protocol.oidc.mappers.FullNameMapper;
import org.keycloak.protocol.oidc.mappers.UserAttributeMapper;
import org.keycloak.protocol.oidc.mappers.UserClientRoleMappingMapper;
import org.keycloak.protocol.oidc.mappers.UserPropertyMapper;
import org.keycloak.protocol.oidc.mappers.UserRealmRoleMappingMapper;
import org.keycloak.protocol.oidc.mappers.UserSessionNoteMapper;
import org.keycloak.representations.IDToken;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.services.ServicesLogger;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import static org.keycloak.models.ImpersonationSessionNote.IMPERSONATOR_ID;
import static org.keycloak.models.ImpersonationSessionNote.IMPERSONATOR_USERNAME;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class OIDCLoginProtocolFactory extends AbstractLoginProtocolFactory {
private static final Logger logger = Logger.getLogger(OIDCLoginProtocolFactory.class);
public static final String USERNAME = "username";
public static final String EMAIL = "email";
public static final String EMAIL_VERIFIED = "email verified";
public static final String GIVEN_NAME = "given name";
public static final String FAMILY_NAME = "family name";
public static final String MIDDLE_NAME = "middle name";
public static final String NICKNAME = "nickname";
public static final String PROFILE_CLAIM = "profile";
public static final String PICTURE = "picture";
public static final String WEBSITE = "website";
public static final String GENDER = "gender";
public static final String BIRTHDATE = "birthdate";
public static final String ZONEINFO = "zoneinfo";
public static final String UPDATED_AT = "updated at";
public static final String FULL_NAME = "full name";
public static final String LOCALE = "locale";
public static final String ADDRESS = "address";
public static final String PHONE_NUMBER = "phone number";
public static final String PHONE_NUMBER_VERIFIED = "phone number verified";
public static final String REALM_ROLES = "realm roles";
public static final String CLIENT_ROLES = "client roles";
public static final String AUDIENCE_RESOLVE = "audience resolve";
public static final String ALLOWED_WEB_ORIGINS = "allowed web origins";
public static final String ACR = "acr loa level";
// microprofile-jwt claims
public static final String UPN = "upn";
public static final String GROUPS = "groups";
public static final String ROLES_SCOPE = "roles";
public static final String WEB_ORIGINS_SCOPE = "web-origins";
public static final String MICROPROFILE_JWT_SCOPE = "microprofile-jwt";
public static final String ACR_SCOPE = "acr";
public static final String PROFILE_SCOPE_CONSENT_TEXT = "${profileScopeConsentText}";
public static final String EMAIL_SCOPE_CONSENT_TEXT = "${emailScopeConsentText}";
public static final String ADDRESS_SCOPE_CONSENT_TEXT = "${addressScopeConsentText}";
public static final String PHONE_SCOPE_CONSENT_TEXT = "${phoneScopeConsentText}";
public static final String OFFLINE_ACCESS_SCOPE_CONSENT_TEXT = Constants.OFFLINE_ACCESS_SCOPE_CONSENT_TEXT;
public static final String ROLES_SCOPE_CONSENT_TEXT = "${rolesScopeConsentText}";
public static final String CONFIG_LEGACY_LOGOUT_REDIRECT_URI = "legacy-logout-redirect-uri";
public static final String SUPPRESS_LOGOUT_CONFIRMATION_SCREEN = "suppress-logout-confirmation-screen";
private OIDCProviderConfig providerConfig;
@Override
public void init(Config.Scope config) {
initBuiltIns();
this.providerConfig = new OIDCProviderConfig(config);
if (providerConfig.isLegacyLogoutRedirectUri()) {
logger.warnf("Deprecated switch '%s' is enabled. Please try to disable it and update your clients to use OpenID Connect compliant way for RP-initiated logout.", CONFIG_LEGACY_LOGOUT_REDIRECT_URI);
}
if (providerConfig.suppressLogoutConfirmationScreen()) {
logger.warnf("Deprecated switch '%s' is enabled. Please try to disable it and update your clients to use OpenID Connect compliant way for RP-initiated logout.", SUPPRESS_LOGOUT_CONFIRMATION_SCREEN);
}
}
@Override
public LoginProtocol create(KeycloakSession session) {
return new OIDCLoginProtocol().setSession(session);
}
@Override
public Map<String, ProtocolMapperModel> getBuiltinMappers() {
return builtins;
}
private Map<String, ProtocolMapperModel> builtins = new HashMap<>();
void initBuiltIns() {
ProtocolMapperModel model;
model = UserAttributeMapper.createClaimMapper(USERNAME,
"username",
"preferred_username", String.class.getSimpleName(),
true, true);
builtins.put(USERNAME, model);
model = UserAttributeMapper.createClaimMapper(EMAIL,
"email",
"email", "String",
true, true);
builtins.put(EMAIL, model);
model = UserAttributeMapper.createClaimMapper(GIVEN_NAME,
"firstName",
"given_name", "String",
true, true);
builtins.put(GIVEN_NAME, model);
model = UserAttributeMapper.createClaimMapper(FAMILY_NAME,
"lastName",
"family_name", "String",
true, true);
builtins.put(FAMILY_NAME, model);
createUserAttributeMapper(MIDDLE_NAME, "middleName", IDToken.MIDDLE_NAME, "String");
createUserAttributeMapper(NICKNAME, "nickname", IDToken.NICKNAME, "String");
createUserAttributeMapper(PROFILE_CLAIM, "profile", IDToken.PROFILE, "String");
createUserAttributeMapper(PICTURE, "picture", IDToken.PICTURE, "String");
createUserAttributeMapper(WEBSITE, "website", IDToken.WEBSITE, "String");
createUserAttributeMapper(GENDER, "gender", IDToken.GENDER, "String");
createUserAttributeMapper(BIRTHDATE, "birthdate", IDToken.BIRTHDATE, "String");
createUserAttributeMapper(ZONEINFO, "zoneinfo", IDToken.ZONEINFO, "String");
createUserAttributeMapper(UPDATED_AT, "updatedAt", IDToken.UPDATED_AT, "long");
createUserAttributeMapper(LOCALE, "locale", IDToken.LOCALE, "String");
createUserAttributeMapper(PHONE_NUMBER, "phoneNumber", IDToken.PHONE_NUMBER, "String");
createUserAttributeMapper(PHONE_NUMBER_VERIFIED, "phoneNumberVerified", IDToken.PHONE_NUMBER_VERIFIED, "boolean");
model = UserPropertyMapper.createClaimMapper(EMAIL_VERIFIED,
"emailVerified",
"email_verified", "boolean",
true, true);
builtins.put(EMAIL_VERIFIED, model);
ProtocolMapperModel fullName = FullNameMapper.create(FULL_NAME, true, true, true);
builtins.put(FULL_NAME, fullName);
ProtocolMapperModel address = AddressMapper.createAddressMapper();
builtins.put(ADDRESS, address);
model = UserSessionNoteMapper.createClaimMapper(KerberosConstants.GSS_DELEGATION_CREDENTIAL_DISPLAY_NAME,
KerberosConstants.GSS_DELEGATION_CREDENTIAL,
KerberosConstants.GSS_DELEGATION_CREDENTIAL, "String",
true, false);
builtins.put(KerberosConstants.GSS_DELEGATION_CREDENTIAL, model);
model = UserRealmRoleMappingMapper.create(null, REALM_ROLES, "realm_access.roles", true, false, true);
builtins.put(REALM_ROLES, model);
model = UserClientRoleMappingMapper.create(null, null, CLIENT_ROLES, "resource_access.${client_id}.roles", true, false, true);
builtins.put(CLIENT_ROLES, model);
model = AudienceResolveProtocolMapper.createClaimMapper(AUDIENCE_RESOLVE);
builtins.put(AUDIENCE_RESOLVE, model);
model = AllowedWebOriginsProtocolMapper.createClaimMapper(ALLOWED_WEB_ORIGINS);
builtins.put(ALLOWED_WEB_ORIGINS, model);
builtins.put(IMPERSONATOR_ID.getDisplayName(), UserSessionNoteMapper.createUserSessionNoteMapper(IMPERSONATOR_ID));
builtins.put(IMPERSONATOR_USERNAME.getDisplayName(), UserSessionNoteMapper.createUserSessionNoteMapper(IMPERSONATOR_USERNAME));
model = UserAttributeMapper.createClaimMapper(UPN, "username",
"upn", "String",
true, true);
builtins.put(UPN, model);
model = UserRealmRoleMappingMapper.create(null, GROUPS, GROUPS, true, true, true);
builtins.put(GROUPS, model);
if (Profile.isFeatureEnabled(Profile.Feature.STEP_UP_AUTHENTICATION)) {
model = AcrProtocolMapper.create(ACR, true, true);
builtins.put(ACR, model);
}
}
private void createUserAttributeMapper(String name, String attrName, String claimName, String type) {
ProtocolMapperModel model = UserAttributeMapper.createClaimMapper(name,
attrName,
claimName, type,
true, true, false);
builtins.put(name, model);
}
@Override
protected void createDefaultClientScopesImpl(RealmModel newRealm) {
//name, family_name, given_name, middle_name, nickname, preferred_username, profile, picture, website, gender, birthdate, zoneinfo, locale, and updated_at.
ClientScopeModel profileScope = newRealm.addClientScope(OAuth2Constants.SCOPE_PROFILE);
profileScope.setDescription("OpenID Connect built-in scope: profile");
profileScope.setDisplayOnConsentScreen(true);
profileScope.setConsentScreenText(PROFILE_SCOPE_CONSENT_TEXT);
profileScope.setIncludeInTokenScope(true);
profileScope.setProtocol(getId());
profileScope.addProtocolMapper(builtins.get(FULL_NAME));
profileScope.addProtocolMapper(builtins.get(FAMILY_NAME));
profileScope.addProtocolMapper(builtins.get(GIVEN_NAME));
profileScope.addProtocolMapper(builtins.get(MIDDLE_NAME));
profileScope.addProtocolMapper(builtins.get(NICKNAME));
profileScope.addProtocolMapper(builtins.get(USERNAME));
profileScope.addProtocolMapper(builtins.get(PROFILE_CLAIM));
profileScope.addProtocolMapper(builtins.get(PICTURE));
profileScope.addProtocolMapper(builtins.get(WEBSITE));
profileScope.addProtocolMapper(builtins.get(GENDER));
profileScope.addProtocolMapper(builtins.get(BIRTHDATE));
profileScope.addProtocolMapper(builtins.get(ZONEINFO));
profileScope.addProtocolMapper(builtins.get(LOCALE));
profileScope.addProtocolMapper(builtins.get(UPDATED_AT));
ClientScopeModel emailScope = newRealm.addClientScope(OAuth2Constants.SCOPE_EMAIL);
emailScope.setDescription("OpenID Connect built-in scope: email");
emailScope.setDisplayOnConsentScreen(true);
emailScope.setConsentScreenText(EMAIL_SCOPE_CONSENT_TEXT);
emailScope.setIncludeInTokenScope(true);
emailScope.setProtocol(getId());
emailScope.addProtocolMapper(builtins.get(EMAIL));
emailScope.addProtocolMapper(builtins.get(EMAIL_VERIFIED));
ClientScopeModel addressScope = newRealm.addClientScope(OAuth2Constants.SCOPE_ADDRESS);
addressScope.setDescription("OpenID Connect built-in scope: address");
addressScope.setDisplayOnConsentScreen(true);
addressScope.setConsentScreenText(ADDRESS_SCOPE_CONSENT_TEXT);
addressScope.setIncludeInTokenScope(true);
addressScope.setProtocol(getId());
addressScope.addProtocolMapper(builtins.get(ADDRESS));
ClientScopeModel phoneScope = newRealm.addClientScope(OAuth2Constants.SCOPE_PHONE);
phoneScope.setDescription("OpenID Connect built-in scope: phone");
phoneScope.setDisplayOnConsentScreen(true);
phoneScope.setConsentScreenText(PHONE_SCOPE_CONSENT_TEXT);
phoneScope.setIncludeInTokenScope(true);
phoneScope.setProtocol(getId());
phoneScope.addProtocolMapper(builtins.get(PHONE_NUMBER));
phoneScope.addProtocolMapper(builtins.get(PHONE_NUMBER_VERIFIED));
// 'profile' and 'email' will be default scopes for now. 'address' and 'phone' will be optional scopes
newRealm.addDefaultClientScope(profileScope, true);
newRealm.addDefaultClientScope(emailScope, true);
newRealm.addDefaultClientScope(addressScope, false);
newRealm.addDefaultClientScope(phoneScope, false);
RoleModel offlineRole = newRealm.getRole(OAuth2Constants.OFFLINE_ACCESS);
if (offlineRole != null) {
ClientScopeModel offlineAccessScope = KeycloakModelUtils.getClientScopeByName(newRealm, OAuth2Constants.OFFLINE_ACCESS);
if (offlineAccessScope == null) {
DefaultClientScopes.createOfflineAccessClientScope(newRealm, offlineRole);
}
}
addRolesClientScope(newRealm);
addWebOriginsClientScope(newRealm);
addMicroprofileJWTClientScope(newRealm);
addAcrClientScope(newRealm);
}
public ClientScopeModel addRolesClientScope(RealmModel newRealm) {
ClientScopeModel rolesScope = KeycloakModelUtils.getClientScopeByName(newRealm, ROLES_SCOPE);
if (rolesScope == null) {
rolesScope = newRealm.addClientScope(ROLES_SCOPE);
rolesScope.setDescription("OpenID Connect scope for add user roles to the access token");
rolesScope.setDisplayOnConsentScreen(true);
rolesScope.setConsentScreenText(ROLES_SCOPE_CONSENT_TEXT);
rolesScope.setIncludeInTokenScope(false);
rolesScope.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
rolesScope.addProtocolMapper(builtins.get(REALM_ROLES));
rolesScope.addProtocolMapper(builtins.get(CLIENT_ROLES));
rolesScope.addProtocolMapper(builtins.get(AUDIENCE_RESOLVE));
// 'roles' will be default client scope
newRealm.addDefaultClientScope(rolesScope, true);
} else {
logger.debugf("Client scope '%s' already exists in realm '%s'. Skip creating it.", ROLES_SCOPE, newRealm.getName());
}
return rolesScope;
}
public ClientScopeModel addWebOriginsClientScope(RealmModel newRealm) {
ClientScopeModel originsScope = KeycloakModelUtils.getClientScopeByName(newRealm, WEB_ORIGINS_SCOPE);
if (originsScope == null) {
originsScope = newRealm.addClientScope(WEB_ORIGINS_SCOPE);
originsScope.setDescription("OpenID Connect scope for add allowed web origins to the access token");
originsScope.setDisplayOnConsentScreen(false); // No requesting consent from user for this. It is rather the permission of client
originsScope.setConsentScreenText("");
originsScope.setIncludeInTokenScope(false);
originsScope.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
originsScope.addProtocolMapper(builtins.get(ALLOWED_WEB_ORIGINS));
// 'web-origins' will be default client scope
newRealm.addDefaultClientScope(originsScope, true);
} else {
logger.debugf("Client scope '%s' already exists in realm '%s'. Skip creating it.", WEB_ORIGINS_SCOPE, newRealm.getName());
}
return originsScope;
}
/**
* Adds the {@code microprofile-jwt} optional client scope to the specified realm. If a {@code microprofile-jwt} client scope
* already exists in the realm then the existing scope is returned. Otherwise, a new scope is created and returned.
*
* @param newRealm the realm to which the {@code microprofile-jwt} scope is to be added.
* @return a reference to the {@code microprofile-jwt} client scope that was either created or already exists in the realm.
*/
public ClientScopeModel addMicroprofileJWTClientScope(RealmModel newRealm) {
ClientScopeModel microprofileScope = KeycloakModelUtils.getClientScopeByName(newRealm, MICROPROFILE_JWT_SCOPE);
if (microprofileScope == null) {
microprofileScope = newRealm.addClientScope(MICROPROFILE_JWT_SCOPE);
microprofileScope.setDescription("Microprofile - JWT built-in scope");
microprofileScope.setDisplayOnConsentScreen(false);
microprofileScope.setIncludeInTokenScope(true);
microprofileScope.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
microprofileScope.addProtocolMapper(builtins.get(UPN));
microprofileScope.addProtocolMapper(builtins.get(GROUPS));
newRealm.addDefaultClientScope(microprofileScope, false);
} else {
logger.debugf("Client scope '%s' already exists in realm '%s'. Skip creating it.", MICROPROFILE_JWT_SCOPE, newRealm.getName());
}
return microprofileScope;
}
public void addAcrClientScope(RealmModel newRealm) {
if (Profile.isFeatureEnabled(Profile.Feature.STEP_UP_AUTHENTICATION)) {
ClientScopeModel acrScope = KeycloakModelUtils.getClientScopeByName(newRealm, ACR_SCOPE);
if (acrScope == null) {
acrScope = newRealm.addClientScope(ACR_SCOPE);
acrScope.setDescription("OpenID Connect scope for add acr (authentication context class reference) to the token");
acrScope.setDisplayOnConsentScreen(false);
acrScope.setIncludeInTokenScope(false);
acrScope.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
acrScope.addProtocolMapper(builtins.get(ACR));
// acr will be realm 'default' client scope
newRealm.addDefaultClientScope(acrScope, true);
logger.debugf("Client scope '%s' created in the realm '%s'.", ACR_SCOPE, newRealm.getName());
} else {
logger.debugf("Client scope '%s' already exists in realm '%s'. Skip creating it.", ACR_SCOPE, newRealm.getName());
}
} else {
logger.debugf("Skip creating client scope '%s' in the realm '%s' due the step-up authentication feature is disabled.", ACR_SCOPE, newRealm.getName());
}
}
@Override
protected void addDefaults(ClientModel client) {
}
@Override
public Object createProtocolEndpoint(KeycloakSession session, EventBuilder event) {
return new OIDCLoginProtocolService(session, event, providerConfig);
}
@Override
public String getId() {
return OIDCLoginProtocol.LOGIN_PROTOCOL;
}
@Override
public void setupClientDefaults(ClientRepresentation rep, ClientModel newClient) {
if (rep.getRootUrl() != null && (rep.getRedirectUris() == null || rep.getRedirectUris().isEmpty())) {
String root = rep.getRootUrl();
if (root.endsWith("/")) root = root + "*";
else root = root + "/*";
newClient.addRedirectUri(root);
Set<String> origins = new HashSet<String>();
String origin = UriUtils.getOrigin(root);
logger.debugv("adding default client origin: {0}" , origin);
origins.add(origin);
newClient.setWebOrigins(origins);
}
// if no client type provided, default to public client
if (rep.isBearerOnly() == null
&& rep.isPublicClient() == null) {
newClient.setPublicClient(true);
newClient.setSecret(null);
} else if (!(Boolean.TRUE.equals(rep.isBearerOnly()) || Boolean.TRUE.equals(rep.isPublicClient()))) {
// if client is confidential, generate a secret if none is defined
if (newClient.getSecret() == null) {
KeycloakModelUtils.generateSecret(newClient);
}
}
if (rep.isBearerOnly() == null) newClient.setBearerOnly(false);
if (rep.getAdminUrl() == null && rep.getRootUrl() != null) {
newClient.setManagementUrl(rep.getRootUrl());
}
// Backwards compatibility only
if (rep.isDirectGrantsOnly() != null) {
ServicesLogger.LOGGER.usingDeprecatedDirectGrantsOnly();
newClient.setStandardFlowEnabled(!rep.isDirectGrantsOnly());
newClient.setDirectAccessGrantsEnabled(rep.isDirectGrantsOnly());
} else {
if (rep.isStandardFlowEnabled() == null) newClient.setStandardFlowEnabled(true);
if (rep.isDirectAccessGrantsEnabled() == null) newClient.setDirectAccessGrantsEnabled(true);
}
if (rep.isImplicitFlowEnabled() == null) newClient.setImplicitFlowEnabled(false);
if (rep.isPublicClient() == null) newClient.setPublicClient(true);
if (rep.isFrontchannelLogout() == null) newClient.setFrontchannelLogout(false);
if (OIDCAdvancedConfigWrapper.fromClientRepresentation(rep).getBackchannelLogoutUrl() == null){
OIDCAdvancedConfigWrapper oidcAdvancedConfigWrapper = OIDCAdvancedConfigWrapper.fromClientModel(newClient);
oidcAdvancedConfigWrapper.setBackchannelLogoutSessionRequired(true);
oidcAdvancedConfigWrapper.setBackchannelLogoutRevokeOfflineTokens(false);
}
}
}