Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions duo-universal-sdk/src/main/java/com/duosecurity/Utils.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.auth0.jwt.interfaces.Claim;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.duosecurity.exception.DuoException;
import com.duosecurity.model.AccessDevice;
Expand All @@ -19,6 +21,7 @@
import java.security.SecureRandom;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Map;

public class Utils {
Expand Down Expand Up @@ -76,9 +79,19 @@ static Token transformDecodedJwtToToken(DecodedJWT decodedJwt) {
token.setAuth_time(decodedJwt.getClaim("auth_time").asInt());
token.setExp(decodedJwt.getClaim("exp").asInt());
token.setSub(decodedJwt.getClaim("sub").asString());
token.setAmr(extractAmr(decodedJwt.getClaim("amr")));
return token;
}

private static List<String> extractAmr(Claim amrClaim) {
try {
return amrClaim.asList(String.class);
} catch (JWTDecodeException e) {
// Non-string array elements (RFC 8176 violation) — treat as absent.
return null;
}
}

static boolean validateCaCert(String[] userCaCerts) {
if (userCaCerts == null || userCaCerts.length == 0) {
return false;
Expand Down
21 changes: 18 additions & 3 deletions duo-universal-sdk/src/main/java/com/duosecurity/model/Token.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.duosecurity.model;

import java.io.Serializable;
import java.util.List;
import java.util.Objects;

public class Token implements Serializable {
Expand All @@ -15,10 +16,12 @@ public class Token implements Serializable {
private Integer auth_time;
private AuthResult auth_result;
private AuthContext auth_context;
private List<String> amr;

/**
* Constructor with all properties.
*
* Constructor for the legacy set of claims. Does not set {@code amr};
* use {@link #setAmr(java.util.List)} for that.
*
* @param iss iss
* @param sub sub
* @param preferredUsername preferred_username
Expand Down Expand Up @@ -121,6 +124,14 @@ public void setAuth_context(AuthContext authContext) {
this.auth_context = authContext;
}

public List<String> getAmr() {
return amr;
}

public void setAmr(List<String> amr) {
this.amr = amr;
}

@Override
public String toString() {
return "Token [iss=" + iss
Expand All @@ -132,6 +143,7 @@ public String toString() {
+ ", auth_time=" + auth_time
+ ", auth_result=" + auth_result
+ ", auth_context=" + auth_context
+ ", amr=" + amr
+ ", getAud()=" + getAud()
+ ", getAuth_context()=" + getAuth_context()
+ ", getAuth_result()=" + getAuth_result()
Expand All @@ -141,6 +153,7 @@ public String toString() {
+ ", getIss()=" + getIss()
+ ", getPreferred_username()=" + getPreferred_username()
+ ", getSub()=" + getSub()
+ ", getAmr()=" + getAmr()
+ ", hashCode()=" + hashCode()
+ ", getClass()=" + getClass()
+ ", toString()=" + super.toString()
Expand All @@ -167,7 +180,8 @@ public boolean equals(Object obj) {
&& Objects.equals(iat, other.iat)
&& Objects.equals(auth_time, other.auth_time)
&& Objects.equals(auth_result, other.auth_result)
&& Objects.equals(auth_context, other.auth_context);
&& Objects.equals(auth_context, other.auth_context)
&& Objects.equals(amr, other.amr);
}

@Override
Expand All @@ -183,6 +197,7 @@ public int hashCode() {
result = prime * result + ((auth_time == null) ? 0 : auth_time.hashCode());
result = prime * result + ((auth_result == null) ? 0 : auth_result.hashCode());
result = prime * result + ((auth_context == null) ? 0 : auth_context.hashCode());
result = prime * result + ((amr == null) ? 0 : amr.hashCode());
return result;
}
}
103 changes: 102 additions & 1 deletion duo-universal-sdk/src/test/java/com/duosecurity/UtilsTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,11 @@
import org.junit.jupiter.api.Test;

import java.net.URL;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import static org.junit.jupiter.api.Assertions.*;
Expand Down Expand Up @@ -70,14 +73,112 @@ void createJWTForAuthURL() throws DuoException {

@Test
void transformDecodedJwtToToken() {
String jwt = createTestJWT();
String jwt = createTestJWT();
// Just testing the transform logic so a simple decode is sufficient
DecodedJWT decodedJWT = JWT.decode(jwt);
Token token = Utils.transformDecodedJwtToToken(decodedJWT);

assertEquals(token.getIss(), "issuer");
assertEquals(token.getSub(), "test");
assertEquals(token.getAud(), "aud");
// amr claim is optional; when absent, the field should be null.
assertNull(token.getAmr());
}

@Test
void transformDecodedJwtToTokenWithAmr() {
List<String> amr = Arrays.asList("mfa", "otp");
String jwt = JWT.create()
.withIssuer("issuer")
.withSubject("test")
.withAudience("aud")
.withArrayClaim("amr", amr.toArray(new String[0]))
.sign(Algorithm.HMAC512(CLIENT_SECRET));
DecodedJWT decodedJWT = JWT.decode(jwt);

Token token = Utils.transformDecodedJwtToToken(decodedJWT);

assertEquals(amr, token.getAmr());
}

@Test
void transformDecodedJwtToTokenWithEmptyAmr() {
String jwt = JWT.create()
.withIssuer("issuer")
.withSubject("test")
.withAudience("aud")
.withArrayClaim("amr", new String[0])
.sign(Algorithm.HMAC512(CLIENT_SECRET));
DecodedJWT decodedJWT = JWT.decode(jwt);

Token token = Utils.transformDecodedJwtToToken(decodedJWT);

assertEquals(Collections.emptyList(), token.getAmr());
}

@Test
void transformDecodedJwtToTokenWithNullAmr() {
String jwt = JWT.create()
.withIssuer("issuer")
.withSubject("test")
.withAudience("aud")
.withNullClaim("amr")
.sign(Algorithm.HMAC512(CLIENT_SECRET));
DecodedJWT decodedJWT = JWT.decode(jwt);

Token token = Utils.transformDecodedJwtToToken(decodedJWT);

assertNull(token.getAmr());
}

@Test
void transformDecodedJwtToTokenWithNonArrayAmr() {
String jwt = JWT.create()
.withIssuer("issuer")
.withSubject("test")
.withAudience("aud")
.withClaim("amr", "mfa")
.sign(Algorithm.HMAC512(CLIENT_SECRET));
DecodedJWT decodedJWT = JWT.decode(jwt);

Token token = Utils.transformDecodedJwtToToken(decodedJWT);

assertNull(token.getAmr());
}

@Test
void transformDecodedJwtToTokenWithNumericAmrElements() {
// Jackson coerces numeric elements to their string form when the target
// type is String, so this does not throw and yields ["1", "2"].
// The try/catch in extractAmr is defense-in-depth for genuinely
// non-coercible element types.
String jwt = JWT.create()
.withIssuer("issuer")
.withSubject("test")
.withAudience("aud")
.withArrayClaim("amr", new Integer[]{1, 2})
.sign(Algorithm.HMAC512(CLIENT_SECRET));
DecodedJWT decodedJWT = JWT.decode(jwt);

Token token = assertDoesNotThrow(() -> Utils.transformDecodedJwtToToken(decodedJWT));

assertEquals(Arrays.asList("1", "2"), token.getAmr());
}

@Test
void tokenEqualityRespectsAmrField() {
Token a = new Token();
a.setAmr(Arrays.asList("mfa"));
Token b = new Token();
b.setAmr(Arrays.asList("mfa"));
Token c = new Token();
c.setAmr(Arrays.asList("otp"));
Token d = new Token();

assertEquals(a, b);
assertEquals(a.hashCode(), b.hashCode());
assertNotEquals(a, c);
assertNotEquals(a, d);
}

@Test
Expand Down
Loading