Skip to content
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

fix/MNT-24542 class cast exception #3092

Merged
merged 6 commits into from
Jan 9, 2025

Conversation

code4uuuu
Copy link
Contributor

@code4uuuu code4uuuu commented Dec 16, 2024

https://hyland.atlassian.net/browse/MNT-24542

Here in this ticket. We are encountering an issue when running ACS with Keycloak configured without the OpenID scope. Upon running the sample SDK with the attached files and executing the generated JAR, we encounter a ClassCastException.

The root cause appears to be that while a UserInfoResponse can be successfully cast to a UserInfoSuccessResponse, it cannot be cast to a UserInfoErrorResponse. This issue occurs in the map, where such an invalid cast is attempted.

To address this, I have implemented a fix. The updated logic first retrieves the UserInfoResponse and checks if it is an instance of UserInfoErrorResponse. If it is, the error message is extracted, and a UserInfoException is thrown. Otherwise, the response is returned as-is.

@code4uuuu code4uuuu changed the title Fix/mnt 24542 class cast exception fix/MNT-24542 class cast exception Dec 16, 2024
Comment on lines 27 to 355
{
final DefaultRefreshTokenTokenResponseClient client = new DefaultRefreshTokenTokenResponseClient();
client.setRestOperations(rest);
return client;
}

private static OAuth2AccessTokenResponseClient<OAuth2PasswordGrantRequest> createPasswordClient(RestOperations rest,
ClientRegistration clientRegistration)
{
final DefaultPasswordTokenResponseClient client = new DefaultPasswordTokenResponseClient();
client.setRestOperations(rest);
Optional.of(clientRegistration)
.map(ClientRegistration::getProviderDetails)
.map(ProviderDetails::getConfigurationMetadata)
.map(metadata -> metadata.get(AUDIENCE.getValue()))
.filter(String.class::isInstance)
.map(String.class::cast)
.ifPresent(audienceValue -> {
final OAuth2PasswordGrantRequestEntityConverter requestEntityConverter = new OAuth2PasswordGrantRequestEntityConverter();
requestEntityConverter.addParametersConverter(audienceParameterConverter(audienceValue));
client.setRequestEntityConverter(requestEntityConverter);
});
return client;
}

private static Converter<OAuth2PasswordGrantRequest, MultiValueMap<String, String>> audienceParameterConverter(
String audienceValue)
{
return (grantRequest) -> {
MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();
parameters.set("audience", audienceValue);

return parameters;
};
}

private static class SpringAccessTokenAuthorization implements AccessTokenAuthorization
{
private final OAuth2AccessTokenResponse tokenResponse;

private SpringAccessTokenAuthorization(OAuth2AccessTokenResponse tokenResponse)
{
this.tokenResponse = requireNonNull(tokenResponse);
}

@Override
public AccessToken getAccessToken()
{
return new SpringAccessToken(tokenResponse.getAccessToken());
}

@Override
public String getRefreshTokenValue()
{
return Optional.of(tokenResponse)
.map(OAuth2AccessTokenResponse::getRefreshToken)
.map(AbstractOAuth2Token::getTokenValue)
.orElse(null);
}
}

private static class SpringAccessToken implements AccessToken
{
private final AbstractOAuth2Token token;

private SpringAccessToken(AbstractOAuth2Token token)
{
this.token = requireNonNull(token);
}

@Override
public String getTokenValue()
{
return token.getTokenValue();
}

@Override
public Instant getExpiresAt()
{
return token.getExpiresAt();
}
}

private static class SpringDecodedAccessToken extends SpringAccessToken implements DecodedAccessToken
{
private final Jwt jwt;

private SpringDecodedAccessToken(Jwt jwt)
{
super(jwt);
this.jwt = jwt;
}

@Override
public Object getClaim(String claim)
{
return jwt.getClaim(claim);
}
}
}

Check warning

Code scanning / PMD

High amount of different objects as members denotes a high coupling Warning

High amount of different objects as members denotes a high coupling

