Implementing one of the most popular authorization protocols in the latest Adobe CMS
According to its website, OAuth 2.0 is the industry-standard protocol for authorization. It’s supported by big vendors like Google and Facebook and is among the most popular auth protocols in the Web. In a nutshell, OAuth allows access to protected resources on a resource server, without providing credentials, by means of issuing “access token” to third parties.
AEM provides both OAuth server and client functions out of the box, and in this article I will focus on AEM acting as OAuth client.
AEM as OAuth client
AEM provides a back-end OAuth client based on the Scribe open source library. This client is integrated with authentication mechanisms in CMS, resulting in a seamless login experience for AEM users. The supported flow (simplified) looks like this:
- User is provided a login link in the following format: https://localhost/content/project/us/en/ page.html/j_security_check?configid=oauth. “oauth” is the name of the “provider” configured in the system. That is covered later in this article. Page.html should be the currently viewed page. This will ensure the user gets back to where the login process started.
- /j_security_check is handled internally in AEM, which takes OAuth configuration based on configid parameter. AEM constructs a redirect link leading to OAuth server containing all information necessary, including clientId, scope, redirectUrl, etc.
- Browser redirects there.
- User is presented with an auth credentials page from OAuth server. This is an external system.
- After providing correct credentials, the user is redirected back to AEM using the provided redirectUrl. This URL contains code parameter used later in auth flow. AEM supports only the specific redirect URL format: /.../callback/j_security_check.
- AEM executes an HTTP request passing code, clientid and clientSecret to OAuth server, obtaining the access token.
- AEM executes another HTTP request to OAuth server, passing access token to the profile endpoint, obtaining user profile information.
- AEM updates (or creates, depending on configuration) user profile in /home/users. The optional access code is stored there, too, for future use.
- User is redirected back to AEM page.
OSGi Configuration
OAuth Authentication Handler
PID: com.adobe.granite.auth.oauth.impl. OAuthAuthenticationHandler
This has only one setting, path, and configures repository root path for URLs handled by OAuth. Effectively it tells AEM which paths should support /j_security_check URLs. / is safe, enabling OAuth handling for all repository paths.
OAuth Application and Provider
PID: com.adobe.granite.auth.oauth.provider
This is the main OAuth configuration. It’s a good idea to configure it using configuration factories.
- oauth.config.id - Configuration identifier. It's used as part of the login URL, so it's good for it to be descriptive or at least written in easy-to-understand language.
- oauth.client.id - Client ID is passed to OAuth server.
- oauth.client.secret - Client Secret is passed to OAuth server. It can be encrypted with AEM Crypto.
- oauth.scope - OAuth scopes is passed to OAuth server.
- oauth.config.provider.id - Provider ID. Provider is a custom OSGi service implementing the com.adobe.granite.auth.oauth.Provider interface. Implementation of this and related classes is covered later in this article.
- oauth.create.users - Ticking this causes AEM to create a user on first login. If it is not enabled, authentication will fail for users not existing in AEM.
- oauth.callBackUrl - URL where user is redirected back from OAuth server after successful login. This can be left empty, AEM will generate and use a default one. Keep in mind this usually must be set in OAuth server too.
- oauth.redirect.request.params - This tells AEM to encode some initial request parameters in OAuth state parameter. Enabling this causes final redirect to point to a page where the user initiated the login process.
- Other parameters are less useful and self-explanatory.
Provider Config Manager
PID: com.adobe.granite.auth.oauth.impl.helper. ProviderConfigManager
This OSGi service is responsible for OAuth timeouts and expirations. Defaults are unreasonably low, leading to login issues when the user spends too much time on the login screen, so it’s advised to tune these settings.
Provider and Related Services Implementation
Provider implementation is a facade class dealing with OAuth. It does different things at different points and is poorly documented. Fortunately, it relies on classes from Scribe library, which has decent documentation and Adobe provides a sample implementation: aem-communities-oauth-sample/LinkedinProviderImpl.java at master · Adobe-Marketing-Cloud/aem-communities-oauth-sample
I’ll break it down phase by phase and cover most methods below.
Initialization and General Configurations
- ProviderType getType() - For most scenarios, it is safe to return ProviderType.OAUTH2 in this method.
- API getAPI() - Scribe’s API implementation. Scribe provides implementations for many popular OAuth vendors. Sample code uses LinkedInAPI to deal with LinkedIn OAuth. If your vendor is not on the list, you’ll need to implement the API yourself.
- getName() - Returns provider name.
- getUserFolderPath() - Returns path to folder where OAuth users are stored (for example, /home/users/oauth).
- getAccessTokenPropertyPath() - Property path for access token storage.
User Profile-Related Activities
These methods are called after retrieving the access code from the OAuth server (not necessarily in listed order):
- getDetailsURL() - Returns URL to OAuth endpoint (usually called Profile Endpoint or User Info Endpoint).
- parseProfileDataResponse() - Method executed to parse the response from the endpoint above.
- getExtendedDetailsURLs() methods - Allow retrieval of additional scope properties.
- mapUserId() - Encodes AEM user ID based on properties retrieved from OAuth.
- mapProperties() - Maps and filters properties from OAuth to AEM profile properties.
- onUserCreate(User user) and onUserUpdate(User user) - Methods called when User is created/updated. This allows setting custom properties, assign to groups, etc.
- getUserIdProperty() - AEM profile property where OAuth user ID is stored. This is used to map OAuth users with existing AEM users.
Some of the methods above are commented in Adobe’s sample code. In case of problems, I recommend deploying sample project on local instance, configure LinkedIn, and debug working project.
Custom API
In case OAuth vendor API is missing, you will have to implement a custom API and (most probably) OAuthService. Scribe offers handy base classes providing some sane defaults.
DefaultApi20
You have to implement two abstract methods: getAccessTokenEndpoint() and getAccessTokenVerb(). Correct values can be obtained from vendor documentation.
OAuth20ServiceImpl
This is the default OAuthService implementation for OAuth 2.0 and unfortunately the only one provided. The only task it does is requesting and decoding the access token. You have to consult vendor documentation for details.
Limitations
- OAuth client intergration in AEM is very basic and doesn’t offer much more than authentication.
- Once OAuth flow completes, AEM “forgets” about OAuth server and only deals with its own user session.
- AEM doesn’t store the refresh token at all. This complicates using access token and OAuth for anything other than initial authentication.
- Users need to exist (or be created on login) in AEM.
- Provider facade interface is poorly designed and hard to understand and implement. Due to its “one class doing everything” nature it offers little extension possibilities.
External Resources
- OAuth 2.0 official page - OAuth 2.0 — OAuth
- OAuth Wikipedia Article - OAuth
- Scribe library homepage - GitHub - scribejava/scribejava: Simple OAuth library for Java
- AEM OAuth Sample project - GitHub - Adobe-Marketing-Cloud/aem-communities-oauth-sample
- DefaultApi20 documentation - DefaultApi20 (The Adobe Experience Manager SDK 2021.11.6058.20211123T163652Z-211100)