Skip to content

Remove and replace various com.nimbusds utils #941

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

Open
wants to merge 3 commits into
base: avdunn/nimbus-removal
Choose a base branch
from
Open
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 @@ -93,6 +93,11 @@ public class AuthenticationErrorCode {
*/
public final static String INVALID_REDIRECT_URI = "invalid_redirect_uri";

/**
* Indicates token endpoint is invalid. Ensure authority and tenant are correctly set, as this endpoint is typically created using those values.
*/
public final static String INVALID_ENDPOINT_URI = "invalid_endpoint_uri";

/**
* MSAL was unable to open the user-default browser. This is either because the current platform
* does not support {@link java.awt.Desktop} or {@link java.awt.Desktop.Action#BROWSE}. Interactive
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@

package com.microsoft.aad.msal4j;

import com.nimbusds.oauth2.sdk.auth.JWTAuthentication;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.experimental.Accessors;
Expand All @@ -13,7 +12,7 @@
@EqualsAndHashCode
final class ClientAssertion implements IClientAssertion {

static final String assertionType = JWTAuthentication.CLIENT_ASSERTION_TYPE;
static final String ASSERTION_TYPE_JWT_BEARER = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer";
private final String assertion;

ClientAssertion(final String assertion) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
package com.microsoft.aad.msal4j;

import com.fasterxml.jackson.annotation.JsonProperty;
import com.nimbusds.jose.util.StandardCharset;

import java.nio.charset.StandardCharsets;
import java.util.Base64;

import static com.microsoft.aad.msal4j.Constants.POINT_DELIMITER;
Expand All @@ -23,9 +23,9 @@ public static ClientInfo createFromJson(String clientInfoJsonBase64Encoded) {
return null;
}

byte[] decodedInput = Base64.getUrlDecoder().decode(clientInfoJsonBase64Encoded.getBytes(StandardCharset.UTF_8));
byte[] decodedInput = Base64.getUrlDecoder().decode(clientInfoJsonBase64Encoded.getBytes(StandardCharsets.UTF_8));

return JsonHelper.convertJsonToObject(new String(decodedInput, StandardCharset.UTF_8), ClientInfo.class);
return JsonHelper.convertJsonToObject(new String(decodedInput, StandardCharsets.UTF_8), ClientInfo.class);
}

String toAccountIdentifier() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@

package com.microsoft.aad.msal4j;

import com.nimbusds.jwt.JWTParser;

import java.net.URL;
import java.util.concurrent.CompletableFuture;

Expand Down Expand Up @@ -67,11 +65,10 @@ default IAuthenticationResult parseBrokerAuthResult(String authority, String idT
if (idToken != null) {
builder.idToken(idToken);
if (accountId != null) {
String idTokenJson =
JWTParser.parse(idToken).getParsedParts()[1].decodeToString();
IdToken idTokenObj = JsonHelper.createIdTokenFromEncodedTokenString(idToken);

builder.accountCacheEntity(AccountCacheEntity.create(clientInfo,
Authority.createAuthority(new URL(authority)), JsonHelper.convertJsonToObject(idTokenJson,
IdToken.class), null));
Authority.createAuthority(new URL(authority)), idTokenObj, null));
}
}
if (accessToken != null) {
Expand Down
41 changes: 0 additions & 41 deletions msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/IdToken.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,29 +4,10 @@
package com.microsoft.aad.msal4j;

import com.fasterxml.jackson.annotation.JsonProperty;
import com.nimbusds.jwt.JWTClaimsSet;

import java.text.ParseException;
import java.util.HashMap;
import java.util.Map;

import java.io.Serializable;

class IdToken implements Serializable {

static final String ISSUER = "iss";
static final String SUBJECT = "sub";
static final String AUDIENCE = "aud";
static final String EXPIRATION_TIME = "exp";
static final String ISSUED_AT = "issuedAt";
static final String NOT_BEFORE = "nbf";
static final String NAME = "name";
static final String PREFERRED_USERNAME = "preferred_username";
static final String OBJECT_IDENTIFIER = "oid";
static final String TENANT_IDENTIFIER = "tid";
static final String UPN = "upn";
static final String UNIQUE_NAME = "unique_name";

@JsonProperty("iss")
protected String issuer;

Expand Down Expand Up @@ -62,26 +43,4 @@ class IdToken implements Serializable {

@JsonProperty("unique_name")
protected String uniqueName;

static IdToken createFromJWTClaims(final JWTClaimsSet claims) throws ParseException {
IdToken idToken = new IdToken();

idToken.issuer = claims.getStringClaim(ISSUER);
idToken.subject = claims.getStringClaim(SUBJECT);
idToken.audience = claims.getStringClaim(AUDIENCE);

idToken.expirationTime = claims.getLongClaim(EXPIRATION_TIME);
idToken.issuedAt = claims.getLongClaim(ISSUED_AT);
idToken.notBefore = claims.getLongClaim(NOT_BEFORE);

idToken.name = claims.getStringClaim(NAME);
idToken.preferredUsername = claims.getStringClaim(PREFERRED_USERNAME);
idToken.objectIdentifier = claims.getStringClaim(OBJECT_IDENTIFIER);
idToken.tenantIdentifier = claims.getStringClaim(TENANT_IDENTIFIER);

idToken.upn = claims.getStringClaim(UPN);
idToken.uniqueName = claims.getStringClaim(UNIQUE_NAME);

return idToken;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,8 @@
import com.fasterxml.jackson.databind.JsonNode;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.nio.charset.StandardCharsets;
import java.util.*;

class JsonHelper {
static ObjectMapper mapper;
Expand All @@ -41,6 +39,18 @@ static <T> T convertJsonToObject(final String json, final Class<T> tClass) {
}
}

static IdToken createIdTokenFromEncodedTokenString(String token) {
String idTokenJson;
try {
idTokenJson = new String(Base64.getUrlDecoder().decode(token.split("\\.")[1]), StandardCharsets.UTF_8);
} catch (ArrayIndexOutOfBoundsException e) {
throw new MsalClientException("Error parsing ID token, missing payload section.",
AuthenticationErrorCode.INVALID_JWT);
}

return JsonHelper.convertJsonToObject(idTokenJson, IdToken.class);
}

//This method is used to convert a JSON string to an object which implements the JsonSerializable interface from com.azure.json
static <T extends JsonSerializable<T>> T convertJsonStringToJsonSerializableObject(String jsonResponse, ReadValueCallback<JsonReader, T> readFunction) {
try (JsonReader jsonReader = JsonProviders.createReader(jsonResponse)) {
Expand Down
94 changes: 50 additions & 44 deletions msal4j-sdk/src/main/java/com/microsoft/aad/msal4j/JwtHelper.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,76 +3,82 @@

package com.microsoft.aad.msal4j;

import java.nio.charset.StandardCharsets;
import java.security.Signature;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.Base64;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;

import com.nimbusds.jose.JWSAlgorithm;
import com.nimbusds.jose.JWSHeader;
import com.nimbusds.jose.JWSHeader.Builder;
import com.nimbusds.jose.crypto.RSASSASigner;
import com.nimbusds.jose.util.Base64;
import com.nimbusds.jose.util.Base64URL;
import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.jwt.SignedJWT;

final class JwtHelper {

static ClientAssertion buildJwt(String clientId, final ClientCertificate credential,
final String jwtAudience, boolean sendX5c,
boolean useSha1) throws MsalClientException {
if (StringHelper.isBlank(clientId)) {
throw new IllegalArgumentException("clientId is null or empty");
}

if (credential == null) {
throw new IllegalArgumentException("credential is null");
}

final long time = System.currentTimeMillis();
ParameterValidationUtils.validateNotBlank("clientId", clientId);
ParameterValidationUtils.validateNotNull("credential", clientId);

final JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
.audience(Collections.singletonList(jwtAudience))
.issuer(clientId)
.jwtID(UUID.randomUUID().toString())
.notBeforeTime(new Date(time))
.expirationTime(new Date(time
+ Constants.AAD_JWT_TOKEN_LIFETIME_SECONDS
* 1000))
.subject(clientId)
.build();

SignedJWT jwt;
try {
JWSHeader.Builder builder = new Builder(JWSAlgorithm.RS256);
final long time = System.currentTimeMillis();

// Build header
Map<String, Object> header = new HashMap<>();
header.put("alg", "RS256");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The alg is PS256 not RS256 to suggest PSS padding instead of PKCS1 padding. Please have a look at the .NET code for this.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The current version of the library uses RS256 here, and has apparently done so since the 'sendX5C' API was first added in 2020 by PR #285:

So this might need a more thorough investigation. Pretty much every integration test sends that "alg=RS256" header when retrieving the token to access ID labs, so the endpoint is either accepting or ignoring it.

header.put("typ", "JWT");

if (sendX5c) {
List<Base64> certs = new ArrayList<>();
for (String cert : credential.getEncodedPublicKeyCertificateChain()) {
certs.add(new Base64(cert));
}
builder.x509CertChain(certs);
List<String> certs = new ArrayList<>(credential.getEncodedPublicKeyCertificateChain());
header.put("x5c", certs);
}

//SHA-256 is preferred, however certain flows still require SHA-1 due to what is supported server-side. If SHA-256
// is not supported or the IClientCredential.publicCertificateHash256() method is not implemented, the library will default to SHA-1.
String hash256 = credential.publicCertificateHash256();
if (useSha1 || hash256 == null) {
builder.x509CertThumbprint(new Base64URL(credential.publicCertificateHash()));
header.put("x5t", credential.publicCertificateHash());
} else {
builder.x509CertSHA256Thumbprint(new Base64URL(hash256));
header.put("x5t#S256", hash256);
}

jwt = new SignedJWT(builder.build(), claimsSet);
final RSASSASigner signer = new RSASSASigner(credential.privateKey());
// Build payload
Map<String, Object> payload = new HashMap<>();
payload.put("aud", jwtAudience);
payload.put("iss", clientId);
payload.put("jti", UUID.randomUUID().toString());
payload.put("nbf", time / 1000);
payload.put("exp", time / 1000 + Constants.AAD_JWT_TOKEN_LIFETIME_SECONDS);
payload.put("sub", clientId);

// Concatenate header and payload
String jsonHeader = JsonHelper.mapper.writeValueAsString(header);
String jsonPayload = JsonHelper.mapper.writeValueAsString(payload);

jwt.sign(signer);
String encodedHeader = base64UrlEncode(jsonHeader.getBytes(StandardCharsets.UTF_8));
String encodedPayload = base64UrlEncode(jsonPayload.getBytes(StandardCharsets.UTF_8));

// Create signature
String dataToSign = encodedHeader + "." + encodedPayload;

Signature sig = Signature.getInstance("SHA256withRSA");
sig.initSign(credential.privateKey());
sig.update(dataToSign.getBytes(StandardCharsets.UTF_8));
byte[] signatureBytes = sig.sign();

String encodedSignature = base64UrlEncode(signatureBytes);

// Build the JWT
String jwt = dataToSign + "." + encodedSignature;

return new ClientAssertion(jwt);
} catch (final Exception e) {
throw new MsalClientException(e);
}
}

return new ClientAssertion(jwt.serialize());
private static String base64UrlEncode(byte[] data) {
return Base64.getUrlEncoder().withoutPadding().encodeToString(data);
}
}
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,11 @@

package com.microsoft.aad.msal4j;

import com.nimbusds.oauth2.sdk.ParseException;
import com.nimbusds.oauth2.sdk.SerializeException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.net.MalformedURLException;
import java.nio.charset.StandardCharsets;
import java.util.*;

class TokenRequestExecutor {
Expand All @@ -30,18 +27,19 @@ class TokenRequestExecutor {
msalRequest.requestContext().apiParameters().tenant() ;
}

AuthenticationResult executeTokenRequest() throws ParseException, IOException {
AuthenticationResult executeTokenRequest() throws IOException {

log.debug("Sending token request to: {}", requestAuthority.canonicalAuthorityUrl());
OAuthHttpRequest oAuthHttpRequest = createOauthHttpRequest();
HttpResponse oauthHttpResponse = oAuthHttpRequest.send();
return createAuthenticationResultFromOauthHttpResponse(oauthHttpResponse);
}

OAuthHttpRequest createOauthHttpRequest() throws SerializeException, MalformedURLException {
OAuthHttpRequest createOauthHttpRequest() throws MalformedURLException {

if (requestAuthority.tokenEndpointUrl() == null) {
throw new SerializeException("The endpoint URI is not specified");
throw new MsalClientException("The endpoint URI is not specified",
AuthenticationErrorCode.INVALID_ENDPOINT_URI);
}

final OAuthHttpRequest oauthHttpRequest = new OAuthHttpRequest(
Expand Down Expand Up @@ -109,27 +107,18 @@ private void addQueryParameters(OAuthHttpRequest oauthHttpRequest) {

private void addJWTBearerAssertionParams(Map<String, String> queryParameters, String assertion) {
queryParameters.put("client_assertion", assertion);
queryParameters.put("client_assertion_type", "urn:ietf:params:oauth:client-assertion-type:jwt-bearer");
queryParameters.put("client_assertion_type", ClientAssertion.ASSERTION_TYPE_JWT_BEARER);
}

private AuthenticationResult createAuthenticationResultFromOauthHttpResponse(
HttpResponse oauthHttpResponse) throws ParseException {
private AuthenticationResult createAuthenticationResultFromOauthHttpResponse(HttpResponse oauthHttpResponse) {
AuthenticationResult result;

if (oauthHttpResponse.statusCode() == HttpHelper.HTTP_STATUS_200) {
final TokenResponse response = TokenResponse.parseHttpResponse(oauthHttpResponse);

AccountCacheEntity accountCacheEntity = null;
if (!StringHelper.isNullOrBlank(response.idToken())) {
String idTokenJson;
try {
idTokenJson = new String(Base64.getUrlDecoder().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);
}

IdToken idToken = JsonHelper.convertJsonToObject(idTokenJson, IdToken.class);
IdToken idToken = JsonHelper.createIdTokenFromEncodedTokenString(response.idToken());

AuthorityType type = msalRequest.application().authenticationAuthority.authorityType;
if (!StringHelper.isBlank(response.getClientInfo())) {
Expand Down
Loading