Skip to content

Commit

Permalink
Using Oauth2 via certificate based authentication and JWT (#1485)
Browse files Browse the repository at this point in the history
* Using Oauth2 via certificate based authentication and JWT

* Making io.jsonwebtoken optional, since it part of local cloud ready SDK
  • Loading branch information
rismehta authored Nov 27, 2024
1 parent 75e25e7 commit 93cf3b0
Show file tree
Hide file tree
Showing 3 changed files with 110 additions and 26 deletions.
14 changes: 14 additions & 0 deletions it/core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@
<Import-Package>
javax.annotation;version=0.0.0,
com.adobe.cq.forms.core.components.models.form;version="[1.0.0,10.0.0)",
io.jsonwebtoken;resolution:=optional;version="[0.0.0,1.0.0)",
*
</Import-Package>
</instructions>
Expand Down Expand Up @@ -162,6 +163,19 @@ Import-Package: javax.annotation;version=0.0.0,*
<artifactId>core-forms-components-af-core</artifactId>
<version>3.0.70</version>
</dependency>
<!-- Json web token dependencies for oauth2 flow -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.2</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.2</version>
<scope>provided</scope>
</dependency>
</dependencies>

<!-- ====================================================================== -->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -170,14 +170,28 @@ public ReplicationResult deliver(TransportContext transportContext, ReplicationT
// todo: publish this form model json to the external system
LOG.info("[HeadlessTransportHandler] Form Model JSON: {}", formModelJson);
/**
PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager();
connectionManager.setDefaultMaxPerRoute(100);
connectionManager.setMaxTotal(100);
CloseableHttpClient httpClient = HttpClients.custom()
.setConnectionManager(connectionManager)
.setDefaultRequestConfig(RequestConfig.custom()
.setConnectTimeout(30000)
.setSocketTimeout(30000)
.setConnectionRequestTimeout(30000)
.build())
.build();
OAuth2Client oauth2Client = new OAuth2Client(
"https://example.com/oauth2/token",
"your_client_id",
"your_client_secret",
"https://example.com/api/publish",
"your_private_key",
"your_certificate_thumbprint",
"your_resource_uri",
httpClient
);
oauth2Client.publishOrDeleteFormModelJson(formModelJson, requestSupplier);
oauth2Client.publishOrDeleteFormModelJson(formModelJson, "https://example.com/api/publish", HttpPost::new);
**/
} else {
LOG.info("[HeadlessTransportHandler] No adaptive form container found for resource {}. Skipping", resource.getPath());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/
package com.adobe.cq.forms.core.components.it.service;

import java.io.IOException;
import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Base64;
import java.util.Date;
import java.util.concurrent.locks.ReentrantLock;

import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpRequestBase;
Expand All @@ -27,34 +35,48 @@
import javax.json.Json;
import javax.json.JsonObject;
import javax.json.JsonReader;
import java.io.IOException;
import java.io.StringReader;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Function;


// these bundles are not present on local cloud ready sdk
// to make this work, you have to download/install 0.11.2 from here, https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-api/0.11.2
// making the import optional
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;

/**
* Uses the OAuth2 Client Credentials flow with a signed JWT (client assertion) for certificate-based authentication.
* The token request payload includes grant_type, client_id, client_assertion_type, client_assertion, and resource.
* The signed JWT includes claims such as iss, sub, aud, jti, iat, and exp.
* The JWT is signed using a private key and includes a certificate thumbprint in the header.
*
* Uses a signed JWT (client assertion) with a private key and certificate thumbprint.
* Provides enhanced security by using certificate-based authentication and a signed JWT.
*/
public class OAuth2Client {
private static final Logger LOG = LoggerFactory.getLogger(OAuth2Client.class);

private final String tokenEndpoint;
private final String clientId;
private final String clientSecret;
private final String apiEndpoint;
private final String privateKey;
private final String certificateThumbprint;
private final String resource;
private final CloseableHttpClient httpClient;

private String accessToken;
private long tokenExpirationTime;
private final ReentrantLock lock = new ReentrantLock();

public OAuth2Client(String tokenEndpoint, String clientId, String clientSecret, String apiEndpoint, CloseableHttpClient httpClient) {
public OAuth2Client(String tokenEndpoint, String clientId, String privateKey, String certificateThumbprint, String resource, CloseableHttpClient httpClient) {
this.tokenEndpoint = tokenEndpoint;
this.clientId = clientId;
this.clientSecret = clientSecret;
this.apiEndpoint = apiEndpoint;
this.privateKey = privateKey;
this.certificateThumbprint = certificateThumbprint;
this.resource = resource;
this.httpClient = httpClient;
}

public void publishOrDeleteFormModelJson(String formModelJson, Function<String, HttpRequestBase> requestSupplier) throws IOException {
public void publishOrDeleteFormModelJson(String formModelJson, String apiEndpoint, Function<String, HttpRequestBase> requestSupplier) throws IOException {
String token = getValidToken();
HttpRequestBase request = requestSupplier.apply(apiEndpoint);
request.setHeader("Authorization", "Bearer " + token);
Expand Down Expand Up @@ -94,7 +116,12 @@ private String getValidToken() throws IOException {
private String fetchOAuth2Token() throws IOException {
HttpPost post = new HttpPost(tokenEndpoint);
post.setHeader("Content-Type", "application/x-www-form-urlencoded");
post.setEntity(new StringEntity("grant_type=client_credentials&client_id=" + clientId + "&client_secret=" + clientSecret));

String clientAssertion = generateClientAssertion();

String payload = "grant_type=client_credentials&client_id=" + clientId + "&client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer&client_assertion=" + clientAssertion + "&resource=" + resource;

post.setEntity(new StringEntity(payload));

try (CloseableHttpResponse response = httpClient.execute(post)) {
if (response.getStatusLine().getStatusCode() == 200) {
Expand All @@ -107,18 +134,7 @@ private String fetchOAuth2Token() throws IOException {
}

private String refreshOAuth2Token() throws IOException {
HttpPost post = new HttpPost(tokenEndpoint);
post.setHeader("Content-Type", "application/x-www-form-urlencoded");
post.setEntity(new StringEntity("grant_type=refresh_token&refresh_token=your_refresh_token&client_id=" + clientId + "&client_secret=" + clientSecret));

try (CloseableHttpResponse response = httpClient.execute(post)) {
if (response.getStatusLine().getStatusCode() == 200) {
String responseBody = EntityUtils.toString(response.getEntity());
return parseToken(responseBody);
} else {
throw new NotOk(response.getStatusLine().getStatusCode());
}
}
return fetchOAuth2Token(); // Assuming the same flow for refresh token
}

private String parseToken(String responseBody) {
Expand All @@ -130,9 +146,49 @@ private String parseToken(String responseBody) {
}
}

private String generateClientAssertion() {
long nowMillis = System.currentTimeMillis();
Date now = new Date(nowMillis);

// Create the JWT claims
JsonObject claims = Json.createObjectBuilder()
.add("iss", clientId)
.add("sub", clientId)
.add("aud", tokenEndpoint)
.add("jti", java.util.UUID.randomUUID().toString())
.add("iat", nowMillis / 1000)
.add("exp", (nowMillis / 1000) + 300) // 5 minutes expiration
.build();

// Create the JWT header
JsonObject header = Json.createObjectBuilder()
.add("alg", "RS256")
.add("x5t", certificateThumbprint)
.build();

// Sign the JWT
return Jwts.builder()
.setHeaderParam("x5t", certificateThumbprint)
.setClaims(claims)
.setHeaderParam("typ", "JWT")
.signWith(SignatureAlgorithm.RS256, getPrivateKey())
.compact();
}

private PrivateKey getPrivateKey() {
try {
byte[] keyBytes = Base64.getDecoder().decode(privateKey);
PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(keyBytes);
KeyFactory kf = KeyFactory.getInstance("RSA");
return kf.generatePrivate(spec);
} catch (Exception e) {
throw new RuntimeException("Failed to load private key", e);
}
}

private static class NotOk extends IOException {
NotOk(int status) {
super("status code = " + status);
}
}
}
}

0 comments on commit 93cf3b0

Please sign in to comment.