Skip to content
Open
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 @@ -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;
Expand Down Expand Up @@ -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 */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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<ProviderConfig> 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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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-<name>.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-<name>.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]
Expand Down
43 changes: 40 additions & 3 deletions openidm-doc/src/main/asciidoc/integrators-guide/chap-auth.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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-<name>.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-<name>.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]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<String, List<IdentityProviderConfig>> identityProviders = new ConcurrentHashMap<>();

@Reference(
name = "identityProviders",
service = IdentityProviderConfig.class,
cardinality = ReferenceCardinality.MULTIPLE,
policy = ReferencePolicy.DYNAMIC)
private final Map<String, List<IdentityProviderConfig>> 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<IdentityProviderConfig> 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();
}

Expand Down Expand Up @@ -201,11 +197,12 @@ public void deactivate(ComponentContext context) {
*/
public List<ProviderConfig> getIdentityProviderByType(final String type) {
final List<ProviderConfig> 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.<IdentityProviderConfig>emptyList())) {
providers.add(config.getIdentityProviderConfig());
}
return providers;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
}
}
Loading
Loading