Compliant with RFC8414, return server metadata at /.well-known/oauth-authorization-server/realms/{realm}

closes #40923

Signed-off-by: Takashi Norimatsu <takashi.norimatsu.ws@hitachi.com>
This commit is contained in:
Takashi Norimatsu 2025-09-04 02:14:37 +09:00 committed by GitHub
parent 4fec0a8630
commit ea63cdc97a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 131 additions and 3 deletions

View File

@ -180,6 +180,10 @@ public class RealmsResource {
}
private void resolveRealmAndUpdateSession(String realmName) {
resolveRealmAndUpdateSession(session, realmName);
}
private static void resolveRealmAndUpdateSession(KeycloakSession session, String realmName) {
RealmManager realmManager = new RealmManager(session);
RealmModel realm = realmManager.getRealmByName(realmName);
if (realm == null) {
@ -225,8 +229,12 @@ public class RealmsResource {
@Produces(MediaType.APPLICATION_JSON)
public Response getWellKnown(final @PathParam("realm") String name,
final @PathParam("alias") String alias) {
resolveRealmAndUpdateSession(name);
checkSsl(session.getContext().getRealm());
return getWellKnownResponse(session, name, alias, logger);
}
public static Response getWellKnownResponse(KeycloakSession session, String name, String alias, Logger logger) throws NotFoundException {
resolveRealmAndUpdateSession(session, name);
checkSsl(session, session.getContext().getRealm());
WellKnownProviderFactory wellKnownProviderFactoryFound = session.getKeycloakSessionFactory().getProviderFactoriesStream(WellKnownProvider.class)
.map(providerFactory -> (WellKnownProviderFactory) providerFactory)
@ -276,6 +284,10 @@ public class RealmsResource {
}
private void checkSsl(RealmModel realm) {
checkSsl(session, realm);
}
private static void checkSsl(KeycloakSession session, RealmModel realm) {
if (!"https".equals(session.getContext().getUri().getBaseUri().getScheme())
&& realm.getSslRequired().isRequired(session.getContext().getConnection())) {
HttpRequest request = session.getContext().getHttpRequest();

View File

@ -0,0 +1,73 @@
/*
* Copyright 2025 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.services.resources;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.NotFoundException;
import jakarta.ws.rs.OPTIONS;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.UriBuilder;
import jakarta.ws.rs.ext.Provider;
import org.jboss.logging.Logger;
import org.keycloak.models.KeycloakSession;
import org.keycloak.protocol.oauth2.OAuth2WellKnownProviderFactory;
import org.keycloak.services.cors.Cors;
import java.util.List;
@Provider
@Path("/.well-known")
public class ServerMetadataResource {
protected static final Logger logger = Logger.getLogger(ServerMetadataResource.class);
@Context
protected KeycloakSession session;
@OPTIONS
@Path("{provider}/realms/{realm}")
@Produces(MediaType.APPLICATION_JSON)
public Response getOAuth2AuthorizationServerWellKnownVersionPreflight(final @PathParam("provider") String providerName,
final @PathParam("realm") String name) {
if (!isValidProvider(providerName)) throw new NotFoundException();
return Cors.builder().allowedMethods("GET").preflight().auth().add(Response.ok());
}
@GET
@Path("{provider}/realms/{realm}")
@Produces(MediaType.APPLICATION_JSON)
public Response getOAuth2AuthorizationServerWellKnown(final @PathParam("provider") String providerName,
final @PathParam("realm") String name) {
if (!isValidProvider(providerName)) throw new NotFoundException();
return RealmsResource.getWellKnownResponse(session, name, providerName, logger);
}
public static UriBuilder wellKnownOAuthProviderUrl(UriBuilder builder) {
return builder.path(ServerMetadataResource.class).path("{provider}/realms/{realm}");
}
private boolean isValidProvider(String providerName) {
// you can add codes here considering the current status of the implementation (preview, experimental).
if (OAuth2WellKnownProviderFactory.PROVIDER_ID.equals(providerName)) return true;
return false;
}
}

View File

@ -28,6 +28,7 @@ import org.keycloak.services.filters.KeycloakSecurityHeadersFilter;
import org.keycloak.services.resources.KeycloakApplication;
import org.keycloak.services.resources.LoadBalancerResource;
import org.keycloak.services.resources.RealmsResource;
import org.keycloak.services.resources.ServerMetadataResource;
import org.keycloak.services.resources.ThemeResource;
import org.keycloak.services.resources.WelcomeResource;
import org.keycloak.services.resources.admin.AdminRoot;
@ -54,6 +55,7 @@ public class ResteasyKeycloakApplication extends KeycloakApplication {
singletons.add(new ObjectMapperResolver());
classes.add(WelcomeResource.class);
classes.add(ServerMetadataResource.class);
if (MultiSiteUtils.isMultiSiteEnabled()) {
// If we are running in multi-site mode, we need to add a resource which to expose

View File

@ -0,0 +1,37 @@
/*
* Copyright 2025 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.testsuite.oauth;
import jakarta.ws.rs.core.UriBuilder;
import org.keycloak.protocol.oauth2.OAuth2WellKnownProviderFactory;
import org.keycloak.services.resources.ServerMetadataResource;
import org.keycloak.testsuite.oidc.AbstractWellKnownProviderTest;
import java.net.URI;
public class RFC8414CompliantOAuth2WellKnownProviderTest extends AbstractWellKnownProviderTest {
protected String getWellKnownProviderId() {
return OAuth2WellKnownProviderFactory.PROVIDER_ID;
}
protected URI getOIDCDiscoveryUri(UriBuilder builder) {
return ServerMetadataResource.wellKnownOAuthProviderUrl(builder).build(this.getWellKnownProviderId(), "test");
}
}

View File

@ -410,9 +410,13 @@ public abstract class AbstractWellKnownProviderTest extends AbstractKeycloakTest
}
}
protected URI getOIDCDiscoveryUri(UriBuilder builder) {
return RealmsResource.wellKnownProviderUrl(builder).build("test", this.getWellKnownProviderId());
}
private String getOIDCDiscoveryConfiguration(Client client, String uriTemplate) {
UriBuilder builder = UriBuilder.fromUri(uriTemplate);
URI oidcDiscoveryUri = RealmsResource.wellKnownProviderUrl(builder).build("test", this.getWellKnownProviderId());
URI oidcDiscoveryUri = getOIDCDiscoveryUri(builder);
WebTarget oidcDiscoveryTarget = client.target(oidcDiscoveryUri);
Response response = oidcDiscoveryTarget.request().get();