From 5ac24678fc40ae7333495ecf6d21d404e6e867f6 Mon Sep 17 00:00:00 2001 From: Valera V Harseko Date: Wed, 23 Sep 2026 17:00:53 +0300 Subject: [PATCH 1/2] [#225] Bind identity provider configs through the DS bind methods The @Reference to IdentityProviderConfig sat on a Map field, a type Declarative Services cannot inject, so SCR rejected it and bindIdentityProviderConfig was never called: /identityProviders stayed empty and no social auth module was generated. Move the annotation to the bind method, and do the same for AuthenticationService, whose bindIdentityProviderService (which registers the provider listener) was bypassed by field injection too. Also make getIdentityProviderByType return an empty list for a type with no bound provider, and add providers atomically. Fixes #225 --- .../openidm/auth/AuthenticationService.java | 21 +++++++++--- .../auth/AuthenticationServiceTest.java | 34 ++++++++++++++++++- .../idp/impl/IdentityProviderService.java | 29 +++++++--------- .../idp/impl/IdentityProviderServiceTest.java | 34 +++++++++++++++++++ 4 files changed, 96 insertions(+), 22 deletions(-) diff --git a/openidm-authnfilter/src/main/java/org/forgerock/openidm/auth/AuthenticationService.java b/openidm-authnfilter/src/main/java/org/forgerock/openidm/auth/AuthenticationService.java index 567e6fd8ee..47a6106a82 100644 --- a/openidm-authnfilter/src/main/java/org/forgerock/openidm/auth/AuthenticationService.java +++ b/openidm-authnfilter/src/main/java/org/forgerock/openidm/auth/AuthenticationService.java @@ -12,7 +12,7 @@ * information: "Portions copyright [year] [name of copyright owner]". * * Copyright 2013-2016 ForgeRock AS - * Portions copyright 2024-2025 3A Systems LLC. + * Portions copyright 2024-2026 3A Systems LLC. */ package org.forgerock.openidm.auth; @@ -243,17 +243,28 @@ public class AuthenticationService implements SingletonResourceProvider, Identit @Reference(policy = ReferencePolicy.DYNAMIC, target="(service.pid=org.forgerock.openidm.auth.config)") private volatile AuthFilterWrapper authFilterWrapper; - @Reference(policy = ReferencePolicy.DYNAMIC, cardinality = ReferenceCardinality.OPTIONAL) private volatile IdentityProviderService identityProviderService; - void bindIdentityProviderService(IdentityProviderService identityProviderService) { + @Reference( + name = "identityProviderService", + policy = ReferencePolicy.DYNAMIC, + cardinality = ReferenceCardinality.OPTIONAL, + unbind = "unbindIdentityProviderService") + void bindIdentityProviderService(IdentityProviderService identityProviderService) + throws IdentityProviderServiceException { this.identityProviderService = identityProviderService; identityProviderService.registerIdentityProviderListener(this); + // no-op until activated; rebuilds the social auth modules if the service arrives later + identityProviderConfigChanged(); } - void unbindIdentityProviderService() { + void unbindIdentityProviderService(IdentityProviderService identityProviderService) + throws IdentityProviderServiceException { identityProviderService.unregisterIdentityProviderListener(this); - identityProviderService = null; + if (this.identityProviderService == identityProviderService) { + this.identityProviderService = null; + identityProviderConfigChanged(); + } } /** An on-demand Provider for the ConnectionFactory */ diff --git a/openidm-authnfilter/src/test/java/org/forgerock/openidm/auth/AuthenticationServiceTest.java b/openidm-authnfilter/src/test/java/org/forgerock/openidm/auth/AuthenticationServiceTest.java index 19f001e772..954b469c5a 100644 --- a/openidm-authnfilter/src/test/java/org/forgerock/openidm/auth/AuthenticationServiceTest.java +++ b/openidm-authnfilter/src/test/java/org/forgerock/openidm/auth/AuthenticationServiceTest.java @@ -22,6 +22,7 @@ import static org.forgerock.json.resource.Requests.newReadRequest; import static org.forgerock.openidm.auth.AuthenticationService.Action; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import javax.security.auth.message.MessageInfo; @@ -86,7 +87,6 @@ public void setUp() throws Exception { OBJECT_MAPPER.readValue(getClass().getResource("/config/authentication.json"), Map.class)); // Instantiate the object to be used with proper mocked IdentityProviderService authenticationService = new AuthenticationService(); - authenticationService.setConfig(authenticationJson); } @AfterMethod @@ -111,6 +111,8 @@ public void testAmendAuthConfig() throws Exception { // Instantiate the object to be used with proper mocked IdentityProviderService authenticationService.bindIdentityProviderService(identityProviderService); + // the reference is bound before the component is activated with its configuration + authenticationService.setConfig(authenticationJson); // Call the amendAuthConfig to see the configuration of authentication.json be modified with // the injected identityProvider config from the IdentityProviderService @@ -154,6 +156,8 @@ public void testAmendAuthConfigWithTwoAuthTypes() throws Exception { // Instantiate the object to be used with proper mocked IdentityProviderService authenticationService.bindIdentityProviderService(identityProviderService); + // the reference is bound before the component is activated with its configuration + authenticationService.setConfig(authenticationJson); // Call the amendAuthConfig to see the configuration of authentication.json be modified with // the injected identityProvider config from the IdentityProviderService @@ -183,6 +187,8 @@ public void testNoProviderConfigsToInject() throws Exception { when(identityProviderService.getIdentityProviders()).thenReturn(providerConfigs); authenticationService.bindIdentityProviderService(identityProviderService); + // the reference is bound before the component is activated with its configuration + authenticationService.setConfig(authenticationJson); // Call the amendAuthConfig to see the configuration of authentication.json be modified with // the injected identityProvider config from the IdentityProviderService; in this test case @@ -234,6 +240,32 @@ public void amendAuthConfigShouldRemoveSocialProvidersModuleWhenIdentityProvider assertThat(authenticationJson.get(AUTH_MODULES).size()).isEqualTo(1); } + @Test + public void bindIdentityProviderServiceShouldRegisterListener() throws Exception { + final IdentityProviderService identityProviderService = mock(IdentityProviderService.class); + + authenticationService.bindIdentityProviderService(identityProviderService); + + verify(identityProviderService).registerIdentityProviderListener(authenticationService); + } + + @Test + public void unbindIdentityProviderServiceShouldUnregisterListenerAndStopInjectingProviders() throws Exception { + final IdentityProviderService identityProviderService = mock(IdentityProviderService.class); + final List openIdProviderConfigs = new ArrayList<>(); + openIdProviderConfigs.add(ProviderConfigMapper.toProviderConfig(googleIdentityProvider)); + when(identityProviderService.getIdentityProviders()).thenReturn(openIdProviderConfigs); + + authenticationService.bindIdentityProviderService(identityProviderService); + authenticationService.unbindIdentityProviderService(identityProviderService); + authenticationService.setConfig(authenticationJson); + authenticationService.amendAuthConfig(authenticationJson.get(AUTH_MODULES)); + + verify(identityProviderService).unregisterIdentityProviderListener(authenticationService); + // only the stand-alone OPENID_CONNECT module is left, no module was generated from the provider + assertThat(authenticationJson.get(AUTH_MODULES).size()).isEqualTo(1); + } + /** * Tests that the attribute that {@link JwtSessionModule#isLogoutRequest(MessageInfo)} expects is present in the * attributesContext. diff --git a/openidm-identity-provider/src/main/java/org/forgerock/openidm/idp/impl/IdentityProviderService.java b/openidm-identity-provider/src/main/java/org/forgerock/openidm/idp/impl/IdentityProviderService.java index 469a88da00..6be063eb4c 100644 --- a/openidm-identity-provider/src/main/java/org/forgerock/openidm/idp/impl/IdentityProviderService.java +++ b/openidm-identity-provider/src/main/java/org/forgerock/openidm/idp/impl/IdentityProviderService.java @@ -12,14 +12,16 @@ * information: "Portions copyright [year] [name of copyright owner]". * * Copyright 2016 ForgeRock AS. - * Portions Copyrighted 2024 3A Systems LLC. + * Portions Copyrighted 2024-2026 3A Systems LLC. */ package org.forgerock.openidm.idp.impl; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; import static org.forgerock.http.handler.HttpClientHandler.OPTION_LOADER; import static org.forgerock.json.JsonValue.field; @@ -143,24 +145,18 @@ private enum Action { availableProviders, getProfile } * The String param in Map is referring to the * type of auth the identity provider supports. */ + private final Map> identityProviders = new ConcurrentHashMap<>(); + @Reference( + name = "identityProviders", service = IdentityProviderConfig.class, cardinality = ReferenceCardinality.MULTIPLE, - policy = ReferencePolicy.DYNAMIC) - private final Map> identityProviders = new ConcurrentHashMap<>(); - + policy = ReferencePolicy.DYNAMIC, + unbind = "unbindIdentityProviderConfig") protected void bindIdentityProviderConfig(final IdentityProviderConfig config) throws IdentityProviderServiceException { - // for this to be true, we do not have any identityProviders of this type - if (!identityProviders.containsKey(config.getIdentityProviderConfig().getType())) { - // initialize new array list to store providers of this type - List providers = new ArrayList<>(); - providers.add(config); - identityProviders.put(config.getIdentityProviderConfig().getType(), providers); - } else { - // we currently have existing configs of this type, just add to it - identityProviders.get(config.getIdentityProviderConfig().getType()).add(config); - } + identityProviders.computeIfAbsent(config.getIdentityProviderConfig().getType(), + type -> new CopyOnWriteArrayList<>()).add(config); notifyListeners(); } @@ -201,11 +197,12 @@ public void deactivate(ComponentContext context) { */ public List getIdentityProviderByType(final String type) { final List providers = new ArrayList<>(); - if (identityProviders == null || identityProviders.size() == 0) { + if (identityProviders.isEmpty()) { logger.debug("No Identity Providers have been configured."); return providers; } - for (final IdentityProviderConfig config : identityProviders.get(type)) { + for (final IdentityProviderConfig config + : identityProviders.getOrDefault(type, Collections.emptyList())) { providers.add(config.getIdentityProviderConfig()); } return providers; diff --git a/openidm-identity-provider/src/test/java/org/forgerock/openidm/idp/impl/IdentityProviderServiceTest.java b/openidm-identity-provider/src/test/java/org/forgerock/openidm/idp/impl/IdentityProviderServiceTest.java index 28b988b9a5..6f6519fd3f 100644 --- a/openidm-identity-provider/src/test/java/org/forgerock/openidm/idp/impl/IdentityProviderServiceTest.java +++ b/openidm-identity-provider/src/test/java/org/forgerock/openidm/idp/impl/IdentityProviderServiceTest.java @@ -21,6 +21,8 @@ import static org.forgerock.json.resource.Requests.newReadRequest; import static org.forgerock.json.test.assertj.AssertJJsonValueAssert.assertThat; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import java.util.Map; @@ -96,4 +98,36 @@ public void testReadInstance() throws Exception { assertThat(google).doesNotContain("client_secret"); // it should be removed by readInstance assertThat(google.isEqualTo(expected)).isTrue(); } + + @Test + public void testGetIdentityProviderByType() throws Exception { + IdentityProviderConfig idpConfig = mock(IdentityProviderConfig.class); + when(idpConfig.getIdentityProviderConfig()).thenReturn(googleIdentityProvider); + + IdentityProviderService service = new IdentityProviderService(); + service.bindIdentityProviderConfig(idpConfig); + + assertThat(service.getIdentityProviderByType("OPENID_CONNECT")).containsExactly(googleIdentityProvider); + // a type with no bound provider yields an empty list rather than failing + assertThat(service.getIdentityProviderByType("OAUTH")).isEmpty(); + } + + @Test + public void testUnbindIdentityProviderConfig() throws Exception { + IdentityProviderConfig idpConfig = mock(IdentityProviderConfig.class); + when(idpConfig.getIdentityProviderConfig()).thenReturn(googleIdentityProvider); + IdentityProviderListener listener = mock(IdentityProviderListener.class); + when(listener.getListenerName()).thenReturn("listener"); + + IdentityProviderService service = new IdentityProviderService(); + service.registerIdentityProviderListener(listener); + service.bindIdentityProviderConfig(idpConfig); + assertThat(service.getIdentityProvider("google")).isSameAs(googleIdentityProvider); + + service.unbindIdentityProviderConfig(idpConfig); + + assertThat(service.getIdentityProviders()).isEmpty(); + assertThat(service.getIdentityProvider("google")).isNull(); + verify(listener, times(2)).identityProviderConfigChanged(); + } } \ No newline at end of file From 6dac993645ce2e166e3a219fbe0ffcdb6ef29afd Mon Sep 17 00:00:00 2001 From: Valera V Harseko Date: Tue, 29 Sep 2026 11:39:26 +0300 Subject: [PATCH 2/2] [#230] Restore the identity provider catalogue and fix the social provider docs conf/identityProviders.json, the catalogue of supported social providers, was dropped from the distribution in 95c573c78, so IdentityProviderService (ConfigurationPolicy.REQUIRE) never started and Configure > Social ID Providers offered nothing. Bring it back with current endpoints: Google's token and userinfo endpoints from its discovery document, Facebook's unversioned Graph endpoints without the discontinued locale field, and LinkedIn's OpenID Connect userinfo in place of the retired v1 API. The Integrator's Guide placed provider configuration in identityProviders.json. A provider is configured in its own identityProvider-.json, which the Admin UI writes, SOCIAL_PROVIDERS reads and the configuration service encrypts client_secret in; identityProviders.json is only the catalogue. Fixes #230 --- .../appendix-auth-modules.adoc | 4 +- .../asciidoc/integrators-guide/chap-auth.adoc | 43 +++++- .../resources/conf/identityProviders.json | 142 ++++++++++++++++++ 3 files changed, 184 insertions(+), 5 deletions(-) create mode 100644 openidm-zip/src/main/resources/conf/identityProviders.json diff --git a/openidm-doc/src/main/asciidoc/integrators-guide/appendix-auth-modules.adoc b/openidm-doc/src/main/asciidoc/integrators-guide/appendix-auth-modules.adoc index cccac0dd8a..b86d9e6fcb 100644 --- a/openidm-doc/src/main/asciidoc/integrators-guide/appendix-auth-modules.adoc +++ b/openidm-doc/src/main/asciidoc/integrators-guide/appendix-auth-modules.adoc @@ -294,11 +294,11 @@ In general, if you add a custom property, the Admin UI writes changes to the `au [#social-providers-module-details] === SOCIAL_PROVIDERS Module Configuration Options -The `SOCIAL_PROVIDERS` module is a meta-module (template) that dynamically generates `OPENID_CONNECT` and `OAUTH` authentication modules at startup for supported providers registered in the `IdentityProviderService`. The identity provider configurations themselves (client IDs, client secrets, authorization endpoints, etc.) are defined in `conf/identityProviders.json`, not inside the `SOCIAL_PROVIDERS` module entry. +The `SOCIAL_PROVIDERS` module is a meta-module (template) that dynamically generates `OPENID_CONNECT` and `OAUTH` authentication modules at startup for supported providers registered in the `IdentityProviderService`. The identity provider configurations themselves (client IDs, client secrets, authorization endpoints, etc.) are defined in one `conf/identityProvider-.json` file per provider, not inside the `SOCIAL_PROVIDERS` module entry. `conf/identityProviders.json` only holds the catalog of providers that the Admin UI offers; see xref:chap-auth.adoc#social-providers-module["SOCIAL_PROVIDERS"]. [NOTE] ====== -The `SOCIAL_PROVIDERS` entry is removed from the active authentication module list at startup — it is never initialized as an authenticator itself. Each `OPENID_CONNECT` or `OAUTH` provider entry in `conf/identityProviders.json` results in exactly one generated authentication module. +The `SOCIAL_PROVIDERS` entry is removed from the active authentication module list at startup — it is never initialized as an authenticator itself. Each `conf/identityProvider-.json` file results in exactly one generated `OPENID_CONNECT` or `OAUTH` authentication module, which carries the `enabled` flag of that provider. ====== [#social-providers-module-prop-basic] diff --git a/openidm-doc/src/main/asciidoc/integrators-guide/chap-auth.adoc b/openidm-doc/src/main/asciidoc/integrators-guide/chap-auth.adoc index c67d7ec701..352801dfd4 100644 --- a/openidm-doc/src/main/asciidoc/integrators-guide/chap-auth.adoc +++ b/openidm-doc/src/main/asciidoc/integrators-guide/chap-auth.adoc @@ -481,17 +481,54 @@ IWA:: The IWA module enables users to authenticate by using Integrated Windows Authentication (IWA), rather than by providing a username and password. For information about configuring the IWA module with OpenIDM, see xref:#openidm-auth-kerberos["Configuring IWA Authentication"]. [[social-providers-module]]SOCIAL_PROVIDERS:: -The `SOCIAL_PROVIDERS` module is a __meta-module__ (template) that bridges the social identity provider configuration in `conf/identityProviders.json` and the OpenIDM authentication filter. It is not an authenticator itself — instead, at startup it scans all providers registered with the `IdentityProviderService`, removes itself from the active module list, and dynamically generates the corresponding `OPENID_CONNECT` or `OAUTH` authentication modules: +The `SOCIAL_PROVIDERS` module is a __meta-module__ (template) that bridges the social identity providers configured in `conf/identityProvider-.json` files and the OpenIDM authentication filter. It is not an authenticator itself — instead, at startup it scans all providers registered with the `IdentityProviderService`, removes itself from the active module list, and dynamically generates the corresponding `OPENID_CONNECT` or `OAUTH` authentication modules: + * For each provider of type `OPENID_CONNECT`, an `OPENID_CONNECT` auth module is generated with `openIdConnectHeader: "authToken"`. * For each provider of type `OAUTH`, an `OAUTH` auth module is generated with `authTokenHeader: "authToken"` and `authResolverHeader: "provider"`. + -The generated modules inherit the `augmentSecurityContext`, `propertyMapping`, and `defaultUserRoles` values from the `SOCIAL_PROVIDERS` template entry. +The generated modules inherit the `augmentSecurityContext`, `propertyMapping`, and `defaultUserRoles` values from the `SOCIAL_PROVIDERS` template entry. Each generated module also carries the `enabled` flag of its provider, so a provider with `"enabled" : false` produces no active module. + -Providers are configured separately in `conf/identityProviders.json`, or via the Admin UI under *Configure > Social ID Providers*. The `SOCIAL_PROVIDERS` module acts as a single configuration point so that operators do not need to add individual `OPENID_CONNECT` or `OAUTH` entries to `authentication.json` for every social provider. +Each provider is configured in a file of its own, `conf/identityProvider-.json`, for example `conf/identityProvider-google.json`. The provider name is taken from the file name. You can write the file by hand, or enable the provider in the Admin UI under *Configure > Social ID Providers*, which writes the same file. OpenIDM stores the `client_secret` of these files encrypted. The `SOCIAL_PROVIDERS` module acts as a single configuration point so that operators do not need to add individual `OPENID_CONNECT` or `OAUTH` entries to `authentication.json` for every social provider. + ++ +A sample `conf/identityProvider-google.json` is as follows: ++ +[source, json] +---- +{ + "name" : "google", + "type" : "OPENID_CONNECT", + "enabled" : true, + "authorization_endpoint" : "https://accounts.google.com/o/oauth2/v2/auth", + "token_endpoint" : "https://oauth2.googleapis.com/token", + "userinfo_endpoint" : "https://openidconnect.googleapis.com/v1/userinfo", + "well-known" : "https://accounts.google.com/.well-known/openid-configuration", + "client_id" : "your-client-id", + "client_secret" : "your-client-secret", + "scope" : [ + "openid", + "profile", + "email" + ], + "authenticationId" : "sub", + "propertyMap" : [ + { + "source" : "sub", + "target" : "id" + }, + { + "source" : "email", + "target" : "username" + } + ] +} +---- + ++ +`conf/identityProviders.json` is the configuration of the identity provider service itself. Its `providers` list is the catalog of supported providers (Google, Facebook, and LinkedIn by default): the templates that the Admin UI offers under *Configure > Social ID Providers*, and that `POST /openidm/identityProviders?_action=availableProviders` returns. An entry in the catalog is not an active provider, and client credentials placed there are neither used nor encrypted. Keep the file in place: without it the identity provider service does not start, and no social provider can be configured. + [NOTE] diff --git a/openidm-zip/src/main/resources/conf/identityProviders.json b/openidm-zip/src/main/resources/conf/identityProviders.json new file mode 100644 index 0000000000..d961f130c6 --- /dev/null +++ b/openidm-zip/src/main/resources/conf/identityProviders.json @@ -0,0 +1,142 @@ +{ + "providers" : [ + { + "name" : "google", + "type" : "OPENID_CONNECT", + "icon" : "", + "authorization_endpoint" : "https://accounts.google.com/o/oauth2/v2/auth", + "token_endpoint" : "https://oauth2.googleapis.com/token", + "userinfo_endpoint" : "https://openidconnect.googleapis.com/v1/userinfo", + "well-known" : "https://accounts.google.com/.well-known/openid-configuration", + "client_id" : "", + "client_secret" : "", + "scope" : [ + "openid", + "profile", + "email" + ], + "authenticationId" : "sub", + "propertyMap" : [ + { + "source" : "sub", + "target" : "id" + }, + { + "source" : "name", + "target" : "displayName" + }, + { + "source" : "given_name", + "target" : "givenName" + }, + { + "source" : "family_name", + "target" : "familyName" + }, + { + "source" : "picture", + "target" : "photoUrl" + }, + { + "source" : "email", + "target" : "email" + }, + { + "source" : "email", + "target" : "username" + } + ] + }, + { + "name" : "facebook", + "type" : "OAUTH", + "icon" : "", + "authorization_endpoint" : "https://www.facebook.com/dialog/oauth", + "token_endpoint" : "https://graph.facebook.com/oauth/access_token", + "userinfo_endpoint" : "https://graph.facebook.com/me?fields=id,name,email,first_name,last_name", + "client_id" : "", + "client_secret" : "", + "scope" : [ + "public_profile", + "email" + ], + "authenticationId" : "id", + "propertyMap" : [ + { + "source" : "id", + "target" : "id" + }, + { + "source" : "name", + "target" : "displayName" + }, + { + "source" : "first_name", + "target" : "givenName" + }, + { + "source" : "last_name", + "target" : "familyName" + }, + { + "source" : "email", + "target" : "email" + }, + { + "source" : "email", + "target" : "username" + } + ] + }, + { + "name" : "linkedIn", + "type" : "OAUTH", + "icon" : "", + "authorization_endpoint" : "https://www.linkedin.com/oauth/v2/authorization", + "token_endpoint" : "https://www.linkedin.com/oauth/v2/accessToken", + "userinfo_endpoint" : "https://api.linkedin.com/v2/userinfo", + "client_id" : "", + "client_secret" : "", + "scope" : [ + "openid", + "profile", + "email" + ], + "authenticationId" : "sub", + "propertyMap" : [ + { + "source" : "sub", + "target" : "id" + }, + { + "source" : "name", + "target" : "displayName" + }, + { + "source" : "given_name", + "target" : "givenName" + }, + { + "source" : "family_name", + "target" : "familyName" + }, + { + "source" : "picture", + "target" : "photoUrl" + }, + { + "source" : "email", + "target" : "email" + }, + { + "source" : "email", + "target" : "username" + }, + { + "source" : "locale", + "target" : "locale" + } + ] + } + ] +}