Skip to content

Remove usage of com.nimbusds.oauth2's Token classes #928

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Apr 4, 2025
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
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@

package com.microsoft.aad.msal4j;

import com.nimbusds.oauth2.sdk.util.JSONObjectUtils;
import net.minidev.json.JSONObject;
import com.nimbusds.oauth2.sdk.ParseException;
import com.azure.json.JsonProviders;
import com.azure.json.JsonReader;

import java.io.IOException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
Expand Down Expand Up @@ -62,12 +62,8 @@ List<String> getHeader(String key) {
return headers.get(key);
}

JSONObject getBodyAsJson() {
try {
return JSONObjectUtils.parse(this.body());
} catch (ParseException e) {
throw new RuntimeException(e);
}
Map<String, String> getBodyAsMap() {
return JsonHelper.convertJsonToMap(this.body);
}

public int statusCode() {
Expand Down
11 changes: 11 additions & 0 deletions msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/JsonHelper.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import java.io.IOException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;

class JsonHelper {
Expand Down Expand Up @@ -51,6 +52,16 @@ static <T extends JsonSerializable<T>> T convertJsonStringToJsonSerializableObje
}
}

//Converts a JSON string to a Map<String, String>
static Map<String, String> convertJsonToMap(String jsonString) {
try (JsonReader reader = JsonProviders.createReader(jsonString)) {
reader.nextToken();
return reader.readMap(JsonReader::getString);
} catch (IOException e) {
throw new MsalClientException("Could not parse JSON from HttpResponse body: " + e.getMessage(), AuthenticationErrorCode.INVALID_JSON);
}
}

/**
* Throws exception if given String does not follow JSON syntax
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
import com.nimbusds.oauth2.sdk.ParseException;
import com.nimbusds.oauth2.sdk.SerializeException;
import com.nimbusds.oauth2.sdk.util.URLUtils;
import com.nimbusds.openid.connect.sdk.token.OIDCTokens;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

Expand Down Expand Up @@ -121,17 +120,11 @@ private AuthenticationResult createAuthenticationResultFromOauthHttpResponse(
if (oauthHttpResponse.statusCode() == HttpHelper.HTTP_STATUS_200) {
final TokenResponse response = TokenResponse.parseHttpResponse(oauthHttpResponse);

OIDCTokens tokens = response.getOIDCTokens();
String refreshToken = null;
if (tokens.getRefreshToken() != null) {
refreshToken = tokens.getRefreshToken().getValue();
}

AccountCacheEntity accountCacheEntity = null;
if (!StringHelper.isNullOrBlank(tokens.getIDTokenString())) {
if (!StringHelper.isNullOrBlank(response.idToken())) {
String idTokenJson;
try {
idTokenJson = new String(Base64.getDecoder().decode(tokens.getIDTokenString().split("\\.")[1]), StandardCharsets.UTF_8);
idTokenJson = new String(Base64.getDecoder().decode(response.idToken().split("\\.")[1]), StandardCharsets.UTF_8);
} catch (ArrayIndexOutOfBoundsException e) {
throw new MsalServiceException("Error parsing ID token, missing payload section. Ensure that the ID token is following the JWT format.",
AuthenticationErrorCode.INVALID_JWT);
Expand Down Expand Up @@ -161,10 +154,10 @@ private AuthenticationResult createAuthenticationResultFromOauthHttpResponse(
long currTimestampSec = new Date().getTime() / 1000;

result = AuthenticationResult.builder().
accessToken(tokens.getAccessToken().getValue()).
refreshToken(refreshToken).
accessToken(response.accessToken()).
refreshToken(response.refreshToken()).
familyId(response.getFoci()).
idToken(tokens.getIDTokenString()).
idToken(response.idToken()).
environment(requestAuthority.host()).
expiresOn(currTimestampSec + response.getExpiresIn()).
extExpiresOn(response.getExtExpiresIn() > 0 ? currTimestampSec + response.getExtExpiresIn() : 0).
Expand Down
117 changes: 30 additions & 87 deletions msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/TokenResponse.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,108 +3,39 @@

package com.microsoft.aad.msal4j;

import com.nimbusds.oauth2.sdk.ParseException;
import com.nimbusds.oauth2.sdk.token.AccessToken;
import com.nimbusds.oauth2.sdk.token.RefreshToken;
import com.nimbusds.oauth2.sdk.util.JSONObjectUtils;
import com.nimbusds.openid.connect.sdk.OIDCTokenResponse;
import com.nimbusds.openid.connect.sdk.token.OIDCTokens;
import net.minidev.json.JSONObject;
import java.util.Map;

class TokenResponse extends OIDCTokenResponse {
class TokenResponse {

private String scope;
private String clientInfo;
private long expiresIn;
private long extExpiresIn;
private String foci;
private long refreshIn;

TokenResponse(final AccessToken accessToken, final RefreshToken refreshToken, final String idToken,
final String scope, String clientInfo, long expiresIn, long extExpiresIn, String foci,
long refreshIn) {
super(new OIDCTokens(idToken, accessToken, refreshToken));
this.scope = scope;
this.clientInfo = clientInfo;
this.expiresIn = expiresIn;
this.extExpiresIn = extExpiresIn;
this.refreshIn = refreshIn;
this.foci = foci;
private String accessToken;
private String idToken;
private String refreshToken;

TokenResponse(Map<String, String> jsonMap) {
this.accessToken = jsonMap.get("access_token");
this.idToken = jsonMap.get("id_token");
this.refreshToken = jsonMap.get("refresh_token");
this.scope = jsonMap.get("scope");
this.clientInfo = jsonMap.get("client_info");
this.expiresIn = StringHelper.isNullOrBlank(jsonMap.get("expires_in")) ? 0 : Long.parseLong(jsonMap.get("expires_in"));
this.extExpiresIn = StringHelper.isNullOrBlank(jsonMap.get("ext_expires_in")) ? 0 : Long.parseLong(jsonMap.get("ext_expires_in"));
this.refreshIn = StringHelper.isNullOrBlank(jsonMap.get("refresh_in")) ? 0: Long.parseLong(jsonMap.get("refresh_in"));
this.foci = jsonMap.get("foci");
}

static TokenResponse parseHttpResponse(final HttpResponse httpResponse) throws ParseException {
static TokenResponse parseHttpResponse(final HttpResponse httpResponse) {

if (httpResponse.statusCode() != HttpHelper.HTTP_STATUS_200) {
throw MsalServiceExceptionFactory.fromHttpResponse(httpResponse);
}

final JSONObject jsonObject = httpResponse.getBodyAsJson();

return parseJsonObject(jsonObject);
}

static Long getLongValue(JSONObject jsonObject, String key) throws ParseException {
Object value = jsonObject.get(key);

if (value instanceof Long) {
return JSONObjectUtils.getLong(jsonObject, key);
} else {
return Long.parseLong(JSONObjectUtils.getString(jsonObject, key));
}
}

static TokenResponse parseJsonObject(final JSONObject jsonObject)
throws ParseException {

// In same cases such as client credentials there isn't an id token. Instead of a null value
// use an empty string in order to avoid an IllegalArgumentException from OIDCTokens.
String idTokenValue = "";
if (jsonObject.containsKey("id_token")) {
idTokenValue = JSONObjectUtils.getString(jsonObject, "id_token");
}

// Parse value
String scopeValue = null;
if (jsonObject.containsKey("scope")) {
scopeValue = JSONObjectUtils.getString(jsonObject, "scope");
}

String clientInfo = null;
if (jsonObject.containsKey("client_info")) {
clientInfo = JSONObjectUtils.getString(jsonObject, "client_info");
}

long expiresIn = 0;
if (jsonObject.containsKey("expires_in")) {
expiresIn = getLongValue(jsonObject, "expires_in");
}

long ext_expires_in = 0;
if (jsonObject.containsKey("ext_expires_in")) {
ext_expires_in = getLongValue(jsonObject, "ext_expires_in");
}

String foci = null;
if (jsonObject.containsKey("foci")) {
foci = JSONObjectUtils.getString(jsonObject, "foci");
}

long refreshIn = 0;
if (jsonObject.containsKey("refresh_in")) {
refreshIn = getLongValue(jsonObject, "refresh_in");
}

try {
final AccessToken accessToken = AccessToken.parse(jsonObject);
final RefreshToken refreshToken = RefreshToken.parse(jsonObject);
return new TokenResponse(accessToken, refreshToken, idTokenValue, scopeValue, clientInfo,
expiresIn, ext_expires_in, foci, refreshIn);
} catch (ParseException e) {
throw new MsalClientException("Invalid or missing token, could not parse. If using B2C, information on a potential B2C issue and workaround can be found here: https://aka.ms/msal4j-b2c-known-issues",
AuthenticationErrorCode.INVALID_JSON);
} catch (Exception e) {
throw new MsalClientException(e);
}
return new TokenResponse(httpResponse.getBodyAsMap());
}

String getScope() {
Expand All @@ -130,4 +61,16 @@ String getFoci() {
long getRefreshIn() {
return this.refreshIn;
}

public String accessToken() {
return accessToken;
}

public String idToken() {
return idToken;
}

public String refreshToken() {
return refreshToken;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ public void tokenCacheEntitiesFormatTest(String folder) throws URISyntaxExceptio
doReturn(msalOAuthHttpRequest).when(request).createOauthHttpRequest();
doReturn(httpResponse).when(msalOAuthHttpRequest).send();
doReturn(200).when(httpResponse).statusCode();
doReturn(JSONObjectUtils.parse(tokenResponse)).when(httpResponse).getBodyAsJson();
doReturn(JsonHelper.convertJsonToMap((tokenResponse))).when(httpResponse).getBodyAsMap();

final AuthenticationResult result = request.executeTokenRequest();

Expand Down Expand Up @@ -186,14 +186,24 @@ private void validateAccessTokenCacheEntity(String folder, String tokenResponse,
String valueExpected = readResource(folder + AT_CACHE_ENTITY);

JSONObject tokenResponseJsonObj = JSONObjectUtils.parse(tokenResponse);
long expireIn = TokenResponse.getLongValue(tokenResponseJsonObj, "expires_in");
long expireIn = getLongValue(tokenResponseJsonObj, "expires_in");

long extExpireIn = TokenResponse.getLongValue(tokenResponseJsonObj, "ext_expires_in");
long extExpireIn = getLongValue(tokenResponseJsonObj, "ext_expires_in");

JSONAssert.assertEquals(valueExpected, valueActual,
new DynamicTimestampsComparator(JSONCompareMode.STRICT, expireIn, extExpireIn));
}

static Long getLongValue(JSONObject jsonObject, String key) throws ParseException {
Object value = jsonObject.get(key);

if (value instanceof Long) {
return JSONObjectUtils.getLong(jsonObject, key);
} else {
return Long.parseLong(JSONObjectUtils.getString(jsonObject, key));
}
}

private void validateRefreshTokenCacheEntity(String folder, TokenCache tokenCache)
throws IOException, URISyntaxException, JSONException {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ private PublicClientApplication getClientApplicationMockedWithOneTokenEndpointRe
switch (responseType) {
case RETRY_AFTER_HEADER:
httpResponse.statusCode(HTTPResponse.SC_OK);
httpResponse.body(TestConfiguration.TOKEN_ENDPOINT_OK_RESPONSE);
httpResponse.body(TestConfiguration.TOKEN_ENDPOINT_OK_RESPONSE_ID_AND_ACCESS);

headers.put("Retry-After", Arrays.asList(THROTTLE_IN_SEC.toString()));
break;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ public final class TestConfiguration {

public final static String AAD_PREFERRED_NETWORK_ENV_ALIAS = "login.microsoftonline.com";

public final static String TOKEN_ENDPOINT_OK_RESPONSE = "{\"access_token\":\"eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6I"
public final static String TOKEN_ENDPOINT_OK_RESPONSE_ID_AND_ACCESS = "{\"access_token\":\"eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6I"
+ "k5HVEZ2ZEstZnl0aEV1THdqcHdBSk9NOW4tQSJ9.eyJhdWQiOiJiN2E2NzFkOC1hNDA4LTQyZmYtODZlMC1hYWY0NDdmZDE3YzQiLCJpc3MiOiJod"
+ "HRwczovL3N0cy53aW5kb3dzLm5ldC8zMGJhYTY2Ni04ZGY4LTQ4ZTctOTdlNi03N2NmZDA5OTU5NjMvIiwiaWF0IjoxMzkzODQ0NTA0LCJuYmYiOj"
+ "EzOTM4NDQ1MDQsImV4cCI6MTM5Mzg0ODQwNCwidmVyIjoiMS4wIiwidGlkIjoiMzBiYWE2NjYtOGRmOC00OGU3LTk3ZTYtNzdjZmQwOTk1OTYzIiwi"
Expand All @@ -66,6 +66,21 @@ public final class TestConfiguration {
"WQiOiI5ZjQ4ODBkOC04MGJhLTRjNDAtOTdiYy1mN2EyM2M3MDMwODQiLCJ1dGlkIjoiZjY0NWFkOTItZTM4ZC00ZDFhLWI1MTAtZDFiMDlhNzRhOGNhIn0\"" +
"}";

public final static String TOKEN_ENDPOINT_OK_RESPONSE_ACCESS_ONLY = "{\"token_type\":\"Bearer\",\"expires_in\":3600," +
"\"access_token\":\"eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6Ik5HVEZ2ZEstZnl0aEV1THdqcHdBSk9NOW4tQSJ9" +
".eyJhdWQiOiJiN2E2NzFkOC1hNDA4LTQyZmYtODZlMC1hYWY0NDdmZDE3YzQiLCJpc3MiOiJodRwczovL3N0cy53aW5kb3dzLm5ldC8" +
"zMGJhYTY2Ni04ZGY4LTQ4ZTctOTdlNi03N2NmZDA5OTU5NjMvIiwiaWF0IjoxMzkzODQ0NTA0LCJuYmYiOjEzOTM4NDQ1MDQsImV4cC" +
"I6MTM5Mzg0ODQwNCwidmVyIjoiMS4wIiwidGlkIjoiMzBiYWE2NjYtOGRmOC00OGU3LTk3ZTYtNzdjZmQwOTk1OTYzIiwib2lkIjoiN" +
"GY4NTk5ODktYTJmZi00MTFlLTkwNDgtYzMyMjI0N2FjNjJjIiwidXBuIjoiYWRtaW5AYWFsdGVzdHMub25taWNyb3NvZnQuY29tIiwi" +
"dW5pcXVlX25hbWUiOiJhZG1pbkBhYWx0ZXN0cy5vbm1pY3Jvc29mdC5jb20iLCJzdWIiOiJqQ0ttUENWWEFzblN1MVBQUzRWblo4c2V" +
"ONTR3U3F0cW1RkpGbW14SEF3IiwiZmFtaWx5X25hbWUiOiJBZG1pbiIsImdpdmVuX25hbWUiOiJBREFMVGVzdHMiLCJhcHBpZCI6Ijk" +
"wODNjY2I4LThhNDYtNDNlNy04NDM5LTFkNjk2ZGY5ODRhZSIsImFwcGlkYWNyIjoiMSIsInNjcCI6InVzZXJfaW1wZXJzb25hdGlvbi" +
"IsImFjciI6IjEifQ.lUfDlkLdNGuAGUukgTnS_uWeSFXljbhId1l9PDrr7AwOSbOzogLvO14TaU294T6HeOQ8e0dUAvxEAMvsK_800A" +
"-AGNvbHK363xDjgmu464ye3CQvwq73GoHkzuxILCJKo0DUj0_XsCpQ4TdkPepuhzaGc-zYsfMU1POuIOB87pzW7e_VDpCdxcN1fuk-7" +
"CECPQb8nrO0L8Du8y-TZDdTSe-i_A0Alv48Zll-6tDY9cxfAR0UyYKl_Kf45kuHAphCWwPsjUxv4rGHhgXZPRlKFq7bkXP2Es4ixCQz" +
"b3bVLLrtQaZjkQ1yn37ngJro8NR63EbHHjHTA9lRmf8KIQ\"" +
"}";

public final static String HTTP_ERROR_RESPONSE = "{\"error\":\"invalid_request\",\"error_description\":\"AADSTS90011: Request "
+ "is ambiguous, multiple application identifiers found. Application identifiers: 'd09bb6da-4d46-4a16-880c-7885d8291fb9"
+ ", 216ef81d-f3b2-47d4-ad21-a4df49b56dee'.\r\nTrace ID: 428a1f68-767d-4a1c-ae8e-f710eeaf4e9b\r\nCorrelation ID: 1e0955"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

package com.microsoft.aad.msal4j;

import com.azure.json.JsonProviders;
import com.azure.json.JsonReader;
import com.nimbusds.jose.*;
import com.nimbusds.jose.crypto.RSASSASigner;
import com.nimbusds.jose.jwk.RSAKey;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@

import com.nimbusds.oauth2.sdk.ParseException;
import com.nimbusds.oauth2.sdk.SerializeException;
import com.nimbusds.oauth2.sdk.util.JSONObjectUtils;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;
import static org.junit.jupiter.api.Assertions.assertEquals;
Expand Down Expand Up @@ -235,7 +234,7 @@ void testExecuteOAuth_Success() throws SerializeException, ParseException, MsalE

doReturn(msalOAuthHttpRequest).when(request).createOauthHttpRequest();
doReturn(httpResponse).when(msalOAuthHttpRequest).send();
doReturn(JSONObjectUtils.parse(TestConfiguration.TOKEN_ENDPOINT_OK_RESPONSE)).when(httpResponse).getBodyAsJson();
doReturn(JsonHelper.convertJsonToMap(TestConfiguration.TOKEN_ENDPOINT_OK_RESPONSE_ID_AND_ACCESS)).when(httpResponse).getBodyAsMap();

doReturn(200).when(httpResponse).statusCode();

Expand Down
Loading