8000 Compliant with RFC8414, return server metadata at /.well-known/oauth-authorization-server/realms/{realm} by tnorimat · Pull Request #41440 · keycloak/keycloak · GitHub
[go: up one dir, main page]

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,10 @@ public ClientsManagementService getClientsManagementService(final @PathParam("re
}

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) {
Expand Down Expand Up @@ -225,8 +229,12 @@ public Response getVersionPreflight(final @PathParam("realm") String name,
@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)
Expand Down Expand Up @@ -276,6 +284,10 @@ public Object resolveRealmExtension(@PathParam("realm") String realmName, @PathP
}

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();
Expand Down
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hello @tnorimat - I am jumping in because a very identical change is required by OpenID4VCI Draft 16 as we will need /.well-known/openid-credential-issuer/realms/{realm}.

https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0-16.html#name-credential-issuer-metadata-

Credential Issuers publishing metadata MUST make a JSON document available at the path formed inserting the string /.well-known/openid-credential-issuer into the Credential Issuer Identifier between the host component and the path component, if any.

For example, the metadata for the Credential Issuer Identifier https://issuer.example.com/tenant would be retrieved from https://issuer.example.com/.well-known/openid-credential-issuer/tenant. The metadata for the Credential Issuer Identifier https://tenant.issuer.example.com would be retrieved from https://tenant.issuer.example.com/.well-known/openid-credential-issuer.

Could this PR take into account extensibility regarding well-known resources in general? I mean something close to the suggestion of @thomasdarimont here: https://cloud-native.slack.com/archives/C05KR0TL4P8/p1752669894360769?thread_ts=1752669762.102799&cid=C05KR0TL4P8 ?

@Path(".well-known")
public class WellKnownRootResource {

    protected static final Logger logger = Logger.getLogger(WellKnownRootResource.class);

    @Context
    protected KeycloakSession session;

    @GET
    @Path("/{providerId}/realms/{realmName}")
    public Response getWellKnown(@PathParam("providerId") String providerId, @PathParam("realmName") String realmName) {

        RealmModel realm = session.realms().getRealmByName(realmName);
        if (realm == null) {
            return Response.status(Response.Status.NOT_FOUND).build();
        }

        session.getContext().setRealm(realm);
        WellKnownProvider knownProvider = session.getProvider(WellKnownProvider.class, providerId);
        if (knownProvider == null) {
            return Response.status(Response.Status.NOT_FOUND).build();
        }

        Object config = knownProvider.getConfig();
        return Response.ok(config).build();
    }
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@IngridPuppet Hello, thank you for the feedback comment. I will incorporate the extensibility you suggested.

Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
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;
Expand All @@ -54,6 +55,7 @@ public ResteasyKeycloakApplication() {

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
Expand Down
Original file line number Diff line number Diff line change
@@ -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");
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -410,9 +410,13 @@ protected OIDCConfigurationRepresentation getOIDCDiscoveryRepresentation(Client
}
}

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();
Expand Down
0