class SpringBasedIdentityServiceFacade implements IdentityServiceFacade
{
private static final Log LOGGER = LogFactory.getLog(SpringBasedIdentityServiceFacade.class);

Check warning

Code scanning / PMD

Logger should be defined private static final and have the correct class Warning

Logger should be defined private static final and have the correct class
class SpringBasedIdentityServiceFacade implements IdentityServiceFacade
{
private static final Log LOGGER = LogFactory.getLog(SpringBasedIdentityServiceFacade.class);
private static final Instant SOME_INSIGNIFICANT_DATE_IN_THE_PAST = Instant.MIN.plusSeconds(12345);

Check warning

Code scanning / PMD

Avoid excessively long variable names like SOME_INSIGNIFICANT_DATE_IN_THE_PAST Warning

Avoid excessively long variable names like SOME_INSIGNIFICANT_DATE_IN_THE_PAST
}
catch (OAuth2AuthorizationException e)
{
LOGGER.debug("Failed to authorize against Authorization Server. Reason: " + e.getError() + ".");

Check warning

Code scanning / PMD

Logger calls should be surrounded by log level guards. Warning

Logger calls should be surrounded by log level guards.
Comment on lines 146 to 158
UserInfoResponse userInfoResponse = UserInfoResponse.parse(httpResponse);

if (userInfoResponse instanceof UserInfoErrorResponse)
{
UserInfoErrorResponse userInfoErrorResponse = (UserInfoErrorResponse) userInfoResponse;
String errorMessage = userInfoErrorResponse.getErrorObject().getDescription();
LOGGER.warn("User Info Request failed: " + errorMessage);
throw new UserInfoException(errorMessage);
}
else
{
return Optional.of(userInfoResponse);
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I have made the following changes for the fix. First we are getting the userInfoResponse after that checking it whether it is instance of UserInfoErrorResponse, if it is then we are extracting errorMessage and throwing UserInfoException otherwise returning as it is

Copy link
Contributor

@SathishK-T SathishK-T left a comment

Choose a reason for hiding this comment

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

LGTM

Copy link
Contributor

@kavitshah-gl kavitshah-gl left a comment

Choose a reason for hiding this comment

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

Need to revert the change, only the part where we are making the changes needs to be formatted. It is really difficult to know what is changed if we just format the whole java file.

@code4uuuu
Copy link
Contributor Author

Need to revert the change, only the part where we are making the changes needs

Agreed but If we will not format the whole java file then our pre-commit stage is failing

Copy link
Contributor

@kavitshah-gl kavitshah-gl left a comment

Choose a reason for hiding this comment

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

Kindly make the changes accordingly.
Also look to resolve the PMD issues.

LOGGER.warn("User Info Request failed: " + errorMessage);
throw new UserInfoException(errorMessage);
}
else
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think we need else here, if the exception is not thrown then by default it will come to this line.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done. Removed the else part

@code4uuuu
Copy link
Contributor Author

Kindly make the changes accordingly.
[Remarks] I have done the changes for the same

Also look to resolve the PMD issues.
[Remarks] These are the PMD warning not the issues, our PMD scan stage already passed in the github action.

Copy link
Contributor

@kavitshah-gl kavitshah-gl left a comment

Choose a reason for hiding this comment

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

Kindly look into the comment for change requested

UserInfoErrorResponse userInfoErrorResponse = (UserInfoErrorResponse) userInfoResponse;
String errorMessage = userInfoErrorResponse.getErrorObject().getDescription();
LOGGER.warn("User Info Request failed: " + errorMessage);
throw new UserInfoException(errorMessage);
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we need to throw the error exception? Can't we extract the error message and use the LOGGER.warn to show the error and then return the empty Optional value as done for ParseException?

Copy link
Contributor

Choose a reason for hiding this comment

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

I agree with @kavitshah-gl .

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Do we need to throw the error exception? Can't we extract the error message and use the LOGGER.warn to show the error and then return the empty Optional value as done for ParseException?

If we send the empty response then the Client SDK is not receiving the error at all which does not help users. Further if we continue with the UserInfoErrorResponse further to the map method then it gives ClassCastException. This is happening since UserInfoResponse can be converted to UserInfoSuccessResponse but UserInfoErrorResponse can't ne converted to UserInfoSuccessResponse and in the map it tries to do that . Thus rather than Spring throw the ClassCastException we are throwing UserInfoException which is of type IdentityServiceFacadeException which is caught gracefully in authenticateImpl method.

Copy link
Contributor

Choose a reason for hiding this comment

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

Okay I think throwing UserInfoException is fine since we are catching it.

Comment on lines 148 to 151
if (userInfoResponse instanceof UserInfoErrorResponse)
{
UserInfoErrorResponse userInfoErrorResponse = (UserInfoErrorResponse) userInfoResponse;
String errorMessage = userInfoErrorResponse.getErrorObject().getDescription();
Copy link
Contributor

Choose a reason for hiding this comment

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

In general, it looks good to me.
Please replace UserInfoErrorResponse userInfoErrorResponse = (UserInfoErrorResponse) userInfoResponse;
with pattern variable:
if (userInfoResponse instanceof UserInfoErrorResponse userInfoErrorResponse).
Moreover, I think we can check if userInfoErrorResponse.getErrorObject() is null.
For example:

String errorMessage = Optional.ofNullable(userInfoErrorResponse.getErrorObject())
                                            .map(ErrorObject::getDescription)
                                            .orElse("No error description found");

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I have addressed the following changes

Copy link
Contributor

@kavitshah-gl kavitshah-gl left a comment

Choose a reason for hiding this comment

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

Approved.

UserInfoErrorResponse userInfoErrorResponse = (UserInfoErrorResponse) userInfoResponse;
String errorMessage = userInfoErrorResponse.getErrorObject().getDescription();
LOGGER.warn("User Info Request failed: " + errorMessage);
throw new UserInfoException(errorMessage);
Copy link
Contributor

Choose a reason for hiding this comment

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

Okay I think throwing UserInfoException is fine since we are catching it.

Copy link
Contributor

@purusothaman-mm purusothaman-mm left a comment

Choose a reason for hiding this comment

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

LGTM

…ty-repo into fix/MNT-24542-Class-Cast-Exception

# Conflicts:
#	repository/src/main/java/org/alfresco/repo/security/authentication/identityservice/SpringBasedIdentityServiceFacade.java
@code4uuuu code4uuuu merged commit 38b796f into master Jan 9, 2025
89 checks passed
@code4uuuu code4uuuu deleted the fix/MNT-24542-Class-Cast-Exception branch January 9, 2025 08:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants