OidcProviderRegistrationValidator.java

/*
 * Copyright (c) 2007-2026 MetaSolutions AB
 *
 * 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.entrystore.rest.springboot.security;

import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.entrystore.rest.springboot.configuration.ConditionalOnBooleanConfig;
import org.entrystore.rest.springboot.configuration.OidcCustomConfiguration;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.stereotype.Component;

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

/**
 * Aborts startup when {@code entrystore.auth.oidc.default-provider} or an
 * {@code entrystore.auth.oidc.provider.{id}} entry names a provider id without a matching
 * {@code spring.security.oauth2.client.registration.{id}} entry. Every id both sides use is known
 * at startup, and such a mismatch breaks logins at runtime — a {@code default-provider} typo turns
 * every hint-less {@code /auth/oidc} into a 500 on an app that reports healthy — so it must fail
 * here, naming the offending key, rather than be discovered from login traffic. The request-time
 * check in {@code AuthController.startOidcLogin} remains as a backstop.
 */
@Component
@ConditionalOnBooleanConfig("entrystore.auth.oidc.enabled")
@RequiredArgsConstructor
public class OidcProviderRegistrationValidator implements InitializingBean {

	private final OidcCustomConfiguration oidcConfiguration;
	// Optional: the bean exists only when spring.security.oauth2.client registrations are configured.
	private final Optional<ClientRegistrationRepository> clientRegistrationRepository;

	@Override
	public void afterPropertiesSet() {
		// An absent repository bean is not an ordinary miss: OIDC is enabled but no registration
		// exists at all (e.g. a mistyped spring.security.oauth2.client.registration prefix), so no
		// login can ever complete — abort rather than let the app report healthy. Resolving the
		// repository up front also keeps the lookups below from re-encoding "absent means not
		// registered", the misclassification this guard exists to remove.
		ClientRegistrationRepository registrations = clientRegistrationRepository.orElseThrow(
				() -> new IllegalStateException("EntryStore startup aborted: entrystore.auth.oidc.enabled=true "
						+ "but no spring.security.oauth2.client.registration entries are configured — no OIDC "
						+ "provider can complete a login. Add at least one client registration or disable OIDC."));
		List<String> unknown = new ArrayList<>();
		String defaultProvider = oidcConfiguration.defaultProvider();
		if (StringUtils.isNotBlank(defaultProvider) && registrations.findByRegistrationId(defaultProvider) == null) {
			unknown.add("entrystore.auth.oidc.default-provider=" + defaultProvider);
		}
		for (String providerId : oidcConfiguration.provider().keySet()) {
			if (registrations.findByRegistrationId(providerId) == null) {
				unknown.add("entrystore.auth.oidc.provider." + providerId + ".*");
			}
		}
		if (!unknown.isEmpty()) {
			throw new IllegalStateException("EntryStore startup aborted: OIDC configuration references "
					+ "provider ids without a matching spring.security.oauth2.client.registration entry: "
					+ unknown + ". Fix the key(s) or add the missing client registration(s).");
		}
	}
}