Skip to content

Commit

Permalink
Dynamic Media with Open API: Optional IMS Authentication for metadata…
Browse files Browse the repository at this point in the history
… requests to get full asset metadata
  • Loading branch information
stefanseifert committed Nov 26, 2024
1 parent 3e461d7 commit 0d3aca8
Show file tree
Hide file tree
Showing 3 changed files with 224 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
* #%L
* wcm.io
* %%
* Copyright (C) 2024 wcm.io
* %%
* 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.
* #L%
*/
package io.wcm.handler.mediasource.ngdm.impl.metadata;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;

import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;

/**
* Used for Jackson Object mapping of JSON response from IMS Token v3 API.
*/
@SuppressWarnings({ "checkstyle:VisibilityModifierCheck", "java:S1104" })
@SuppressFBWarnings("UUF_UNUSED_PUBLIC_OR_PROTECTED_FIELD")
@JsonIgnoreProperties(ignoreUnknown = true)
final class AccessTokenResponse {

@JsonProperty("access_token")
public String accessToken;

@JsonProperty("token_type")
public String tokenType;

@JsonProperty("expires_in")
public long expiresInSec;

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
/*
* #%L
* wcm.io
* %%
* Copyright (C) 2024 wcm.io
* %%
* 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.
* #L%
*/
package io.wcm.handler.mediasource.ngdm.impl.metadata;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;

import org.apache.http.HttpStatus;
import org.apache.http.NameValuePair;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;
import org.checkerframework.checker.index.qual.NonNegative;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.fasterxml.jackson.databind.json.JsonMapper;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.Expiry;

/**
* Manages IMS access tokens with expiration handling.
*/
class ImsAccessTokenCache {

private static final long EXPERIATION_BUFFER_SEC = 5;

// cache IMS access tokens until they expire
private final Cache<String, AccessTokenResponse> tokenCache = Caffeine.newBuilder()
.expireAfter(new Expiry<String, AccessTokenResponse>() {
@Override
public long expireAfterCreate(String key, AccessTokenResponse value, long currentTime) {
// substract a few secs from expiration time to be on the safe side
return TimeUnit.SECONDS.toNanos(value.expiresInSec - EXPERIATION_BUFFER_SEC);
}
@Override
public long expireAfterUpdate(String key, AccessTokenResponse value, long currentTime, @NonNegative long currentDuration) {
// not used
return Long.MAX_VALUE;
}
@Override
public long expireAfterRead(String key, AccessTokenResponse value, long currentTime, @NonNegative long currentDuration) {
// not used
return Long.MAX_VALUE;
}
})
.build();

private static final JsonMapper OBJECT_MAPPER = new JsonMapper();
private static final Logger log = LoggerFactory.getLogger(ImsAccessTokenCache.class);

private final CloseableHttpClient httpClient;
private final String imsTokenApiUrl;

ImsAccessTokenCache(@NotNull CloseableHttpClient httpClient, @NotNull String imsTokenApiUrl) {
this.httpClient = httpClient;
this.imsTokenApiUrl = imsTokenApiUrl;
}

/**
* Get IMS OAuth access token
* @param clientId Client ID
* @param clientSecret Client Secret
* @param scope Scope
* @return Access token or null if access token could not be obtained
*/
public @Nullable String getAccessToken(@NotNull String clientId, @NotNull String clientSecret, @NotNull String scope) {
String key = clientId + "::" + scope;
return tokenCache.get(key, k -> createAccessToken(clientId, clientSecret, scope)).accessToken;
}

private @Nullable AccessTokenResponse createAccessToken(@NotNull String clientId, @NotNull String clientSecret, @NotNull String scope) {
List<NameValuePair> formData = new ArrayList<>();
formData.add(new BasicNameValuePair("grant_type", "client_credentials"));
formData.add(new BasicNameValuePair("client_id", clientId));
formData.add(new BasicNameValuePair("client_secret", clientSecret));
formData.add(new BasicNameValuePair("scope", scope));

HttpPost httpPost = new HttpPost(imsTokenApiUrl);
httpPost.setEntity(new UrlEncodedFormEntity(formData, StandardCharsets.UTF_8));

try (CloseableHttpResponse response = httpClient.execute(httpPost)) {
return processResponse(response);
}
catch (IOException ex) {
log.warn("Unable to obtain access token from URL {}", imsTokenApiUrl, ex);
return null;
}
}

@SuppressWarnings("null")
private @Nullable AccessTokenResponse processResponse(@NotNull CloseableHttpResponse response) throws IOException {
switch (response.getStatusLine().getStatusCode()) {
case HttpStatus.SC_OK:
String jsonResponse = EntityUtils.toString(response.getEntity());
AccessTokenResponse accessTokenResponse = OBJECT_MAPPER.readValue(jsonResponse, AccessTokenResponse.class);
log.trace("HTTP response for access token reqeust from {} returned a response, expires in {} sec",
imsTokenApiUrl, accessTokenResponse.expiresInSec);
return accessTokenResponse;
case HttpStatus.SC_NOT_FOUND:
log.trace("HTTP response for access token request from {} returns HTTP 404.", imsTokenApiUrl);
break;
default:
log.warn("Unexpected HTTP response for access token request from {}: {}", imsTokenApiUrl, response.getStatusLine());
break;
}
return null;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,26 @@ public class NextGenDynamicMediaMetadataServiceImpl implements NextGenDynamicMed
description = "Proxy port")
int proxyPort();

@AttributeDefinition(
name = "IMS Token API URL",
description = "API to obtain IMS access token for obtaining full metadata.")
String imsTokenApiUrl() default "https://ims-na1.adobelogin.com/ims/token/v3";

@AttributeDefinition(
name = "IMS OAuth Client ID",
description = "Optional: If you want to fetch the full metadata for assets, provide the IMS OAuth Client ID.")
String authenticationClientId();

@AttributeDefinition(
name = "IMS OAuth Client Secret",
description = "Optional: If you want to fetch the full metadata for assets, provide the IMS OAuth Client Secret.")
String authenticationClientSecret();

@AttributeDefinition(
name = "IMS OAuth Scope",
description = "OAuth Scope to use for obtaining IMS access token.")
String authenticationScope() default "openid,AdobeID,read_organizations,additional_info.projectedProductContext,read_pc.dma_aem_ams";

}

@Reference
Expand All @@ -107,13 +127,27 @@ public class NextGenDynamicMediaMetadataServiceImpl implements NextGenDynamicMed
private boolean enabled;
private CloseableHttpClient httpClient;

private ImsAccessTokenCache imsAccessTokenCache;
private String authenticationClientId;
private String authenticationClientSecret;
private String authenticationScope;

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

@Activate
private void activate(Config config) {
this.enabled = config.enabled();
if (enabled) {
httpClient = createHttpClient(config);

// if configured, enable IMS access token fetching
String imsTokenApiUrl = config.imsTokenApiUrl();
authenticationClientId = config.authenticationClientId();
authenticationClientSecret = config.authenticationClientSecret();
authenticationScope = config.authenticationScope();
if (StringUtils.isNoneBlank(imsTokenApiUrl, authenticationClientId, authenticationClientSecret, authenticationScope)) {
imsAccessTokenCache = new ImsAccessTokenCache(httpClient, config.imsTokenApiUrl());
}
}
}

Expand Down Expand Up @@ -147,6 +181,7 @@ private static Collection<Header> convertHeaders(String[] headers) {
private void deactivate() throws IOException {
if (httpClient != null) {
httpClient.close();
imsAccessTokenCache = null;
}
}

Expand All @@ -171,6 +206,15 @@ public boolean isEnabled() {
}

HttpGet httpGet = new HttpGet(metadataUrl);

// add IMS access if configured
if (imsAccessTokenCache != null) {
String accessToken = imsAccessTokenCache.getAccessToken(authenticationClientId, authenticationClientSecret, authenticationScope);
if (accessToken != null) {
httpGet.addHeader("Authorization", "Bearer " + accessToken);
}
}

try (CloseableHttpResponse response = httpClient.execute(httpGet)) {
return processResponse(response, metadataUrl);
}
Expand Down

0 comments on commit 0d3aca8

Please sign in to comment.