Skip to content

Commit 02191b5

Browse files
committed
Added Hmac Encrytion Layer
1 parent a70e8ec commit 02191b5

File tree

6 files changed

+181
-118
lines changed

6 files changed

+181
-118
lines changed

Dockerfile

Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,16 @@
1-
FROM gradle:6.9.2-jdk8
1+
FROM tomcat:9-jdk11
22

3-
WORKDIR /home/gradle
4-
# Copy your project files
5-
COPY . .
3+
# Remove default Tomcat apps
4+
RUN rm -rf /usr/local/tomcat/webapps/*
65

7-
# Ensure the Gradle wrapper is executable
8-
RUN chmod +x ./gradlew
6+
# Change Tomcat's default port from 8080 to 3000
7+
RUN sed -i 's/port="8080"/port="3000"/' /usr/local/tomcat/conf/server.xml
98

10-
# Expose both ports for your MCD test
11-
EXPOSE 3000
12-
EXPOSE 8080
13-
EXPOSE 5005
9+
# Copy the locally-built WAR into Tomcat
10+
COPY build/libs/mvc-auth-commons-*.war /usr/local/tomcat/webapps/ROOT.war
1411

15-
# Use --no-daemon to keep the container process alive
16-
# We use the wrapper (./gradlew) to ensure consistency
17-
#CMD ["./gradlew", "appRun", "--no-daemon", "-Pgretty.managed=false"]
18-
ENV GRADLE_OPTS="-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005"
19-
CMD ["gradle", "appRun", "--no-daemon"]
12+
EXPOSE 3000 5005
13+
14+
ENV JAVA_OPTS="-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005"
15+
16+
CMD ["catalina.sh", "run"]

src/main/java/com/auth0/AuthorizeUrl.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ public class AuthorizeUrl {
2929
private String state;
3030
private final AuthAPI authAPI;
3131
private String cookiePath;
32+
private String originDomain;
33+
private String clientSecret;
3234

3335
private boolean used;
3436
private Map<String, String> params;
@@ -142,6 +144,16 @@ AuthorizeUrl withCookiePath(String cookiePath) {
142144
return this;
143145
}
144146

147+
/**
148+
* Sets the origin domain and client secret for HMAC-signed origin domain cookie storage.
149+
* Called internally by RequestProcessor for MCD support.
150+
*/
151+
AuthorizeUrl withOriginDomain(String originDomain, String clientSecret) {
152+
this.originDomain = originDomain;
153+
this.clientSecret = clientSecret;
154+
return this;
155+
}
156+
145157
/**
146158
* Sets the state value.
147159
*
@@ -248,6 +260,12 @@ private void storeTransient() {
248260

249261
TransientCookieStore.storeState(response, state, sameSiteValue, useLegacySameSiteCookie, setSecureCookie, cookiePath);
250262
TransientCookieStore.storeNonce(response, nonce, sameSiteValue, useLegacySameSiteCookie, setSecureCookie, cookiePath);
263+
264+
// Store HMAC-signed origin domain with the same SameSite value as state/nonce
265+
if (originDomain != null && clientSecret != null) {
266+
TransientCookieStore.storeSignedOriginDomain(response, originDomain,
267+
sameSiteValue, cookiePath, setSecureCookie, clientSecret);
268+
}
251269
}
252270

253271
// Also store in Session just in case developer uses deprecated

src/main/java/com/auth0/RequestProcessor.java

Lines changed: 24 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,12 @@
55
import com.auth0.exception.Auth0Exception;
66
import com.auth0.json.auth.TokenHolder;
77
import com.auth0.net.Telemetry;
8-
import com.fasterxml.jackson.core.type.TypeReference;
9-
import com.fasterxml.jackson.databind.ObjectMapper;
108
import com.google.common.annotations.VisibleForTesting;
119

1210
import javax.servlet.http.HttpServletRequest;
1311
import javax.servlet.http.HttpServletResponse;
1412
import java.util.Arrays;
1513
import java.util.List;
16-
import java.util.Map;
1714

1815
import static com.auth0.InvalidRequestException.*;
1916

@@ -183,7 +180,6 @@ AuthAPI createClientForDomain(String domain) {
183180
setupTelemetry(client);
184181
}
185182

186-
System.out.println("Created dynamic AuthAPI for domain: " + domain + " " + clientId);
187183
return client;
188184
}
189185

@@ -231,24 +227,12 @@ AuthorizeUrl buildAuthorizeUrl(HttpServletRequest request, HttpServletResponse r
231227
}
232228

233229
// null response means state and nonce will be stored in session, so legacy
234-
// cookie flag does not apply
230+
// cookie flag does not apply and origin domain cookie cannot be set
235231
if (response != null) {
236232
creator.withLegacySameSiteCookie(useLegacySameSiteCookie);
233+
creator.withOriginDomain(originDomain, clientSecret);
237234
}
238235

239-
boolean isSecure = request.isSecure();
240-
241-
TransientCookieStore.storeOriginData(
242-
response,
243-
originDomain,
244-
SameSite.LAX,
245-
constructIssuer(originDomain),
246-
cookiePath,
247-
isSecure);
248-
249-
TransientCookieStore.storeOriginData(response, originDomain, SameSite.LAX, constructIssuer(originDomain), cookiePath,
250-
isSecure);
251-
252236
return getAuthorizeUrl(nonce, creator);
253237
}
254238

@@ -269,19 +253,21 @@ Tokens process(HttpServletRequest request, HttpServletResponse response) throws
269253
assertNoError(request);
270254
assertValidState(request, response);
271255

272-
// Retrieve stored origin domain and issuer from the authorization flow
273-
String originDomain = TransientCookieStore.getOriginDomain(request, response);
274-
String originIssuer = TransientCookieStore.getOriginIssuer(request, response);
256+
// Extract origin_domain from the HMAC-signed transaction state cookie.
257+
// If the cookie was tampered with, getSignedOriginDomain returns null.
258+
String originDomain = null;
259+
if (response != null) {
260+
originDomain = TransientCookieStore.getSignedOriginDomain(request, response, clientSecret);
261+
}
275262

263+
// Fallback for session-based (deprecated) flow or if cookie was not set
276264
if (originDomain == null) {
277265
originDomain = domainProvider.getDomain(request);
278266
}
279267

280-
if (originIssuer == null) {
281-
originIssuer = constructIssuer(originDomain);
282-
}
268+
// Always derive the issuer from the verified domain — never from a cookie
269+
String originIssuer = constructIssuer(originDomain);
283270

284-
// Each request will create its own verification options with the correct issuer
285271
Tokens frontChannelTokens = getFrontChannelTokens(request, originDomain, originIssuer);
286272
List<String> responseTypeList = getResponseType();
287273

@@ -322,18 +308,24 @@ private Tokens getVerifiedTokens(HttpServletRequest request, HttpServletResponse
322308
Tokens codeExchangeTokens = null;
323309

324310
// Get nonce for this specific request
325-
String nonce = response != null
326-
? (TransientCookieStore.getNonce(request, response) != null
327-
? TransientCookieStore.getNonce(request, response)
328-
: RandomStorage.removeSessionNonce(request))
329-
: RandomStorage.removeSessionNonce(request);
311+
String nonce;
312+
if (response != null) {
313+
nonce = TransientCookieStore.getNonce(request, response);
314+
// Fallback to session if cookie was not set (deprecated API path)
315+
if (nonce == null) {
316+
nonce = RandomStorage.removeSessionNonce(request);
317+
}
318+
} else {
319+
nonce = RandomStorage.removeSessionNonce(request);
320+
}
330321

331322
IdTokenVerifier.Options requestVerifyOptions = createRequestVerifyOptions(originIssuer, nonce);
332323

333324
try {
334325
if (responseTypeList.contains(KEY_ID_TOKEN)) {
335-
// Implicit/Hybrid flow: must verify front-channel ID Token first
336-
validateIdTokenIssuer(frontChannelTokens.getIdToken(), originIssuer);
326+
// Implicit/Hybrid flow: must verify front-channel ID Token first.
327+
// The issuer is derived from the HMAC-verified domain, so this check
328+
// validates the token's iss against a trusted value.
337329
tokenVerifier.verify(frontChannelTokens.getIdToken(), requestVerifyOptions);
338330
}
339331
if (responseTypeList.contains(KEY_CODE)) {
@@ -344,7 +336,6 @@ private Tokens getVerifiedTokens(HttpServletRequest request, HttpServletResponse
344336
// If we already verified the front-channel token, don't verify it again.
345337
String idTokenFromCodeExchange = codeExchangeTokens.getIdToken();
346338
if (idTokenFromCodeExchange != null) {
347-
validateIdTokenIssuer(idTokenFromCodeExchange, originIssuer);
348339
tokenVerifier.verify(idTokenFromCodeExchange, requestVerifyOptions);
349340
}
350341
}
@@ -384,66 +375,6 @@ private IdTokenVerifier.Options createRequestVerifyOptions(String issuer, String
384375
return requestOptions;
385376
}
386377

387-
/**
388-
* Validates that the ID Token's issuer matches the expected origin issuer.
389-
*
390-
* @param idToken the ID Token to validate
391-
* @param expectedIssuer the expected issuer from the authorization flow
392-
* @throws IdentityVerificationException if the issuer doesn't match
393-
*/
394-
private void validateIdTokenIssuer(String idToken, String expectedIssuer) throws IdentityVerificationException {
395-
if (idToken == null || expectedIssuer == null) {
396-
return;
397-
}
398-
399-
try {
400-
String[] parts = idToken.split("\\.");
401-
if (parts.length != 3) {
402-
throw new IdentityVerificationException(JWT_VERIFICATION_ERROR, "Invalid ID Token format", null);
403-
}
404-
405-
String payload = new String(java.util.Base64.getUrlDecoder().decode(parts[1]));
406-
String tokenIssuer = extractIssuerFromPayload(payload);
407-
408-
if (!tokenIssuer.equals(expectedIssuer)) {
409-
throw new IdentityVerificationException(JWT_VERIFICATION_ERROR,
410-
String.format("Token issuer '%s' does not match expected issuer '%s'",
411-
tokenIssuer, expectedIssuer),
412-
null);
413-
}
414-
} catch (Exception e) {
415-
if (e instanceof IdentityVerificationException) {
416-
throw e;
417-
}
418-
throw new IdentityVerificationException(JWT_VERIFICATION_ERROR,
419-
"Failed to validate token issuer: " + e.getMessage(), e);
420-
}
421-
}
422-
423-
/**
424-
* Extracts the issuer (iss) claim from the ID Token payload.
425-
*
426-
* @param payload the decoded payload of the ID Token
427-
* @return the issuer claim value
428-
* @throws IdentityVerificationException if the issuer claim is missing
429-
*/
430-
private String extractIssuerFromPayload(String payload) throws IdentityVerificationException {
431-
try {
432-
Map<String, Object> payloadMap = new ObjectMapper().readValue(payload,
433-
new TypeReference<Map<String, Object>>() {
434-
});
435-
if (payloadMap.containsKey("iss")) {
436-
return payloadMap.get("iss").toString();
437-
} else {
438-
throw new IdentityVerificationException(JWT_VERIFICATION_ERROR,
439-
"Issuer claim (iss) is missing in the ID Token payload.", null);
440-
}
441-
} catch (Exception e) {
442-
throw new IdentityVerificationException(JWT_VERIFICATION_ERROR,
443-
"Failed to parse ID Token payload: " + e.getMessage(), e);
444-
}
445-
}
446-
447378
List<String> getResponseType() {
448379
return Arrays.asList(responseType.split(" "));
449380
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
package com.auth0;
2+
3+
import javax.crypto.Mac;
4+
import javax.crypto.spec.SecretKeySpec;
5+
import java.nio.charset.StandardCharsets;
6+
import java.security.InvalidKeyException;
7+
import java.security.MessageDigest;
8+
import java.security.NoSuchAlgorithmException;
9+
10+
/**
11+
* Utility class for HMAC-signing cookie values to prevent tampering.
12+
* <p>
13+
* Values are stored as {@code value|signature} where the signature is an
14+
* HMAC-SHA256 hex digest computed using the application's client secret.
15+
* On read, the signature is verified before the value is trusted.
16+
*/
17+
class SignedCookieUtils {
18+
19+
private static final String HMAC_ALGORITHM = "HmacSHA256";
20+
private static final String SEPARATOR = "|";
21+
22+
private SignedCookieUtils() {}
23+
24+
/**
25+
* Signs a value using HMAC-SHA256 with the provided secret.
26+
*
27+
* @param value the value to sign
28+
* @param secret the secret key for HMAC
29+
* @return the signed value in the format {@code value|signature}
30+
* @throws IllegalArgumentException if value or secret is null
31+
*/
32+
static String sign(String value, String secret) {
33+
if (value == null || secret == null) {
34+
throw new IllegalArgumentException("Value and secret must not be null");
35+
}
36+
String signature = computeHmac(value, secret);
37+
return value + SEPARATOR + signature;
38+
}
39+
40+
/**
41+
* Verifies the HMAC signature and extracts the original value.
42+
*
43+
* @param signedValue the signed value in the format {@code value|signature}
44+
* @param secret the secret key used to verify the HMAC
45+
* @return the original value if the signature is valid, or {@code null} if
46+
* the signature is invalid or the format is unexpected
47+
*/
48+
static String verifyAndExtract(String signedValue, String secret) {
49+
System.out.println("Verifying signed value: " + signedValue);
50+
if (signedValue == null || secret == null) {
51+
return null;
52+
}
53+
54+
int separatorIndex = signedValue.lastIndexOf(SEPARATOR);
55+
if (separatorIndex <= 0 || separatorIndex >= signedValue.length() - 1) {
56+
return null;
57+
}
58+
59+
String value = signedValue.substring(0, separatorIndex);
60+
String signature = signedValue.substring(separatorIndex + 1);
61+
62+
String expectedSignature = computeHmac(value, secret);
63+
64+
// Constant-time comparison to prevent timing attacks
65+
if (MessageDigest.isEqual(
66+
expectedSignature.getBytes(StandardCharsets.UTF_8),
67+
signature.getBytes(StandardCharsets.UTF_8))) {
68+
return value;
69+
}
70+
71+
return null;
72+
}
73+
74+
private static String computeHmac(String value, String secret) {
75+
try {
76+
Mac mac = Mac.getInstance(HMAC_ALGORITHM);
77+
SecretKeySpec keySpec = new SecretKeySpec(
78+
secret.getBytes(StandardCharsets.UTF_8), HMAC_ALGORITHM);
79+
mac.init(keySpec);
80+
byte[] hmacBytes = mac.doFinal(value.getBytes(StandardCharsets.UTF_8));
81+
return bytesToHex(hmacBytes);
82+
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
83+
throw new RuntimeException("Failed to compute HMAC-SHA256", e);
84+
}
85+
}
86+
87+
private static String bytesToHex(byte[] bytes) {
88+
StringBuilder sb = new StringBuilder(bytes.length * 2);
89+
for (byte b : bytes) {
90+
sb.append(String.format("%02x", b));
91+
}
92+
return sb.toString();
93+
}
94+
}

src/main/java/com/auth0/StorageUtils.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ private StorageUtils() {}
1111
static final String STATE_KEY = "com.auth0.state";
1212
static final String NONCE_KEY = "com.auth0.nonce";
1313
static final String ORIGIN_DOMAIN_KEY = "com.auth0.origin_domain";
14-
static final String ORIGIN_ISSUER_KEY = "com.auth0.origin_issuer";
1514

1615
/**
1716
* Generates a new random string using {@link SecureRandom}.

0 commit comments

Comments
 (0)