mirror of
https://github.com/keycloak/keycloak.git
synced 2026-01-09 15:02:05 -03:30
hide scopes from scopes_supported in discovery endpoint
Closes #10388 Signed-off-by: cgeorgilakis-grnet <cgeorgilakis@admin.grnet.gr> Signed-off-by: Alexander Schwartz <alexander.schwartz@ibm.com> Co-authored-by: Alexander Schwartz <alexander.schwartz@ibm.com>
This commit is contained in:
parent
2216ada20b
commit
1c0d4616a5
@ -1,6 +1,15 @@
|
||||
|
||||
=== Limiting scope
|
||||
|
||||
By default, new client applications have unlimited `role scope mappings`. Every access token for that client contains all permissions that the user has. If an attacker compromises the client and obtains the client's access tokens, each system that the user can access is compromised.
|
||||
==== Scope availability
|
||||
|
||||
By default, new client applications have unlimited `role scope mappings`. Every access token for that client contains all permissions that the user has. If an attacker compromises the client and obtains the client's access tokens, each system that the user can access is compromised.
|
||||
|
||||
Limit the roles of an access token by using the <<_role_scope_mappings, Scope menu>> for each client. Alternatively, you can set role scope mappings at the Client Scope level and assign Client Scopes to your client by using the <<_client_scopes_linking, Client Scope menu>>.
|
||||
|
||||
Removing the offline scope for a client also removes the ability to issue long-lived offline tokens for a client and offers better control over sessions by users.
|
||||
|
||||
==== Scope visibility
|
||||
|
||||
By default, all scopes are included in the OpenID Connect discovery endpoint.
|
||||
To reduce the discoverability and OSINT-exposure, you can configure each scope to be excluded.
|
||||
|
||||
@ -3542,6 +3542,8 @@ oid4vciEnabled=Enable OID4VCI
|
||||
oid4vciEnabledHelp=Enable this option to allow the client to request verifiable credentials from Keycloak's OID4VCI credential endpoint.
|
||||
noAccessPolicies=No access policies
|
||||
noAccessPoliciesInstructions=There haven't been configured any access policies yet. Click the button below to configure the first policy.
|
||||
includeInOpenIdProviderMetadata=Include in OpenID Provider Metadata
|
||||
includeInOpenIdProviderMetadataHelp=If on, this client scope will be included in OpenID Provider Metadata.
|
||||
# standard error responses OAuth
|
||||
invalid_request=Invalid request
|
||||
unauthorized_client=Unauthorized client
|
||||
@ -3604,4 +3606,4 @@ changeStatusTooltip=Enable or disable this workflow
|
||||
workflowEnabled=Workflow enabled
|
||||
workflowDisabled=Workflow disabled
|
||||
workflowUpdated=Workflow updated successfully
|
||||
workflowUpdateError=Could not update the workflow\: {{error}}
|
||||
workflowUpdateError=Could not update the workflow\: {{error}}
|
||||
|
||||
@ -64,6 +64,7 @@ export const ScopeForm = ({ clientScope, save }: ScopeFormProps) => {
|
||||
|
||||
const isOid4vcProtocol = selectedProtocol === OID4VC_PROTOCOL;
|
||||
const isOid4vcEnabled = isFeatureEnabled(Feature.OpenId4VCI);
|
||||
const isNotSaml = selectedProtocol != "saml";
|
||||
|
||||
const setDynamicRegex = (value: string, append: boolean) =>
|
||||
setValue(
|
||||
@ -190,6 +191,17 @@ export const ScopeForm = ({ clientScope, save }: ScopeFormProps) => {
|
||||
labelIcon={t("includeInTokenScopeHelp")}
|
||||
stringify
|
||||
/>
|
||||
{isNotSaml && (
|
||||
<DefaultSwitchControl
|
||||
name={convertAttributeNameToForm<ClientScopeDefaultOptionalType>(
|
||||
"attributes.include.in.openid.provider.metadata",
|
||||
)}
|
||||
defaultValue="true"
|
||||
label={t("includeInOpenIdProviderMetadata")}
|
||||
labelIcon={t("includeInOpenIdProviderMetadataHelp")}
|
||||
stringify
|
||||
/>
|
||||
)}
|
||||
<TextControl
|
||||
name={convertAttributeNameToForm<ClientScopeDefaultOptionalType>(
|
||||
"attributes.gui.order",
|
||||
|
||||
@ -525,6 +525,16 @@ public class ClientModelLazyDelegate implements ClientModel {
|
||||
return getDelegate().getDynamicScopeRegexp();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isIncludeInOpenIDProviderMetadata() {
|
||||
return getDelegate().isIncludeInOpenIDProviderMetadata();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setIncludeInOpenIDProviderMetadata(boolean includeInOpenIDProviderMetadata) {
|
||||
getDelegate().setIncludeInOpenIDProviderMetadata(includeInOpenIDProviderMetadata);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<RoleModel> getScopeMappingsStream() {
|
||||
return getDelegate().getScopeMappingsStream();
|
||||
|
||||
@ -73,6 +73,7 @@ public interface ClientScopeModel extends ProtocolMapperContainerModel, ScopeCon
|
||||
String INCLUDE_IN_TOKEN_SCOPE = "include.in.token.scope";
|
||||
String IS_DYNAMIC_SCOPE = "is.dynamic.scope";
|
||||
String DYNAMIC_SCOPE_REGEXP = "dynamic.scope.regexp";
|
||||
String INCLUDE_IN_OPENID_PROVIDER_METADATA = "include.in.openid.provider.metadata";
|
||||
|
||||
default boolean isDisplayOnConsentScreen() {
|
||||
String displayVal = getAttribute(DISPLAY_ON_CONSENT_SCREEN);
|
||||
@ -125,4 +126,13 @@ public interface ClientScopeModel extends ProtocolMapperContainerModel, ScopeCon
|
||||
default String getDynamicScopeRegexp() {
|
||||
return getAttribute(DYNAMIC_SCOPE_REGEXP);
|
||||
}
|
||||
|
||||
default boolean isIncludeInOpenIDProviderMetadata() {
|
||||
String includeInOpenIDProviderMetadata = getAttribute(INCLUDE_IN_OPENID_PROVIDER_METADATA);
|
||||
return includeInOpenIDProviderMetadata == null ? true : Boolean.parseBoolean(includeInOpenIDProviderMetadata);
|
||||
}
|
||||
|
||||
default void setIncludeInOpenIDProviderMetadata(boolean includeInOpenIDProviderMetadata) {
|
||||
setAttribute(INCLUDE_IN_OPENID_PROVIDER_METADATA, String.valueOf(includeInOpenIDProviderMetadata));
|
||||
}
|
||||
}
|
||||
|
||||
@ -169,7 +169,7 @@ public class OIDCWellKnownProvider implements WellKnownProvider {
|
||||
// Include client scopes can be disabled in the environments with thousands of client scopes to avoid potentially expensive iteration over client scopes
|
||||
if (includeClientScopes) {
|
||||
List<String> scopeNames = realm.getClientScopesStream()
|
||||
.filter(clientScope -> Objects.equals(OIDCLoginProtocol.LOGIN_PROTOCOL, clientScope.getProtocol()))
|
||||
.filter(clientScope -> Objects.equals(OIDCLoginProtocol.LOGIN_PROTOCOL, clientScope.getProtocol()) && clientScope.isIncludeInOpenIDProviderMetadata())
|
||||
.map(ClientScopeModel::getName)
|
||||
.collect(Collectors.toList());
|
||||
if (!scopeNames.contains(OAuth2Constants.SCOPE_OPENID)) {
|
||||
|
||||
@ -36,13 +36,16 @@ import org.keycloak.crypto.Algorithm;
|
||||
import org.keycloak.http.simple.SimpleHttpResponse;
|
||||
import org.keycloak.jose.jwe.JWEConstants;
|
||||
import org.keycloak.jose.jwk.JSONWebKeySet;
|
||||
import org.keycloak.models.ClientScopeModel;
|
||||
import org.keycloak.models.Constants;
|
||||
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||
import org.keycloak.protocol.oidc.OIDCLoginProtocolFactory;
|
||||
import org.keycloak.protocol.oidc.OIDCLoginProtocolService;
|
||||
import org.keycloak.protocol.oidc.representations.MTLSEndpointAliases;
|
||||
import org.keycloak.protocol.oidc.representations.OIDCConfigurationRepresentation;
|
||||
import org.keycloak.protocol.oidc.utils.OIDCResponseType;
|
||||
import org.keycloak.representations.IDToken;
|
||||
import org.keycloak.representations.idm.ClientScopeRepresentation;
|
||||
import org.keycloak.representations.idm.RealmRepresentation;
|
||||
import org.keycloak.services.clientregistration.ClientRegistrationService;
|
||||
import org.keycloak.services.clientregistration.oidc.OIDCClientRegistrationProviderFactory;
|
||||
@ -51,6 +54,7 @@ import org.keycloak.services.resources.RealmsResource;
|
||||
import org.keycloak.testsuite.AbstractKeycloakTest;
|
||||
import org.keycloak.testsuite.Assert;
|
||||
import org.keycloak.testsuite.AbstractAdminTest;
|
||||
import org.keycloak.testsuite.admin.ApiUtil;
|
||||
import org.keycloak.testsuite.broker.util.SimpleHttpDefault;
|
||||
import org.keycloak.testsuite.forms.BrowserFlowTest;
|
||||
import org.keycloak.testsuite.forms.LevelOfAssuranceFlowTest;
|
||||
@ -60,6 +64,7 @@ import org.keycloak.testsuite.util.oauth.AccessTokenResponse;
|
||||
import org.keycloak.testsuite.util.oauth.AuthorizationEndpointResponse;
|
||||
import org.keycloak.testsuite.util.oauth.OAuthClient;
|
||||
import org.keycloak.testsuite.util.TokenSignatureUtil;
|
||||
import org.keycloak.testsuite.wellknown.CustomOIDCWellKnownProviderFactory;
|
||||
import org.keycloak.util.JsonSerialization;
|
||||
|
||||
import java.io.IOException;
|
||||
@ -67,6 +72,8 @@ import java.net.URI;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static jakarta.ws.rs.core.HttpHeaders.ACCEPT;
|
||||
import static jakarta.ws.rs.core.HttpHeaders.CONTENT_TYPE;
|
||||
@ -388,6 +395,55 @@ public abstract class AbstractWellKnownProviderTest extends AbstractKeycloakTest
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDefaultProviderCustomizations() throws IOException {
|
||||
Client client = AdminClientUtil.createResteasyClient();
|
||||
String showScopeId = null;
|
||||
String hideScopeId = null;
|
||||
try {
|
||||
OIDCConfigurationRepresentation oidcConfig = getOIDCDiscoveryRepresentation(client, OAuthClient.AUTH_SERVER_ROOT);
|
||||
|
||||
// Exact names already tested in OIDC
|
||||
assertScopesSupportedMatchesWithRealm(oidcConfig);
|
||||
|
||||
//create 2 client scope - one with hideFromOpenIDProviderMetadata equal to true
|
||||
ClientScopeRepresentation clientScope = new ClientScopeRepresentation();
|
||||
clientScope.setName("show-scope");
|
||||
clientScope.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
|
||||
Response resp = adminClient.realm("test").clientScopes().create(clientScope);
|
||||
showScopeId = ApiUtil.getCreatedId(resp);
|
||||
resp.close();
|
||||
|
||||
ClientScopeRepresentation clientScope2 = new ClientScopeRepresentation();
|
||||
clientScope2.setName("hidden-scope");
|
||||
clientScope2.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
|
||||
Map<String,String> attributes = new HashMap<>();
|
||||
attributes.put(ClientScopeModel.INCLUDE_IN_OPENID_PROVIDER_METADATA,"false");
|
||||
clientScope2.setAttributes(attributes);
|
||||
Response resp2 = adminClient.realm("test").clientScopes().create(clientScope2);
|
||||
hideScopeId = ApiUtil.getCreatedId(resp2);
|
||||
resp2.close();
|
||||
|
||||
List<String> expectedScopeList = Stream.of(OAuth2Constants.SCOPE_OPENID, OAuth2Constants.OFFLINE_ACCESS,
|
||||
OAuth2Constants.SCOPE_PROFILE, OAuth2Constants.SCOPE_EMAIL, OAuth2Constants.SCOPE_PHONE, OAuth2Constants.SCOPE_ADDRESS, OIDCLoginProtocolFactory.ACR_SCOPE, OIDCLoginProtocolFactory.BASIC_SCOPE,
|
||||
OIDCLoginProtocolFactory.ROLES_SCOPE, OIDCLoginProtocolFactory.WEB_ORIGINS_SCOPE, OIDCLoginProtocolFactory.MICROPROFILE_JWT_SCOPE, OAuth2Constants.ORGANIZATION,
|
||||
ServiceAccountConstants.SERVICE_ACCOUNT_SCOPE, "show-scope").collect(Collectors.toList());
|
||||
oidcConfig = getOIDCDiscoveryRepresentation(client, OAuthClient.AUTH_SERVER_ROOT);
|
||||
assertScopesSupportedMatchesWithRealm(oidcConfig, expectedScopeList);
|
||||
} finally {
|
||||
getTestingClient().testing().setSystemPropertyOnServer(CustomOIDCWellKnownProviderFactory.INCLUDE_CLIENT_SCOPES, null);
|
||||
if ( showScopeId != null)
|
||||
adminClient.realm("test").clientScopes().get(showScopeId).remove();
|
||||
if ( hideScopeId != null)
|
||||
adminClient.realm("test").clientScopes().get(hideScopeId).remove();
|
||||
client.close();
|
||||
}
|
||||
}
|
||||
|
||||
private void assertScopesSupportedMatchesWithRealm(OIDCConfigurationRepresentation oidcConfig, List<String> expectedScopeList) {
|
||||
Assert.assertNames(oidcConfig.getScopesSupported(), expectedScopeList.toArray(new String[expectedScopeList.size()]) );
|
||||
}
|
||||
|
||||
protected void assertScopesSupportedMatchesWithRealm(OIDCConfigurationRepresentation oidcConfig) {
|
||||
Assert.assertNames(oidcConfig.getScopesSupported(), OAuth2Constants.SCOPE_OPENID, OAuth2Constants.OFFLINE_ACCESS,
|
||||
OAuth2Constants.SCOPE_PROFILE, OAuth2Constants.SCOPE_EMAIL, OAuth2Constants.SCOPE_PHONE, OAuth2Constants.SCOPE_ADDRESS, OIDCLoginProtocolFactory.ACR_SCOPE, OIDCLoginProtocolFactory.BASIC_SCOPE,
|
||||
|
||||
@ -17,18 +17,7 @@
|
||||
|
||||
package org.keycloak.testsuite.oidc;
|
||||
|
||||
import jakarta.ws.rs.client.Client;
|
||||
import org.junit.Test;
|
||||
import org.keycloak.protocol.oidc.OIDCWellKnownProviderFactory;
|
||||
import org.keycloak.protocol.oidc.representations.MTLSEndpointAliases;
|
||||
import org.keycloak.protocol.oidc.representations.OIDCConfigurationRepresentation;
|
||||
import org.keycloak.testsuite.Assert;
|
||||
import org.keycloak.testsuite.util.AdminClientUtil;
|
||||
import org.keycloak.testsuite.util.oauth.OAuthClient;
|
||||
import org.keycloak.testsuite.wellknown.CustomOIDCWellKnownProviderFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
@ -39,33 +28,4 @@ public class OIDCWellKnownProviderTest extends AbstractWellKnownProviderTest {
|
||||
return OIDCWellKnownProviderFactory.PROVIDER_ID;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDefaultProviderCustomizations() throws IOException {
|
||||
Client client = AdminClientUtil.createResteasyClient();
|
||||
try {
|
||||
OIDCConfigurationRepresentation oidcConfig = getOIDCDiscoveryRepresentation(client, OAuthClient.AUTH_SERVER_ROOT);
|
||||
|
||||
// Assert that CustomOIDCWellKnownProvider was used as a prioritized provider over default OIDCWellKnownProvider
|
||||
MTLSEndpointAliases mtlsEndpointAliases = oidcConfig.getMtlsEndpointAliases();
|
||||
Assert.assertEquals("https://placeholder-host-set-by-testsuite-provider/registration", mtlsEndpointAliases.getRegistrationEndpoint());
|
||||
Assert.assertEquals("bar", oidcConfig.getOtherClaims().get("foo"));
|
||||
|
||||
// Assert some configuration was overriden
|
||||
Assert.assertEquals("some-new-property-value", oidcConfig.getOtherClaims().get("some-new-property"));
|
||||
Assert.assertEquals("nested-value", ((Map) oidcConfig.getOtherClaims().get("some-new-property-compound")).get("nested1"));
|
||||
Assert.assertNames(oidcConfig.getIntrospectionEndpointAuthMethodsSupported(), "private_key_jwt", "client_secret_jwt", "tls_client_auth", "custom_nonexisting_authenticator");
|
||||
|
||||
// Exact names already tested in OIDC
|
||||
assertScopesSupportedMatchesWithRealm(oidcConfig);
|
||||
|
||||
// Temporarily disable client scopes
|
||||
getTestingClient().testing().setSystemPropertyOnServer(CustomOIDCWellKnownProviderFactory.INCLUDE_CLIENT_SCOPES, "false");
|
||||
oidcConfig = getOIDCDiscoveryRepresentation(client, OAuthClient.AUTH_SERVER_ROOT);
|
||||
Assert.assertNull(oidcConfig.getScopesSupported());
|
||||
} finally {
|
||||
getTestingClient().testing().setSystemPropertyOnServer(CustomOIDCWellKnownProviderFactory.INCLUDE_CLIENT_SCOPES, null);
|
||||
client.close();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user