KEYCLOAK-18276 client content screen enhancement
This commit is contained in:
parent
4fd29759ad
commit
63c9845cb9
22 changed files with 536 additions and 3 deletions
|
@ -14,6 +14,10 @@ public class ClientRepresentation {
|
|||
private String baseUrl;
|
||||
private String effectiveUrl;
|
||||
private ConsentRepresentation consent;
|
||||
private String logoUri;
|
||||
private String policyUri;
|
||||
private String tosUri;
|
||||
|
||||
|
||||
public String getClientId() {
|
||||
return clientId;
|
||||
|
@ -94,4 +98,28 @@ public class ClientRepresentation {
|
|||
public void setConsent(ConsentRepresentation consent) {
|
||||
this.consent = consent;
|
||||
}
|
||||
|
||||
public String getLogoUri() {
|
||||
return logoUri;
|
||||
}
|
||||
|
||||
public void setLogoUri(String logoUri) {
|
||||
this.logoUri = logoUri;
|
||||
}
|
||||
|
||||
public String getPolicyUri() {
|
||||
return policyUri;
|
||||
}
|
||||
|
||||
public void setPolicyUri(String policyUri) {
|
||||
this.policyUri = policyUri;
|
||||
}
|
||||
|
||||
public String getTosUri() {
|
||||
return tosUri;
|
||||
}
|
||||
|
||||
public void setTosUri(String tosUri) {
|
||||
this.tosUri = tosUri;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
package org.keycloak.dom.saml.v2.mdui;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* Java class for localizedURIType complex type.
|
||||
*
|
||||
* <p>
|
||||
* The following schema fragment specifies the expected content contained within this class.
|
||||
*
|
||||
* <pre>
|
||||
* <complexType name="KeywordsType">
|
||||
* <simpleContent>
|
||||
* <extension base="mdui:listOfStrings">
|
||||
* <attribute ref="{http://www.w3.org/XML/1998/namespace}lang use="required""/>
|
||||
* </extension>
|
||||
* </simpleContent>
|
||||
* </complexType>
|
||||
* <simpleType name="listOfStrings">
|
||||
* <list itemType="string"/>
|
||||
* </simpleType>
|
||||
* </pre>
|
||||
*/
|
||||
public class KeywordsType {
|
||||
|
||||
protected List<String> values;
|
||||
protected String lang;
|
||||
|
||||
public KeywordsType(String lang) {
|
||||
this.lang = lang;
|
||||
}
|
||||
|
||||
public List<String> getValues() {
|
||||
return values;
|
||||
}
|
||||
|
||||
public void setValues(List<String> values) {
|
||||
this.values = values;
|
||||
}
|
||||
|
||||
public String getLang() {
|
||||
return lang;
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
package org.keycloak.dom.saml.v2.mdui;
|
||||
|
||||
import java.net.URI;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* Java class for localizedURIType complex type.
|
||||
*
|
||||
* <p>
|
||||
* The following schema fragment specifies the expected content contained within this class.
|
||||
*
|
||||
* <pre>
|
||||
* <complexType name="LogoType">
|
||||
* <simpleContent>
|
||||
* <extension base="<http://www.w3.org/2001/XMLSchema>anyURI">
|
||||
* <attribute name="height" type="positiveInteger" use="required""/>
|
||||
* <attribute name="width" type="positiveInteger" use="required""/>
|
||||
* <attribute ref="{http://www.w3.org/XML/1998/namespace}lang "/>
|
||||
* </extension>
|
||||
* </simpleContent>
|
||||
* </complexType>
|
||||
* </pre>
|
||||
*/
|
||||
public class LogoType {
|
||||
|
||||
protected URI value;
|
||||
protected int height;
|
||||
protected int width;
|
||||
protected String lang;
|
||||
|
||||
public LogoType(int height, int width) {
|
||||
this.height = height;
|
||||
this.width = width;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the value of the value property.
|
||||
*
|
||||
* @return possible object is {@link String }
|
||||
*/
|
||||
public URI getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the value of the value property.
|
||||
*
|
||||
* @param value allowed object is {@link String }
|
||||
*/
|
||||
public void setValue(URI value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public String getLang() {
|
||||
return lang;
|
||||
}
|
||||
|
||||
public void setLang(String lang) {
|
||||
this.lang = lang;
|
||||
}
|
||||
|
||||
public int getHeight() {
|
||||
return height;
|
||||
}
|
||||
|
||||
public int getWidth() {
|
||||
return width;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,92 @@
|
|||
package org.keycloak.dom.saml.v2.mdui;
|
||||
|
||||
import org.keycloak.dom.saml.v2.metadata.LocalizedNameType;
|
||||
import org.keycloak.dom.saml.v2.metadata.LocalizedURIType;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* *
|
||||
* <p>
|
||||
* Java class for UIInfoType complex type.
|
||||
*
|
||||
* <p>
|
||||
* The following schema fragment specifies the expected content contained within this class.
|
||||
*
|
||||
* <pre>
|
||||
* <element name="UIInfo" type="mdui:UIInfoType"/>
|
||||
* <complexType name="UIInfoType">
|
||||
* <choice minOccurs="0" maxOccurs="unbounded">
|
||||
* <element ref="mdui:DisplayName"/>
|
||||
* <element ref="mdui:Description"/>
|
||||
* <element ref="mdui:Keywords"/>
|
||||
* <element ref="mdui:Logo"/>
|
||||
* <element ref="mdui:InformationURL"/>
|
||||
* <element ref="mdui:PrivacyStatementURL"/>
|
||||
* <any namespace="##other" processContents="lax"/>
|
||||
* </choice>
|
||||
* </complexType>
|
||||
*
|
||||
* </pre>
|
||||
*/
|
||||
|
||||
public class UIInfoType implements Serializable {
|
||||
|
||||
protected List<LocalizedNameType> displayName = new ArrayList<>();
|
||||
protected List<LocalizedNameType> description = new ArrayList<>();
|
||||
protected List<KeywordsType> keywords = new ArrayList<>();
|
||||
protected List<LocalizedURIType> informationURL = new ArrayList<>();
|
||||
protected List<LocalizedURIType> privacyStatementURL = new ArrayList<>();
|
||||
protected List<LogoType> logo = new ArrayList<>();
|
||||
|
||||
public void addDisplayName(LocalizedNameType displayName) {
|
||||
this.displayName.add(displayName);
|
||||
}
|
||||
|
||||
public void addDescription(LocalizedNameType description) {
|
||||
this.description.add(description);
|
||||
}
|
||||
|
||||
public void addKeywords(KeywordsType keywords) {
|
||||
this.keywords.add(keywords);
|
||||
}
|
||||
|
||||
public void addInformationURL(LocalizedURIType informationURL) {
|
||||
this.informationURL.add(informationURL);
|
||||
}
|
||||
|
||||
public void addPrivacyStatementURL(LocalizedURIType privacyStatementURL) {
|
||||
this.privacyStatementURL.add(privacyStatementURL);
|
||||
}
|
||||
|
||||
public void addLogo(LogoType logo) {
|
||||
this.logo.add(logo);
|
||||
}
|
||||
|
||||
public List<LocalizedNameType> getDisplayName() {
|
||||
return displayName;
|
||||
}
|
||||
|
||||
public List<LocalizedNameType> getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public List<KeywordsType> getKeywords() {
|
||||
return keywords;
|
||||
}
|
||||
|
||||
public List<LocalizedURIType> getInformationURL() {
|
||||
return informationURL;
|
||||
}
|
||||
|
||||
public List<LocalizedURIType> getPrivacyStatementURL() {
|
||||
return privacyStatementURL;
|
||||
}
|
||||
|
||||
public List<LogoType> getLogo() {
|
||||
return logo;
|
||||
}
|
||||
|
||||
}
|
|
@ -21,6 +21,7 @@ import java.util.Collections;
|
|||
import java.util.List;
|
||||
|
||||
import org.keycloak.dom.saml.v2.mdattr.EntityAttributes;
|
||||
import org.keycloak.dom.saml.v2.mdui.UIInfoType;
|
||||
import org.w3c.dom.Element;
|
||||
|
||||
/**
|
||||
|
@ -111,4 +112,13 @@ public class ExtensionsType {
|
|||
return null;
|
||||
}
|
||||
|
||||
public UIInfoType getUIInfo() {
|
||||
for (Object o : this.any) {
|
||||
if (o instanceof UIInfoType) {
|
||||
return (UIInfoType) o;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -67,6 +67,8 @@ public enum JBossSAMLURIConstants {
|
|||
METADATA_NSURI("urn:oasis:names:tc:SAML:2.0:metadata"),
|
||||
// http://docs.oasis-open.org/security/saml/Post2.0/sstc-metadata-attr-cd-01.pdf
|
||||
METADATA_ENTITY_ATTRIBUTES_NSURI("urn:oasis:names:tc:SAML:metadata:attribute"),
|
||||
//http://docs.oasis-open.org/security/saml/Post2.0/sstc-saml-metadata-ui/v1.0/os/sstc-saml-metadata-ui-v1.0-os.pdf
|
||||
METADATA_UI("urn:oasis:names:tc:SAML:metadata:ui"),
|
||||
|
||||
NAMEID_FORMAT_TRANSIENT("urn:oasis:names:tc:SAML:2.0:nameid-format:transient"),
|
||||
NAMEID_FORMAT_PERSISTENT("urn:oasis:names:tc:SAML:2.0:nameid-format:persistent"),
|
||||
|
|
|
@ -0,0 +1,95 @@
|
|||
package org.keycloak.saml.processing.core.parsers.saml.mdui;
|
||||
|
||||
import static org.keycloak.saml.processing.core.parsers.saml.metadata.SAMLMetadataQNames.ATTR_LANG;
|
||||
import static org.keycloak.saml.processing.core.parsers.saml.metadata.SAMLMetadataQNames.ATTR_WIDTH;
|
||||
import static org.keycloak.saml.processing.core.parsers.saml.metadata.SAMLMetadataQNames.ATTR_HEIGHT;
|
||||
|
||||
import org.keycloak.dom.saml.v2.mdui.KeywordsType;
|
||||
import org.keycloak.dom.saml.v2.mdui.LogoType;
|
||||
import org.keycloak.dom.saml.v2.mdui.UIInfoType;
|
||||
import org.keycloak.dom.saml.v2.metadata.LocalizedNameType;
|
||||
import org.keycloak.dom.saml.v2.metadata.LocalizedURIType;
|
||||
import org.keycloak.saml.common.exceptions.ParsingException;
|
||||
import org.keycloak.saml.common.util.StaxParserUtil;
|
||||
import org.keycloak.saml.processing.core.parsers.saml.metadata.AbstractStaxSamlMetadataParser;
|
||||
import org.keycloak.saml.processing.core.parsers.saml.metadata.SAMLMetadataQNames;
|
||||
|
||||
import javax.xml.stream.XMLEventReader;
|
||||
import javax.xml.stream.events.StartElement;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
|
||||
public class SAMLUIInfoParser extends AbstractStaxSamlMetadataParser<UIInfoType> {
|
||||
|
||||
private static final SAMLUIInfoParser INSTANCE = new SAMLUIInfoParser();
|
||||
|
||||
private SAMLUIInfoParser() {
|
||||
super(SAMLMetadataQNames.UIINFO);
|
||||
}
|
||||
|
||||
public static SAMLUIInfoParser getInstance() {
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected UIInfoType instantiateElement(XMLEventReader xmlEventReader, StartElement element) throws ParsingException {
|
||||
return new UIInfoType();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void processSubElement(XMLEventReader xmlEventReader, UIInfoType target, SAMLMetadataQNames element,
|
||||
StartElement elementDetail) throws ParsingException {
|
||||
switch (element) {
|
||||
case DISPLAY_NAME:
|
||||
LocalizedNameType displayName = new LocalizedNameType(
|
||||
StaxParserUtil.getRequiredAttributeValue(elementDetail, ATTR_LANG));
|
||||
StaxParserUtil.advance(xmlEventReader);
|
||||
displayName.setValue(StaxParserUtil.getElementText(xmlEventReader));
|
||||
target.addDisplayName(displayName);
|
||||
break;
|
||||
case DESCRIPTION:
|
||||
LocalizedNameType description = new LocalizedNameType(
|
||||
StaxParserUtil.getRequiredAttributeValue(elementDetail, ATTR_LANG));
|
||||
StaxParserUtil.advance(xmlEventReader);
|
||||
description.setValue(StaxParserUtil.getElementText(xmlEventReader));
|
||||
target.addDescription(description);
|
||||
break;
|
||||
case KEYWORDS:
|
||||
KeywordsType keywords = new KeywordsType(StaxParserUtil.getRequiredAttributeValue(elementDetail, ATTR_LANG));
|
||||
target.addKeywords(keywords);
|
||||
break;
|
||||
case INFORMATION_URL:
|
||||
LocalizedURIType informationURL = new LocalizedURIType(
|
||||
StaxParserUtil.getRequiredAttributeValue(elementDetail, ATTR_LANG));
|
||||
StaxParserUtil.advance(xmlEventReader);
|
||||
informationURL.setValue(URI.create(StaxParserUtil.getElementText(xmlEventReader)));
|
||||
target.addInformationURL(informationURL);
|
||||
break;
|
||||
case PRIVACY_STATEMENT_URL:
|
||||
LocalizedURIType privacyStatementURL = new LocalizedURIType(
|
||||
StaxParserUtil.getRequiredAttributeValue(elementDetail, ATTR_LANG));
|
||||
StaxParserUtil.advance(xmlEventReader);
|
||||
privacyStatementURL.setValue(URI.create(StaxParserUtil.getElementText(xmlEventReader)));
|
||||
target.addPrivacyStatementURL(privacyStatementURL);
|
||||
break;
|
||||
case LOGO:
|
||||
LogoType logo = new LogoType(
|
||||
Integer.parseInt(StaxParserUtil.getRequiredAttributeValue(elementDetail, ATTR_HEIGHT )),
|
||||
Integer.parseInt(StaxParserUtil.getRequiredAttributeValue(elementDetail, ATTR_WIDTH)));
|
||||
String lang = StaxParserUtil.getAttributeValue(elementDetail, ATTR_LANG);
|
||||
if (lang != null)
|
||||
logo.setLang(lang);
|
||||
StaxParserUtil.advance(xmlEventReader);
|
||||
try {
|
||||
String logoValue =StaxParserUtil.getElementText(xmlEventReader).replaceAll("\\s+", "");
|
||||
logo.setValue(new URI(logoValue));
|
||||
} catch (URISyntaxException ex) {
|
||||
throw new ParsingException(ex);
|
||||
}
|
||||
target.addLogo(logo);
|
||||
break;
|
||||
default:
|
||||
throw LOGGER.parserUnknownTag(StaxParserUtil.getElementName(elementDetail), elementDetail.getLocation());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -23,6 +23,7 @@ import org.keycloak.dom.saml.v2.metadata.ExtensionsType;
|
|||
import org.keycloak.saml.common.exceptions.ParsingException;
|
||||
import org.keycloak.saml.common.util.StaxParserUtil;
|
||||
import org.keycloak.saml.processing.core.parsers.saml.mdattr.SAMLEntityAttributesParser;
|
||||
import org.keycloak.saml.processing.core.parsers.saml.mdui.SAMLUIInfoParser;
|
||||
|
||||
/**
|
||||
* Parses <samlp:Extensions> SAML2 element into series of DOM nodes.
|
||||
|
@ -51,6 +52,9 @@ public class SAMLExtensionsParser extends AbstractStaxSamlMetadataParser<Extensi
|
|||
StartElement elementDetail) throws ParsingException {
|
||||
|
||||
switch (element) {
|
||||
case UIINFO:
|
||||
target.addExtension(SAMLUIInfoParser.getInstance().parse(xmlEventReader));
|
||||
break;
|
||||
case ENTITY_ATTRIBUTES:
|
||||
target.addExtension(SAMLEntityAttributesParser.getInstance().parse(xmlEventReader));
|
||||
break;
|
||||
|
|
|
@ -55,6 +55,15 @@ public enum SAMLMetadataQNames implements HasQName {
|
|||
SURNAME("SurName"),
|
||||
TELEPHONE_NUMBER("TelephoneNumber"),
|
||||
|
||||
//mdui elements
|
||||
DESCRIPTION(JBossSAMLURIConstants.METADATA_UI, "Description"),
|
||||
DISPLAY_NAME(JBossSAMLURIConstants.METADATA_UI, "DisplayName"),
|
||||
INFORMATION_URL(JBossSAMLURIConstants.METADATA_UI, "InformationURL"),
|
||||
KEYWORDS(JBossSAMLURIConstants.METADATA_UI, "Keywords"),
|
||||
LOGO(JBossSAMLURIConstants.METADATA_UI, "Logo"),
|
||||
PRIVACY_STATEMENT_URL(JBossSAMLURIConstants.METADATA_UI, "PrivacyStatementURL"),
|
||||
UIINFO(JBossSAMLURIConstants.METADATA_UI, "UIInfo"),
|
||||
|
||||
// Attribute names
|
||||
ATTR_ENTITY_ID(null, "entityID"),
|
||||
ATTR_ID(null, "ID"),
|
||||
|
@ -77,6 +86,8 @@ public enum SAMLMetadataQNames implements HasQName {
|
|||
ATTR_IS_REQUIRED(null, "isRequired"),
|
||||
ATTR_NAME(null, "Name"),
|
||||
ATTR_NAME_FORMAT(null, "NameFormat"),
|
||||
ATTR_WIDTH(null, "width"),
|
||||
ATTR_HEIGHT(null, "height"),
|
||||
// Elements from other namespaces that can be direct subelements of this namespace's elements
|
||||
SIGNATURE(XmlDSigQNames.SIGNATURE),
|
||||
KEY_INFO(XmlDSigQNames.KEY_INFO),
|
||||
|
|
|
@ -37,6 +37,9 @@ public interface ClientModel extends ClientScopeModel, RoleContainerModel, Prot
|
|||
String PRIVATE_KEY = "privateKey";
|
||||
String PUBLIC_KEY = "publicKey";
|
||||
String X509CERTIFICATE = "X509Certificate";
|
||||
String LOGO_URI ="logoUri";
|
||||
String POLICY_URI ="policyUri";
|
||||
String TOS_URI ="tosUri";
|
||||
|
||||
public static class SearchableFields {
|
||||
public static final SearchableModelField<ClientModel> ID = new SearchableModelField<>("id", String.class);
|
||||
|
|
|
@ -20,6 +20,7 @@ package org.keycloak.protocol.oidc;
|
|||
import org.keycloak.authentication.authenticators.client.X509ClientAuthenticator;
|
||||
import org.keycloak.jose.jws.Algorithm;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.ClientScopeModel;
|
||||
import org.keycloak.models.Constants;
|
||||
import org.keycloak.representations.idm.ClientRepresentation;
|
||||
import org.keycloak.utils.StringUtil;
|
||||
|
@ -316,6 +317,18 @@ public class OIDCAdvancedConfigWrapper {
|
|||
return getAttribute(OIDCConfigAttributes.FRONT_CHANNEL_LOGOUT_URI);
|
||||
}
|
||||
|
||||
public void setLogoUri(String logoUri) {
|
||||
setAttribute(ClientModel.LOGO_URI, logoUri);
|
||||
}
|
||||
|
||||
public void setPolicyUri(String policyUri) {
|
||||
setAttribute(ClientModel.POLICY_URI, policyUri);
|
||||
}
|
||||
|
||||
public void setTosUri(String tosUri) {
|
||||
setAttribute(ClientModel.TOS_URI, tosUri);
|
||||
}
|
||||
|
||||
private String getAttribute(String attrKey) {
|
||||
if (clientModel != null) {
|
||||
return clientModel.getAttribute(attrKey);
|
||||
|
|
|
@ -28,6 +28,7 @@ import org.keycloak.dom.saml.v2.metadata.KeyTypes;
|
|||
import org.keycloak.dom.saml.v2.metadata.SPSSODescriptorType;
|
||||
import org.keycloak.exportimport.ClientDescriptionConverter;
|
||||
import org.keycloak.exportimport.ClientDescriptionConverterFactory;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||
|
@ -231,6 +232,15 @@ public class EntityDescriptorDescriptionConverter implements ClientDescriptionCo
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (spDescriptorType.getExtensions() != null && spDescriptorType.getExtensions().getUIInfo() != null) {
|
||||
if (!spDescriptorType.getExtensions().getUIInfo().getLogo().isEmpty()) {
|
||||
attributes.put(ClientModel.LOGO_URI, spDescriptorType.getExtensions().getUIInfo().getLogo().get(0).getValue().toString());
|
||||
}
|
||||
if (!spDescriptorType.getExtensions().getUIInfo().getPrivacyStatementURL().isEmpty()) {
|
||||
attributes.put(ClientModel.POLICY_URI, spDescriptorType.getExtensions().getUIInfo().getPrivacyStatementURL().stream().filter(dn -> "en".equals(dn.getLang())).findFirst().orElse(spDescriptorType.getExtensions().getUIInfo().getPrivacyStatementURL().get(0)).getValue().toString());
|
||||
}
|
||||
}
|
||||
|
||||
app.setProtocolMappers(spDescriptorType.getAttributeConsumingService().stream().flatMap(att -> att.getRequestedAttribute().stream())
|
||||
.map(attr -> {
|
||||
|
|
|
@ -192,6 +192,18 @@ public class DescriptionConverter {
|
|||
configWrapper.setBackchannelLogoutRevokeOfflineTokens(clientOIDC.getBackchannelLogoutRevokeOfflineTokens());
|
||||
}
|
||||
|
||||
if (clientOIDC.getLogoUri() != null) {
|
||||
configWrapper.setLogoUri(clientOIDC.getLogoUri());
|
||||
}
|
||||
|
||||
if (clientOIDC.getPolicyUri() != null) {
|
||||
configWrapper.setPolicyUri(clientOIDC.getPolicyUri());
|
||||
}
|
||||
|
||||
if (clientOIDC.getTosUri() != null) {
|
||||
configWrapper.setTosUri(clientOIDC.getTosUri());
|
||||
}
|
||||
|
||||
// CIBA
|
||||
String backchannelTokenDeliveryMode = clientOIDC.getBackchannelTokenDeliveryMode();
|
||||
if (backchannelTokenDeliveryMode != null) {
|
||||
|
|
|
@ -287,6 +287,9 @@ public class AccountRestService {
|
|||
UserConsentModel consentModel = consents.get(model.getClientId());
|
||||
if(consentModel != null) {
|
||||
representation.setConsent(modelToRepresentation(consentModel));
|
||||
representation.setLogoUri(model.getAttribute(ClientModel.LOGO_URI));
|
||||
representation.setPolicyUri(model.getAttribute(ClientModel.POLICY_URI));
|
||||
representation.setTosUri(model.getAttribute(ClientModel.TOS_URI));
|
||||
}
|
||||
return representation;
|
||||
}
|
||||
|
|
|
@ -17,10 +17,11 @@
|
|||
package org.keycloak.validation;
|
||||
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.ClientScopeModel;
|
||||
import org.keycloak.protocol.ProtocolMapperConfigException;
|
||||
import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper;
|
||||
import org.keycloak.protocol.oidc.grants.ciba.CibaClientValidation;
|
||||
import org.keycloak.protocol.oidc.OIDCConfigAttributes;
|
||||
import org.keycloak.protocol.oidc.grants.ciba.CibaClientValidation;
|
||||
import org.keycloak.protocol.oidc.mappers.PairwiseSubMapperHelper;
|
||||
import org.keycloak.protocol.oidc.utils.PairwiseSubMapperUtils;
|
||||
import org.keycloak.protocol.oidc.utils.PairwiseSubMapperValidator;
|
||||
|
@ -58,7 +59,22 @@ public class DefaultClientValidationProvider implements ClientValidationProvider
|
|||
BACKCHANNEL_LOGOUT_URL("backchannelLogoutUrl",
|
||||
"Backchannel logout URL is not a valid URL", "backchannelLogoutUrlIsInvalid",
|
||||
null, null,
|
||||
"Backchannel logout URL uses an illegal scheme", "backchannelLogoutUrlIllegalSchemeError");
|
||||
"Backchannel logout URL uses an illegal scheme", "backchannelLogoutUrlIllegalSchemeError"),
|
||||
|
||||
LOGO_URI(ClientModel.LOGO_URI,
|
||||
"Logo URL is not a valid URL", "logoURLInvalid",
|
||||
null, null,
|
||||
"Logo URL uses an illegal scheme", "logoURLIllegalSchemeError"),
|
||||
|
||||
POLICY_URI(ClientModel.POLICY_URI,
|
||||
"Policy URL is not a valid URL", "policyURLInvalid",
|
||||
null, null,
|
||||
"Policy URL uses an illegal scheme", "policyURLIllegalSchemeError"),
|
||||
|
||||
TOS_URI(ClientModel.TOS_URI,
|
||||
"Terms of service URL is not a valid URL", "tosURLInvalid",
|
||||
null, null,
|
||||
"Terms of service URL uses an illegal scheme", "tosURLIllegalSchemeError");
|
||||
|
||||
private String fieldId;
|
||||
|
||||
|
@ -151,6 +167,9 @@ public class DefaultClientValidationProvider implements ClientValidationProvider
|
|||
client.getRedirectUris().stream()
|
||||
.map(u -> ResolveRelative.resolveRelativeUri(authServerUrl, authServerUrl, rootUrl, u))
|
||||
.forEach(u -> checkUri(FieldMessages.REDIRECT_URIS, u, context, false, true));
|
||||
checkUriLogo(FieldMessages.LOGO_URI, client.getAttribute(ClientModel.LOGO_URI), context);
|
||||
checkUri(FieldMessages.POLICY_URI, client.getAttribute(ClientModel.POLICY_URI), context, true, false);
|
||||
checkUri(FieldMessages.TOS_URI, client.getAttribute(ClientModel.TOS_URI), context, true, false);
|
||||
}
|
||||
|
||||
private void checkUri(FieldMessages field, String url, ValidationContext<ClientModel> context, boolean checkValidUrl, boolean checkFragment) {
|
||||
|
@ -185,6 +204,24 @@ public class DefaultClientValidationProvider implements ClientValidationProvider
|
|||
}
|
||||
}
|
||||
|
||||
private void checkUriLogo(FieldMessages field, String url, ValidationContext<ClientModel> context) {
|
||||
if (url == null || url.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
URI uri = new URI(url);
|
||||
|
||||
if (uri.getScheme() != null && uri.getScheme().equals("javascript")) {
|
||||
context.addError(field.getFieldId(), field.getScheme(), field.getSchemeKey());
|
||||
}
|
||||
|
||||
}
|
||||
catch (URISyntaxException e) {
|
||||
context.addError(field.getFieldId(), field.getInvalid(), field.getInvalidKey());
|
||||
}
|
||||
}
|
||||
|
||||
private void validatePairwiseInClientModel(ValidationContext<ClientModel> context) {
|
||||
List<ProtocolMapperRepresentation> foundPairwiseMappers = PairwiseSubMapperUtils.getPairwiseSubMappers(toRepresentation(context.getObjectToValidate(), context.getSession()));
|
||||
|
||||
|
|
|
@ -31,6 +31,7 @@ import org.keycloak.admin.client.resource.UsersResource;
|
|||
import org.keycloak.common.Profile;
|
||||
import org.keycloak.events.Details;
|
||||
import org.keycloak.events.Errors;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.representations.idm.ClientRepresentation;
|
||||
import org.keycloak.representations.idm.ClientScopeRepresentation;
|
||||
import org.keycloak.representations.idm.EventRepresentation;
|
||||
|
@ -65,6 +66,7 @@ import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.A
|
|||
import org.keycloak.testsuite.util.OAuthClient;
|
||||
import org.keycloak.testsuite.util.OAuthClient.AccessTokenResponse;
|
||||
import org.keycloak.testsuite.util.OAuthClient.AuthorizationEndpointResponse;
|
||||
import org.openqa.selenium.By;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
|
||||
|
@ -468,6 +470,37 @@ public class ConsentsTest extends AbstractKeycloakTest {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConsentWithAdditionalClientAttributes() {
|
||||
// setup account client to require consent
|
||||
RealmResource providerRealm = adminClient.realm(providerRealmName());
|
||||
ClientResource accountClient = findClientByClientId(providerRealm, "account");
|
||||
|
||||
ClientRepresentation clientRepresentation = accountClient.toRepresentation();
|
||||
clientRepresentation.setConsentRequired(true);
|
||||
clientRepresentation.getAttributes().put(ClientModel.LOGO_URI,"https://www.keycloak.org/resources/images/keycloak_logo_480x108.png");
|
||||
clientRepresentation.getAttributes().put(ClientModel.POLICY_URI,"https://www.keycloak.org/policy");
|
||||
clientRepresentation.getAttributes().put(ClientModel.TOS_URI,"https://www.keycloak.org/tos");
|
||||
accountClient.update(clientRepresentation);
|
||||
|
||||
// setup correct realm
|
||||
accountPage.setAuthRealm(providerRealmName());
|
||||
|
||||
// navigate to account console and login
|
||||
accountPage.navigateTo();
|
||||
loginPage.form().login(getUserLogin(), getUserPassword());
|
||||
|
||||
consentPage.assertCurrent();
|
||||
assertTrue("logoUri must be presented", driver.findElement(By.xpath("//img[@src='https://www.keycloak.org/resources/images/keycloak_logo_480x108.png']")).isDisplayed());
|
||||
assertTrue("policyUri must be presented", driver.findElement(By.xpath("//a[@href='https://www.keycloak.org/policy']")).isDisplayed());
|
||||
assertTrue("tosUri must be presented", driver.findElement(By.xpath("//a[@href='https://www.keycloak.org/tos']")).isDisplayed());
|
||||
|
||||
consentPage.confirm();
|
||||
|
||||
// successful login
|
||||
accountPage.assertCurrent();
|
||||
}
|
||||
|
||||
private String getAccountUrl(String realmName) {
|
||||
return getAuthRoot() + "/auth/realms/" + realmName + "/account";
|
||||
}
|
||||
|
|
|
@ -450,6 +450,13 @@ authorization-encrypted-response-alg=Authorization Response Encryption Key Manag
|
|||
authorization-encrypted-response-alg.tooltip=JWA Algorithm used for key management in encrypting the authorization response when the response mode is jwt. This option is needed if you want encrypted authorization response. If left empty, the authorization response is just signed, but not encrypted.
|
||||
authorization-encrypted-response-enc=Authorization Response Encryption Content Encryption Algorithm
|
||||
authorization-encrypted-response-enc.tooltip=JWA Algorithm used for content encryption in encrypting the authorization response when the response mode is jwt. This option is needed if you want encrypted authorization response. If left empty, the authorization response is just signed, but not encrypted.
|
||||
logo-uri=Logo URL
|
||||
logo-uri.tooltip=URL that references a logo for the Client application
|
||||
policy-uri=Policy URL
|
||||
policy-uri.tooltip=URL that the Relying Party Client provides to the End-User to read about the how the profile data will be used
|
||||
tos-uri=Terms of service URL
|
||||
tos-uri.tooltip=URL that the Relying Party Client provides to the End-User to read about the Relying Party's terms of service
|
||||
|
||||
|
||||
# client import
|
||||
import-client=Import Client
|
||||
|
|
|
@ -369,6 +369,27 @@
|
|||
</div>
|
||||
<kc-tooltip>{{:: 'idp-sso-url-ref.tooltip' | translate}}</kc-tooltip>
|
||||
</div>
|
||||
<div class="form-group" data-ng-show="protocol == 'saml' || protocol == 'openid-connect'">
|
||||
<label class="col-md-2 control-label" for="logoUri">{{:: 'logo-uri' | translate}}</label>
|
||||
<div class="col-sm-6">
|
||||
<input class="form-control" type="text" name="logoUri" id="logoUri" data-ng-model="clientEdit.attributes.logoUri">
|
||||
</div>
|
||||
<kc-tooltip>{{:: 'logo-uri.tooltip' | translate}}</kc-tooltip>
|
||||
</div>
|
||||
<div class="form-group" data-ng-show="protocol == 'saml' || protocol == 'openid-connect'">
|
||||
<label class="col-md-2 control-label" for="policyUri">{{:: 'policy-uri' | translate}}</label>
|
||||
<div class="col-sm-6">
|
||||
<input class="form-control" type="text" name="policyUri" id="policyUri" data-ng-model="clientEdit.attributes.policyUri">
|
||||
</div>
|
||||
<kc-tooltip>{{:: 'policy-uri.tooltip' | translate}}</kc-tooltip>
|
||||
</div>
|
||||
<div class="form-group" data-ng-show="protocol == 'saml' || protocol == 'openid-connect'">
|
||||
<label class="col-md-2 control-label" for="tosUri">{{:: 'tos-uri' | translate}}</label>
|
||||
<div class="col-sm-6">
|
||||
<input class="form-control" type="text" name="tosUri" id="tosUri" data-ng-model="clientEdit.attributes.tosUri">
|
||||
</div>
|
||||
<kc-tooltip>{{:: 'tos-uri.tooltip' | translate}}</kc-tooltip>
|
||||
</div>
|
||||
<div class="form-group clearfix block" data-ng-show="protocol == 'saml'">
|
||||
<label class="col-md-2 control-label" for="idpInitiatedRelayState">{{:: 'idp-sso-relay-state' | translate}}</label>
|
||||
<div class="col-sm-6">
|
||||
|
|
|
@ -1,11 +1,16 @@
|
|||
<#import "template.ftl" as layout>
|
||||
<@layout.registrationLayout bodyClass="oauth"; section>
|
||||
<#if section = "header">
|
||||
<#if client.attributes.logoUri??>
|
||||
<img src="${client.attributes.logoUri}"/>
|
||||
</#if>
|
||||
<p>
|
||||
<#if client.name?has_content>
|
||||
${msg("oauthGrantTitle",advancedMsg(client.name))}
|
||||
<#else>
|
||||
${msg("oauthGrantTitle",client.clientId)}
|
||||
</#if>
|
||||
</p>
|
||||
<#elseif section = "form">
|
||||
<div id="kc-oauth" class="content-area">
|
||||
<h3>${msg("oauthGrantRequest")}</h3>
|
||||
|
@ -18,6 +23,23 @@
|
|||
</#list>
|
||||
</#if>
|
||||
</ul>
|
||||
<#if client.attributes.policyUri?? || client.attributes.tosUri??>
|
||||
<h3>
|
||||
<#if client.name?has_content>
|
||||
${msg("oauthGrantInformation",advancedMsg(client.name))}
|
||||
<#else>
|
||||
${msg("oauthGrantInformation",client.clientId)}
|
||||
</#if>
|
||||
<#if client.attributes.tosUri??>
|
||||
${msg("oauthGrantReview")}
|
||||
<a href="${client.attributes.tosUri}" target="_blank">${msg("oauthGrantTos")}</a>
|
||||
</#if>
|
||||
<#if client.attributes.policyUri??>
|
||||
${msg("oauthGrantReview")}
|
||||
<a href="${client.attributes.policyUri}" target="_blank">${msg("oauthGrantPolicy")}</a>
|
||||
</#if>
|
||||
</h3>
|
||||
</#if>
|
||||
|
||||
<form class="form-actions" action="${url.oauthAction}" method="POST">
|
||||
<input type="hidden" name="code" value="${oauth.code}">
|
||||
|
|
|
@ -35,6 +35,10 @@ loginIdpReviewProfileTitle=Update Account Information
|
|||
loginTimeout=Your login attempt timed out. Login will start from the beginning.
|
||||
oauthGrantTitle=Grant Access to {0}
|
||||
oauthGrantTitleHtml={0}
|
||||
oauthGrantInformation=Make sure you trust {0} by learning how {0} will handle your data.
|
||||
oauthGrantReview=You could review the
|
||||
oauthGrantTos=terms of service.
|
||||
oauthGrantPolicy=privacy policy.
|
||||
errorTitle=We are sorry...
|
||||
errorTitleHtml=We are <strong>sorry</strong> ...
|
||||
emailVerifyTitle=Email verification
|
||||
|
|
|
@ -113,6 +113,8 @@ removeModalTitle=Remove Access
|
|||
removeModalMessage=This will remove the currently granted access permission for {0}. You will need to grant access again if you want to use this app.
|
||||
confirmButton=Confirm
|
||||
infoMessage=By clicking 'Remove Access', you will remove granted permissions of this application. This application will no longer use your information.
|
||||
termsOfService=Terms of service
|
||||
policy=Privacy policy
|
||||
|
||||
#Delete Account page
|
||||
doDelete=Delete
|
||||
|
|
|
@ -68,6 +68,9 @@ interface Application {
|
|||
offlineAccess: boolean;
|
||||
userConsentRequired: boolean;
|
||||
scope: string[];
|
||||
logoUri: string;
|
||||
policyUri: string;
|
||||
tosUri: string;
|
||||
}
|
||||
|
||||
export class ApplicationsPage extends React.Component<ApplicationsPageProps, ApplicationsPageState> {
|
||||
|
@ -175,7 +178,7 @@ export class ApplicationsPage extends React.Component<ApplicationsPageProps, App
|
|||
id={this.elementId("expandable", application)}
|
||||
isHidden={!this.state.isRowOpen[appIndex]}
|
||||
>
|
||||
<Grid sm={12} md={12} lg={12}>
|
||||
<Grid sm={6} md={6} lg={6}>
|
||||
<div className='pf-c-content'>
|
||||
<GridItem><strong>{Msg.localize('client') + ': '}</strong> {application.clientId}</GridItem>
|
||||
{application.description &&
|
||||
|
@ -196,6 +199,8 @@ export class ApplicationsPage extends React.Component<ApplicationsPageProps, App
|
|||
</React.Fragment>
|
||||
)
|
||||
})}
|
||||
{application.tosUri && <GridItem><strong>{Msg.localize('termsOfService') + ': '}</strong>{application.tosUri}</GridItem>}
|
||||
{application.policyUri && <GridItem><strong>{Msg.localize('policy') + ': '}</strong>{application.policyUri}</GridItem>}
|
||||
<GridItem><strong>{Msg.localize('accessGrantedOn') + ': '}</strong>
|
||||
{new Intl.DateTimeFormat(locale, {
|
||||
year: 'numeric',
|
||||
|
@ -209,6 +214,7 @@ export class ApplicationsPage extends React.Component<ApplicationsPageProps, App
|
|||
</React.Fragment>
|
||||
}
|
||||
</div>
|
||||
{application.logoUri && <div className='pf-c-content'><img src={application.logoUri} /></div> }
|
||||
</Grid>
|
||||
{(application.consent || application.offlineAccess) &&
|
||||
<Grid gutter='sm'>
|
||||
|
|
Loading…
Reference in a new issue