ImsAccessTokenCache.java
/*
* #%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.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, long currentDuration) {
// not used
return Long.MAX_VALUE;
}
@Override
public long expireAfterRead(String key, AccessTokenResponse value, long currentTime, 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;
AccessTokenResponse accessTokenResponse = tokenCache.get(key, k -> createAccessToken(clientId, clientSecret, scope));
if (accessTokenResponse != null) {
return accessTokenResponse.accessToken;
}
return null;
}
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 {
if (response.getStatusLine().getStatusCode() == 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;
}
else {
log.warn("Unexpected HTTP response for access token request from {}: {}", imsTokenApiUrl, response.getStatusLine());
return null;
}
}
}