From 65a14d6c6de0d885dffceeb587c743a900f74de1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Deleuze?= Date: Mon, 1 Sep 2025 18:23:31 +0200 Subject: [PATCH] Add Jackson 3 support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit adds support for Jackson 3 which has the following major differences with the Jackson 2 one: - jackson subpackage instead of jackson2 - Jackson type prefix instead of Jackson2 - JsonMapper instead of ObjectMapper - For configuration, JsonMapper.Builder instead of ObjectMapper since the latter is now immutable - Remove custom support for unmodifiable collections - Use safe default typing via a PolymorphicTypeValidator Jackson 3 changes compared to Jackson 2 are documented in https://cowtowncoder.medium.com/jackson-3-0-0-ga-released-1f669cda529a and https://github.com/FasterXML/jackson/blob/main/jackson3/MIGRATING_TO_JACKSON_3.md. This commit does not cover webauthn which is a special case (uses jackson sub-package for Jackson 2 support) which will be handled in a distinct commit. See gh-17832 Signed-off-by: Sébastien Deleuze --- cas/spring-security-cas.gradle | 1 + .../cas/jackson/AssertionImplMixin.java | 60 +++ .../jackson/AttributePrincipalImplMixin.java | 59 +++ .../jackson/CasAuthenticationTokenMixin.java | 69 +++ .../cas/jackson/CasJacksonModule.java | 71 ++++ .../security/cas/jackson/package-info.java | 20 + .../security/cas/jackson2/package-info.java | 2 +- .../CasAuthenticationTokenMixinTests.java | 151 +++++++ .../server/authorization/JwkSetTests.java | 7 +- .../OAuth2AuthorizationCodeGrantTests.java | 7 +- .../OAuth2ClientCredentialsGrantTests.java | 7 +- .../OAuth2RefreshTokenGrantTests.java | 7 +- .../OAuth2TokenIntrospectionTests.java | 7 +- .../OAuth2TokenRevocationTests.java | 7 +- .../server/authorization/OidcTests.java | 7 +- core/spring-security-core.gradle | 1 + .../AnonymousAuthenticationTokenMixin.java | 57 +++ .../jackson/BadCredentialsExceptionMixin.java | 46 ++ .../security/jackson/CoreJacksonModule.java | 113 +++++ .../jackson/FactorGrantedAuthorityMixin.java | 50 +++ .../RememberMeAuthenticationTokenMixin.java | 60 +++ .../jackson/SecurityJacksonModule.java | 42 ++ .../jackson/SecurityJacksonModules.java | 203 +++++++++ .../jackson/SimpleGrantedAuthorityMixin.java | 47 +++ .../security/jackson/UserDeserializer.java | 81 ++++ .../security/jackson/UserMixin.java | 41 ++ ...sswordAuthenticationTokenDeserializer.java | 105 +++++ ...rnamePasswordAuthenticationTokenMixin.java | 41 ++ .../security/jackson/package-info.java | 23 + ...sswordAuthenticationTokenDeserializer.java | 2 +- .../security/jackson2/package-info.java | 5 +- ...AuthorizationAdvisorProxyFactoryTests.java | 11 +- .../security/jackson/AbstractMixinTests.java | 51 +++ ...nonymousAuthenticationTokenMixinTests.java | 83 ++++ .../BadCredentialsExceptionMixinTests.java | 60 +++ .../FactorGrantedAuthorityMixinTests.java | 60 +++ ...memberMeAuthenticationTokenMixinTests.java | 128 ++++++ .../jackson/SecurityContextMixinTests.java | 69 +++ .../jackson/SecurityJacksonModulesTests.java | 85 ++++ .../SimpleGrantedAuthorityMixinTests.java | 67 +++ .../jackson/UnmodifiableMapTests.java | 54 +++ .../jackson/UserDeserializerTests.java | 129 ++++++ ...PasswordAuthenticationTokenMixinTests.java | 221 ++++++++++ .../pages/features/integrations/jackson.adoc | 26 +- .../pages/servlet/integrations/jackson.adoc | 18 +- ldap/spring-security-ldap.gradle | 1 + .../ldap/jackson/InetOrgPersonMixin.java | 38 ++ .../ldap/jackson/LdapAuthorityMixin.java | 47 +++ .../ldap/jackson/LdapJacksonModule.java | 71 ++++ .../jackson/LdapUserDetailsImplMixin.java | 38 ++ .../security/ldap/jackson/PersonMixin.java | 38 ++ .../security/ldap/jackson/package-info.java | 20 + .../security/ldap/jackson2/package-info.java | 20 + .../ldap/jackson/InetOrgPersonMixinTests.java | 194 +++++++++ .../LdapUserDetailsImplMixinTests.java | 124 ++++++ .../ldap/jackson/PersonMixinTests.java | 137 ++++++ ...ecurity-oauth2-authorization-server.gradle | 3 +- .../JdbcOAuth2AuthorizationService.java | 115 +++-- .../authorization/jackson/JsonNodeUtils.java | 66 +++ .../jackson/JwsAlgorithmMixin.java | 36 ++ ...Auth2AuthorizationRequestDeserializer.java | 77 ++++ .../OAuth2AuthorizationRequestMixin.java | 40 ++ ...Auth2AuthorizationServerJacksonModule.java | 95 +++++ .../OAuth2TokenExchangeActorMixin.java | 44 ++ ...angeCompositeAuthenticationTokenMixin.java | 47 +++ .../jackson/OAuth2TokenFormatMixin.java | 42 ++ .../JdbcOAuth2AuthorizationServiceTests.java | 6 +- .../JdbcRegisteredClientRepositoryTests.java | 1 + ...AuthorizationServerJacksonModuleTests.java | 112 +++++ .../TestingAuthenticationTokenMixin.java | 49 +++ .../spring-security-oauth2-client.gradle | 1 + .../ClientRegistrationDeserializer.java | 76 ++++ .../jackson/ClientRegistrationMixin.java | 42 ++ .../jackson/DefaultOAuth2UserMixin.java | 50 +++ .../client/jackson/DefaultOidcUserMixin.java | 53 +++ .../oauth2/client/jackson/JsonNodeUtils.java | 67 +++ .../jackson/OAuth2AccessTokenMixin.java | 52 +++ .../OAuth2AuthenticationExceptionMixin.java | 56 +++ .../OAuth2AuthenticationTokenMixin.java | 52 +++ ...Auth2AuthorizationRequestDeserializer.java | 72 ++++ .../OAuth2AuthorizationRequestMixin.java | 42 ++ .../jackson/OAuth2AuthorizedClientMixin.java | 50 +++ .../jackson/OAuth2ClientJacksonModule.java | 118 ++++++ .../client/jackson/OAuth2ErrorMixin.java | 47 +++ .../jackson/OAuth2RefreshTokenMixin.java | 46 ++ .../jackson/OAuth2UserAuthorityMixin.java | 47 +++ .../client/jackson/OidcIdTokenMixin.java | 48 +++ .../jackson/OidcUserAuthorityMixin.java | 49 +++ .../client/jackson/OidcUserInfoMixin.java | 46 ++ .../oauth2/client/jackson/StdConverters.java | 94 +++++ .../oauth2/client/jackson/package-info.java | 20 + .../oauth2/client/jackson2/package-info.java | 20 + ...uth2AuthenticationExceptionMixinTests.java | 129 ++++++ .../OAuth2AuthenticationTokenMixinTests.java | 339 +++++++++++++++ .../OAuth2AuthorizationRequestMixinTests.java | 199 +++++++++ .../OAuth2AuthorizedClientMixinTests.java | 395 ++++++++++++++++++ .../client/jackson/StdConvertersTests.java | 53 +++ .../ClientRegistrationsTests.java | 6 +- .../spring-security-oauth2-jose.gradle | 2 +- .../security/oauth2/jwt/JwtDecodersTests.java | 19 +- ...ecoderProviderConfigurationUtilsTests.java | 16 +- .../oauth2/jwt/ReactiveJwtDecodersTests.java | 19 +- ...ing-security-oauth2-resource-server.gradle | 2 +- ...OAuth2ProtectedResourceMetadataFilter.java | 1 + ...gReactiveOpaqueTokenIntrospectorTests.java | 4 +- ...ing-security-saml2-service-provider.gradle | 1 + ...faultSaml2AuthenticatedPrincipalMixin.java | 52 +++ .../Saml2AssertionAuthenticationMixin.java | 53 +++ .../Saml2AuthenticationExceptionMixin.java | 54 +++ .../jackson/Saml2AuthenticationMixin.java | 52 +++ .../saml2/jackson/Saml2ErrorMixin.java | 44 ++ .../saml2/jackson/Saml2JacksonModule.java | 91 ++++ .../jackson/Saml2LogoutRequestMixin.java | 55 +++ .../Saml2PostAuthenticationRequestMixin.java | 49 +++ ...ml2RedirectAuthenticationRequestMixin.java | 50 +++ ...leSaml2ResponseAssertionAccessorMixin.java | 52 +++ .../security/saml2/jackson/package-info.java | 20 + .../security/saml2/jackson2/package-info.java | 20 + .../OpenSaml5AuthenticationProviderTests.java | 7 +- ...Saml2AuthenticatedPrincipalMixinTests.java | 105 +++++ ...aml2AuthenticationExceptionMixinTests.java | 61 +++ .../Saml2AuthenticationMixinTests.java | 64 +++ .../jackson/Saml2LogoutRequestMixinTests.java | 90 ++++ ...l2PostAuthenticationRequestMixinTests.java | 97 +++++ ...directAuthenticationRequestMixinTests.java | 84 ++++ .../saml2/jackson/TestSaml2JsonPayloads.java | 257 ++++++++++++ web/spring-security-web.gradle | 1 + .../web/jackson/CookieDeserializer.java | 65 +++ .../security/web/jackson/CookieMixin.java | 37 ++ .../web/jackson/DefaultCsrfTokenMixin.java | 48 +++ .../web/jackson/DefaultSavedRequestMixin.java | 45 ++ ...icatedAuthenticationTokenDeserializer.java | 79 ++++ ...AuthenticatedAuthenticationTokenMixin.java | 43 ++ .../web/jackson/SavedCookieMixin.java | 45 ++ .../SwitchUserGrantedAuthorityMixIn.java | 46 ++ .../WebAuthenticationDetailsMixin.java | 44 ++ .../web/jackson/WebJacksonModule.java | 76 ++++ .../web/jackson/WebServletJacksonModule.java | 73 ++++ .../security/web/jackson/package-info.java | 20 + .../security/web/jackson2/package-info.java | 5 +- .../jackson/DefaultCsrfServerTokenMixin.java | 48 +++ .../jackson/WebServerJacksonModule.java | 65 +++ .../web/server/jackson/package-info.java | 23 + .../web/server/jackson2/package-info.java | 2 +- .../web/jackson/AbstractMixinTests.java | 38 ++ .../web/jackson/CookieMixinTests.java | 95 +++++ .../jackson/DefaultCsrfTokenMixinTests.java | 73 ++++ .../DefaultSavedRequestMixinTests.java | 172 ++++++++ ...nticatedAuthenticationTokenMixinTests.java | 70 ++++ .../web/jackson/SavedCookieMixinTests.java | 98 +++++ .../SwitchUserGrantedAuthorityMixInTests.java | 82 ++++ .../WebAuthenticationDetailsMixinTests.java | 81 ++++ .../DefaultCsrfServerTokenMixinTests.java | 76 ++++ .../AuthenticatorAttachmentDeserializer.java | 12 +- .../AuthenticatorTransportDeserializer.java | 12 +- .../COSEAlgorithmIdentifierDeserializer.java | 12 +- 156 files changed, 9052 insertions(+), 146 deletions(-) create mode 100644 cas/src/main/java/org/springframework/security/cas/jackson/AssertionImplMixin.java create mode 100644 cas/src/main/java/org/springframework/security/cas/jackson/AttributePrincipalImplMixin.java create mode 100644 cas/src/main/java/org/springframework/security/cas/jackson/CasAuthenticationTokenMixin.java create mode 100644 cas/src/main/java/org/springframework/security/cas/jackson/CasJacksonModule.java create mode 100644 cas/src/main/java/org/springframework/security/cas/jackson/package-info.java create mode 100644 cas/src/test/java/org/springframework/security/cas/jackson/CasAuthenticationTokenMixinTests.java create mode 100644 core/src/main/java/org/springframework/security/jackson/AnonymousAuthenticationTokenMixin.java create mode 100644 core/src/main/java/org/springframework/security/jackson/BadCredentialsExceptionMixin.java create mode 100644 core/src/main/java/org/springframework/security/jackson/CoreJacksonModule.java create mode 100644 core/src/main/java/org/springframework/security/jackson/FactorGrantedAuthorityMixin.java create mode 100644 core/src/main/java/org/springframework/security/jackson/RememberMeAuthenticationTokenMixin.java create mode 100644 core/src/main/java/org/springframework/security/jackson/SecurityJacksonModule.java create mode 100644 core/src/main/java/org/springframework/security/jackson/SecurityJacksonModules.java create mode 100644 core/src/main/java/org/springframework/security/jackson/SimpleGrantedAuthorityMixin.java create mode 100644 core/src/main/java/org/springframework/security/jackson/UserDeserializer.java create mode 100644 core/src/main/java/org/springframework/security/jackson/UserMixin.java create mode 100644 core/src/main/java/org/springframework/security/jackson/UsernamePasswordAuthenticationTokenDeserializer.java create mode 100644 core/src/main/java/org/springframework/security/jackson/UsernamePasswordAuthenticationTokenMixin.java create mode 100644 core/src/main/java/org/springframework/security/jackson/package-info.java create mode 100644 core/src/test/java/org/springframework/security/jackson/AbstractMixinTests.java create mode 100644 core/src/test/java/org/springframework/security/jackson/AnonymousAuthenticationTokenMixinTests.java create mode 100644 core/src/test/java/org/springframework/security/jackson/BadCredentialsExceptionMixinTests.java create mode 100644 core/src/test/java/org/springframework/security/jackson/FactorGrantedAuthorityMixinTests.java create mode 100644 core/src/test/java/org/springframework/security/jackson/RememberMeAuthenticationTokenMixinTests.java create mode 100644 core/src/test/java/org/springframework/security/jackson/SecurityContextMixinTests.java create mode 100644 core/src/test/java/org/springframework/security/jackson/SecurityJacksonModulesTests.java create mode 100644 core/src/test/java/org/springframework/security/jackson/SimpleGrantedAuthorityMixinTests.java create mode 100644 core/src/test/java/org/springframework/security/jackson/UnmodifiableMapTests.java create mode 100644 core/src/test/java/org/springframework/security/jackson/UserDeserializerTests.java create mode 100644 core/src/test/java/org/springframework/security/jackson/UsernamePasswordAuthenticationTokenMixinTests.java create mode 100644 ldap/src/main/java/org/springframework/security/ldap/jackson/InetOrgPersonMixin.java create mode 100644 ldap/src/main/java/org/springframework/security/ldap/jackson/LdapAuthorityMixin.java create mode 100644 ldap/src/main/java/org/springframework/security/ldap/jackson/LdapJacksonModule.java create mode 100644 ldap/src/main/java/org/springframework/security/ldap/jackson/LdapUserDetailsImplMixin.java create mode 100644 ldap/src/main/java/org/springframework/security/ldap/jackson/PersonMixin.java create mode 100644 ldap/src/main/java/org/springframework/security/ldap/jackson/package-info.java create mode 100644 ldap/src/main/java/org/springframework/security/ldap/jackson2/package-info.java create mode 100644 ldap/src/test/java/org/springframework/security/ldap/jackson/InetOrgPersonMixinTests.java create mode 100644 ldap/src/test/java/org/springframework/security/ldap/jackson/LdapUserDetailsImplMixinTests.java create mode 100644 ldap/src/test/java/org/springframework/security/ldap/jackson/PersonMixinTests.java create mode 100644 oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/jackson/JsonNodeUtils.java create mode 100644 oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/jackson/JwsAlgorithmMixin.java create mode 100644 oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/jackson/OAuth2AuthorizationRequestDeserializer.java create mode 100644 oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/jackson/OAuth2AuthorizationRequestMixin.java create mode 100644 oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/jackson/OAuth2AuthorizationServerJacksonModule.java create mode 100644 oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/jackson/OAuth2TokenExchangeActorMixin.java create mode 100644 oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/jackson/OAuth2TokenExchangeCompositeAuthenticationTokenMixin.java create mode 100644 oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/jackson/OAuth2TokenFormatMixin.java create mode 100644 oauth2/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/jackson/OAuth2AuthorizationServerJacksonModuleTests.java create mode 100644 oauth2/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/jackson/TestingAuthenticationTokenMixin.java create mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson/ClientRegistrationDeserializer.java create mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson/ClientRegistrationMixin.java create mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson/DefaultOAuth2UserMixin.java create mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson/DefaultOidcUserMixin.java create mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson/JsonNodeUtils.java create mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson/OAuth2AccessTokenMixin.java create mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson/OAuth2AuthenticationExceptionMixin.java create mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson/OAuth2AuthenticationTokenMixin.java create mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson/OAuth2AuthorizationRequestDeserializer.java create mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson/OAuth2AuthorizationRequestMixin.java create mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson/OAuth2AuthorizedClientMixin.java create mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson/OAuth2ClientJacksonModule.java create mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson/OAuth2ErrorMixin.java create mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson/OAuth2RefreshTokenMixin.java create mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson/OAuth2UserAuthorityMixin.java create mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson/OidcIdTokenMixin.java create mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson/OidcUserAuthorityMixin.java create mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson/OidcUserInfoMixin.java create mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson/StdConverters.java create mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson/package-info.java create mode 100644 oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/package-info.java create mode 100644 oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/jackson/OAuth2AuthenticationExceptionMixinTests.java create mode 100644 oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/jackson/OAuth2AuthenticationTokenMixinTests.java create mode 100644 oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/jackson/OAuth2AuthorizationRequestMixinTests.java create mode 100644 oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/jackson/OAuth2AuthorizedClientMixinTests.java create mode 100644 oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/jackson/StdConvertersTests.java create mode 100644 saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/jackson/DefaultSaml2AuthenticatedPrincipalMixin.java create mode 100644 saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/jackson/Saml2AssertionAuthenticationMixin.java create mode 100644 saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/jackson/Saml2AuthenticationExceptionMixin.java create mode 100644 saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/jackson/Saml2AuthenticationMixin.java create mode 100644 saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/jackson/Saml2ErrorMixin.java create mode 100644 saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/jackson/Saml2JacksonModule.java create mode 100644 saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/jackson/Saml2LogoutRequestMixin.java create mode 100644 saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/jackson/Saml2PostAuthenticationRequestMixin.java create mode 100644 saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/jackson/Saml2RedirectAuthenticationRequestMixin.java create mode 100644 saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/jackson/SimpleSaml2ResponseAssertionAccessorMixin.java create mode 100644 saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/jackson/package-info.java create mode 100644 saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/jackson2/package-info.java create mode 100644 saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/jackson/DefaultSaml2AuthenticatedPrincipalMixinTests.java create mode 100644 saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/jackson/Saml2AuthenticationExceptionMixinTests.java create mode 100644 saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/jackson/Saml2AuthenticationMixinTests.java create mode 100644 saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/jackson/Saml2LogoutRequestMixinTests.java create mode 100644 saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/jackson/Saml2PostAuthenticationRequestMixinTests.java create mode 100644 saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/jackson/Saml2RedirectAuthenticationRequestMixinTests.java create mode 100644 saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/jackson/TestSaml2JsonPayloads.java create mode 100644 web/src/main/java/org/springframework/security/web/jackson/CookieDeserializer.java create mode 100644 web/src/main/java/org/springframework/security/web/jackson/CookieMixin.java create mode 100644 web/src/main/java/org/springframework/security/web/jackson/DefaultCsrfTokenMixin.java create mode 100644 web/src/main/java/org/springframework/security/web/jackson/DefaultSavedRequestMixin.java create mode 100644 web/src/main/java/org/springframework/security/web/jackson/PreAuthenticatedAuthenticationTokenDeserializer.java create mode 100644 web/src/main/java/org/springframework/security/web/jackson/PreAuthenticatedAuthenticationTokenMixin.java create mode 100644 web/src/main/java/org/springframework/security/web/jackson/SavedCookieMixin.java create mode 100644 web/src/main/java/org/springframework/security/web/jackson/SwitchUserGrantedAuthorityMixIn.java create mode 100644 web/src/main/java/org/springframework/security/web/jackson/WebAuthenticationDetailsMixin.java create mode 100644 web/src/main/java/org/springframework/security/web/jackson/WebJacksonModule.java create mode 100644 web/src/main/java/org/springframework/security/web/jackson/WebServletJacksonModule.java create mode 100644 web/src/main/java/org/springframework/security/web/jackson/package-info.java create mode 100644 web/src/main/java/org/springframework/security/web/server/jackson/DefaultCsrfServerTokenMixin.java create mode 100644 web/src/main/java/org/springframework/security/web/server/jackson/WebServerJacksonModule.java create mode 100644 web/src/main/java/org/springframework/security/web/server/jackson/package-info.java create mode 100644 web/src/test/java/org/springframework/security/web/jackson/AbstractMixinTests.java create mode 100644 web/src/test/java/org/springframework/security/web/jackson/CookieMixinTests.java create mode 100644 web/src/test/java/org/springframework/security/web/jackson/DefaultCsrfTokenMixinTests.java create mode 100644 web/src/test/java/org/springframework/security/web/jackson/DefaultSavedRequestMixinTests.java create mode 100644 web/src/test/java/org/springframework/security/web/jackson/PreAuthenticatedAuthenticationTokenMixinTests.java create mode 100644 web/src/test/java/org/springframework/security/web/jackson/SavedCookieMixinTests.java create mode 100644 web/src/test/java/org/springframework/security/web/jackson/SwitchUserGrantedAuthorityMixInTests.java create mode 100644 web/src/test/java/org/springframework/security/web/jackson/WebAuthenticationDetailsMixinTests.java create mode 100644 web/src/test/java/org/springframework/security/web/server/jackson/DefaultCsrfServerTokenMixinTests.java diff --git a/cas/spring-security-cas.gradle b/cas/spring-security-cas.gradle index a475013057..23c5f04a56 100644 --- a/cas/spring-security-cas.gradle +++ b/cas/spring-security-cas.gradle @@ -15,6 +15,7 @@ dependencies { api 'org.springframework:spring-web' optional 'com.fasterxml.jackson.core:jackson-databind' + optional 'tools.jackson.core:jackson-databind' provided 'jakarta.servlet:jakarta.servlet-api' diff --git a/cas/src/main/java/org/springframework/security/cas/jackson/AssertionImplMixin.java b/cas/src/main/java/org/springframework/security/cas/jackson/AssertionImplMixin.java new file mode 100644 index 0000000000..6191108cb1 --- /dev/null +++ b/cas/src/main/java/org/springframework/security/cas/jackson/AssertionImplMixin.java @@ -0,0 +1,60 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.cas.jackson; + +import java.util.Date; +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import org.apereo.cas.client.authentication.AttributePrincipal; + +/** + * Helps in jackson deserialization of class + * {@link org.apereo.cas.client.validation.AssertionImpl}, which is used with + * {@link org.springframework.security.cas.authentication.CasAuthenticationToken}. + * + * @author Sebastien Deleuze + * @author Jitendra Singh + * @since 7.0 + * @see CasJacksonModule + * @see org.springframework.security.jackson.SecurityJacksonModules + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) +@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE, + isGetterVisibility = JsonAutoDetect.Visibility.NONE) +class AssertionImplMixin { + + /** + * Mixin Constructor helps in deserialize + * {@link org.apereo.cas.client.validation.AssertionImpl} + * @param principal the Principal to associate with the Assertion. + * @param validFromDate when the assertion is valid from. + * @param validUntilDate when the assertion is valid to. + * @param authenticationDate when the assertion is authenticated. + * @param attributes the key/value pairs for this attribute. + */ + @JsonCreator + AssertionImplMixin(@JsonProperty("principal") AttributePrincipal principal, + @JsonProperty("validFromDate") Date validFromDate, @JsonProperty("validUntilDate") Date validUntilDate, + @JsonProperty("authenticationDate") Date authenticationDate, + @JsonProperty("attributes") Map attributes) { + } + +} diff --git a/cas/src/main/java/org/springframework/security/cas/jackson/AttributePrincipalImplMixin.java b/cas/src/main/java/org/springframework/security/cas/jackson/AttributePrincipalImplMixin.java new file mode 100644 index 0000000000..f5d88a6051 --- /dev/null +++ b/cas/src/main/java/org/springframework/security/cas/jackson/AttributePrincipalImplMixin.java @@ -0,0 +1,59 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.cas.jackson; + +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import org.apereo.cas.client.proxy.ProxyRetriever; + +/** + * Helps in deserialize + * {@link org.apereo.cas.client.authentication.AttributePrincipalImpl} which is used with + * {@link org.springframework.security.cas.authentication.CasAuthenticationToken}. + * + * @author Sebastien Deleuze + * @author Jitendra Singh + * @since 7.0 + * @see CasJacksonModule + * @see org.springframework.security.jackson.SecurityJacksonModules + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) +@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE, + isGetterVisibility = JsonAutoDetect.Visibility.NONE) +class AttributePrincipalImplMixin { + + /** + * Mixin Constructor helps in deserialize + * {@link org.apereo.cas.client.authentication.AttributePrincipalImpl} + * @param name the unique identifier for the principal. + * @param attributes the key/value pairs for this principal. + * @param proxyGrantingTicket the ticket associated with this principal. + * @param proxyRetriever the ProxyRetriever implementation to call back to the CAS + * server. + */ + @JsonCreator + AttributePrincipalImplMixin(@JsonProperty("name") String name, + @JsonProperty("attributes") Map attributes, + @JsonProperty("proxyGrantingTicket") String proxyGrantingTicket, + @JsonProperty("proxyRetriever") ProxyRetriever proxyRetriever) { + } + +} diff --git a/cas/src/main/java/org/springframework/security/cas/jackson/CasAuthenticationTokenMixin.java b/cas/src/main/java/org/springframework/security/cas/jackson/CasAuthenticationTokenMixin.java new file mode 100644 index 0000000000..adfc583810 --- /dev/null +++ b/cas/src/main/java/org/springframework/security/cas/jackson/CasAuthenticationTokenMixin.java @@ -0,0 +1,69 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.cas.jackson; + +import java.util.Collection; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import org.apereo.cas.client.validation.Assertion; + +import org.springframework.security.cas.authentication.CasAuthenticationProvider; +import org.springframework.security.cas.authentication.CasAuthenticationToken; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +/** + * Mixin class which helps in deserialize {@link CasAuthenticationToken} using jackson. + * + * @author Sebastien Deleuze + * @author Jitendra Singh + * @since 7.0 + * @see CasJacksonModule + * @see org.springframework.security.jackson.SecurityJacksonModules + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) +@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, isGetterVisibility = JsonAutoDetect.Visibility.NONE, + getterVisibility = JsonAutoDetect.Visibility.NONE, creatorVisibility = JsonAutoDetect.Visibility.ANY) +class CasAuthenticationTokenMixin { + + /** + * Mixin Constructor helps in deserialize {@link CasAuthenticationToken} + * @param keyHash hashCode of provided key to identify if this object made by a given + * {@link CasAuthenticationProvider} + * @param principal typically the UserDetails object (cannot be null) + * @param credentials the service/proxy ticket ID from CAS (cannot be + * null) + * @param authorities the authorities granted to the user (from the + * {@link org.springframework.security.core.userdetails.UserDetailsService}) (cannot + * be null) + * @param userDetails the user details (from the + * {@link org.springframework.security.core.userdetails.UserDetailsService}) (cannot + * be null) + * @param assertion the assertion returned from the CAS servers. It contains the + * principal and how to obtain a proxy ticket for the user. + */ + @JsonCreator + CasAuthenticationTokenMixin(@JsonProperty("keyHash") Integer keyHash, @JsonProperty("principal") Object principal, + @JsonProperty("credentials") Object credentials, + @JsonProperty("authorities") Collection authorities, + @JsonProperty("userDetails") UserDetails userDetails, @JsonProperty("assertion") Assertion assertion) { + } + +} diff --git a/cas/src/main/java/org/springframework/security/cas/jackson/CasJacksonModule.java b/cas/src/main/java/org/springframework/security/cas/jackson/CasJacksonModule.java new file mode 100644 index 0000000000..0e5e2cc4d1 --- /dev/null +++ b/cas/src/main/java/org/springframework/security/cas/jackson/CasJacksonModule.java @@ -0,0 +1,71 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.cas.jackson; + +import org.apereo.cas.client.authentication.AttributePrincipalImpl; +import org.apereo.cas.client.validation.AssertionImpl; +import tools.jackson.core.Version; +import tools.jackson.databind.jsontype.BasicPolymorphicTypeValidator; + +import org.springframework.security.cas.authentication.CasAuthenticationToken; +import org.springframework.security.jackson.SecurityJacksonModule; +import org.springframework.security.jackson.SecurityJacksonModules; + +/** + * Jackson module for spring-security-cas. This module register + * {@link AssertionImplMixin}, {@link AttributePrincipalImplMixin} and + * {@link CasAuthenticationTokenMixin}. If no default typing enabled by default then it'll + * enable it because typing info is needed to properly serialize/deserialize objects. In + * order to use this module just add this module into your JsonMapper configuration. + * + *

+ * The recommended way to configure it is to use {@link SecurityJacksonModules} in order + * to enable properly automatic inclusion of type information with related validation. + * + *

+ *     ClassLoader loader = getClass().getClassLoader();
+ *     JsonMapper mapper = JsonMapper.builder()
+ * 				.addModules(SecurityJacksonModules.getModules(loader))
+ * 				.build();
+ * 
+ * + * @author Sebastien Deleuze + * @author Jitendra Singh + * @since 7.0 + * @see SecurityJacksonModules + */ +public class CasJacksonModule extends SecurityJacksonModule { + + public CasJacksonModule() { + super(CasJacksonModule.class.getName(), new Version(1, 0, 0, null, null, null)); + } + + @Override + public void configurePolymorphicTypeValidator(BasicPolymorphicTypeValidator.Builder builder) { + builder.allowIfSubType(AssertionImpl.class) + .allowIfSubType(AttributePrincipalImpl.class) + .allowIfSubType(CasAuthenticationToken.class); + } + + @Override + public void setupModule(SetupContext context) { + context.setMixIn(AssertionImpl.class, AssertionImplMixin.class); + context.setMixIn(AttributePrincipalImpl.class, AttributePrincipalImplMixin.class); + context.setMixIn(CasAuthenticationToken.class, CasAuthenticationTokenMixin.class); + } + +} diff --git a/cas/src/main/java/org/springframework/security/cas/jackson/package-info.java b/cas/src/main/java/org/springframework/security/cas/jackson/package-info.java new file mode 100644 index 0000000000..261d47c9b5 --- /dev/null +++ b/cas/src/main/java/org/springframework/security/cas/jackson/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Jackson 3+ serialization support for CAS. + */ +package org.springframework.security.cas.jackson; diff --git a/cas/src/main/java/org/springframework/security/cas/jackson2/package-info.java b/cas/src/main/java/org/springframework/security/cas/jackson2/package-info.java index dae1c1502d..86000c1c2c 100644 --- a/cas/src/main/java/org/springframework/security/cas/jackson2/package-info.java +++ b/cas/src/main/java/org/springframework/security/cas/jackson2/package-info.java @@ -15,7 +15,7 @@ */ /** - * Jackson support for CAS. + * Jackson 2 support for CAS. */ @NullMarked package org.springframework.security.cas.jackson2; diff --git a/cas/src/test/java/org/springframework/security/cas/jackson/CasAuthenticationTokenMixinTests.java b/cas/src/test/java/org/springframework/security/cas/jackson/CasAuthenticationTokenMixinTests.java new file mode 100644 index 0000000000..9eefc0fd89 --- /dev/null +++ b/cas/src/test/java/org/springframework/security/cas/jackson/CasAuthenticationTokenMixinTests.java @@ -0,0 +1,151 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.cas.jackson; + +import java.io.IOException; +import java.util.Collection; +import java.util.Collections; +import java.util.Date; + +import org.apereo.cas.client.authentication.AttributePrincipalImpl; +import org.apereo.cas.client.validation.Assertion; +import org.apereo.cas.client.validation.AssertionImpl; +import org.json.JSONException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; +import tools.jackson.databind.json.JsonMapper; + +import org.springframework.security.cas.authentication.CasAuthenticationToken; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.jackson.SecurityJacksonModules; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Jitendra Singh + * @since 4.2 + */ +public class CasAuthenticationTokenMixinTests { + + private static final String KEY = "casKey"; + + private static final String PASSWORD = "\"1234\""; + + private static final Date START_DATE = new Date(); + + private static final Date END_DATE = new Date(); + + public static final String AUTHORITY_JSON = "{\"@class\": \"org.springframework.security.core.authority.SimpleGrantedAuthority\", \"authority\": \"ROLE_USER\"}"; + + public static final String AUTHORITIES_SET_JSON = "[\"java.util.Collections$UnmodifiableSet\", [" + AUTHORITY_JSON + + "]]"; + + public static final String AUTHORITIES_ARRAYLIST_JSON = "[\"java.util.Collections$UnmodifiableRandomAccessList\", [" + + AUTHORITY_JSON + "]]"; + + // @formatter:off + public static final String USER_JSON = "{" + + "\"@class\": \"org.springframework.security.core.userdetails.User\", " + + "\"username\": \"admin\"," + + " \"password\": " + PASSWORD + ", " + + "\"accountNonExpired\": true, " + + "\"accountNonLocked\": true, " + + "\"credentialsNonExpired\": true, " + + "\"enabled\": true, " + + "\"authorities\": " + AUTHORITIES_SET_JSON + + "}"; + // @formatter:on + private static final String CAS_TOKEN_JSON = "{" + + "\"@class\": \"org.springframework.security.cas.authentication.CasAuthenticationToken\", " + + "\"keyHash\": " + KEY.hashCode() + "," + "\"principal\": " + USER_JSON + ", " + "\"credentials\": " + + PASSWORD + ", " + "\"authorities\": " + AUTHORITIES_ARRAYLIST_JSON + "," + "\"userDetails\": " + USER_JSON + + "," + "\"authenticated\": true, " + "\"details\": null," + "\"assertion\": {" + + "\"@class\": \"org.apereo.cas.client.validation.AssertionImpl\", " + "\"principal\": {" + + "\"@class\": \"org.apereo.cas.client.authentication.AttributePrincipalImpl\", " + + "\"name\": \"assertName\", " + "\"attributes\": {\"@class\": \"java.util.Collections$EmptyMap\"}, " + + "\"proxyGrantingTicket\": null, " + "\"proxyRetriever\": null" + "}, " + + "\"validFromDate\": [\"java.util.Date\", " + START_DATE.getTime() + "], " + + "\"validUntilDate\": [\"java.util.Date\", " + END_DATE.getTime() + "]," + + "\"authenticationDate\": [\"java.util.Date\", " + START_DATE.getTime() + "], " + + "\"attributes\": {\"@class\": \"java.util.Collections$EmptyMap\"}," + + "\"context\": {\"@class\":\"java.util.HashMap\"}" + "}" + "}"; + + private static final String CAS_TOKEN_CLEARED_JSON = CAS_TOKEN_JSON.replaceFirst(PASSWORD, "null"); + + protected JsonMapper mapper; + + @BeforeEach + public void setup() { + ClassLoader loader = getClass().getClassLoader(); + this.mapper = JsonMapper.builder().addModules(SecurityJacksonModules.getModules(loader)).build(); + } + + @Test + public void serializeCasAuthenticationTest() throws JSONException { + CasAuthenticationToken token = createCasAuthenticationToken(); + String actualJson = this.mapper.writeValueAsString(token); + JSONAssert.assertEquals(CAS_TOKEN_JSON, actualJson, true); + } + + @Test + public void serializeCasAuthenticationTestAfterEraseCredentialInvoked() throws JSONException { + CasAuthenticationToken token = createCasAuthenticationToken(); + token.eraseCredentials(); + String actualJson = this.mapper.writeValueAsString(token); + JSONAssert.assertEquals(CAS_TOKEN_CLEARED_JSON, actualJson, true); + } + + @Test + public void deserializeCasAuthenticationTestAfterEraseCredentialInvoked() { + CasAuthenticationToken token = this.mapper.readValue(CAS_TOKEN_CLEARED_JSON, CasAuthenticationToken.class); + assertThat(((UserDetails) token.getPrincipal()).getPassword()).isNull(); + } + + @Test + public void deserializeCasAuthenticationTest() throws IOException { + CasAuthenticationToken token = this.mapper.readValue(CAS_TOKEN_JSON, CasAuthenticationToken.class); + assertThat(token).isNotNull(); + assertThat(token.getPrincipal()).isNotNull().isInstanceOf(User.class); + assertThat(((User) token.getPrincipal()).getUsername()).isEqualTo("admin"); + assertThat(((User) token.getPrincipal()).getPassword()).isEqualTo("1234"); + assertThat(token.getUserDetails()).isNotNull().isInstanceOf(User.class); + assertThat(token.getAssertion()).isNotNull().isInstanceOf(AssertionImpl.class); + assertThat(token.getKeyHash()).isEqualTo(KEY.hashCode()); + assertThat(token.getUserDetails().getAuthorities()).extracting(GrantedAuthority::getAuthority) + .containsOnly("ROLE_USER"); + assertThat(token.getAssertion().getAuthenticationDate()).isEqualTo(START_DATE); + assertThat(token.getAssertion().getValidFromDate()).isEqualTo(START_DATE); + assertThat(token.getAssertion().getValidUntilDate()).isEqualTo(END_DATE); + assertThat(token.getAssertion().getPrincipal().getName()).isEqualTo("assertName"); + assertThat(token.getAssertion().getAttributes()).hasSize(0); + } + + private CasAuthenticationToken createCasAuthenticationToken() { + User principal = new User("admin", "1234", Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER"))); + Collection authorities = Collections + .singletonList(new SimpleGrantedAuthority("ROLE_USER")); + Assertion assertion = new AssertionImpl(new AttributePrincipalImpl("assertName"), START_DATE, END_DATE, + START_DATE, Collections.emptyMap()); + return new CasAuthenticationToken(KEY, principal, principal.getPassword(), authorities, + new User("admin", "1234", authorities), assertion); + } + +} diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/JwkSetTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/JwkSetTests.java index ac36914000..663b279c00 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/JwkSetTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/JwkSetTests.java @@ -24,6 +24,7 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import tools.jackson.databind.json.JsonMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; @@ -167,7 +168,8 @@ public class JwkSetTests { RowMapper(RegisteredClientRepository registeredClientRepository) { super(registeredClientRepository); - getObjectMapper().addMixIn(TestingAuthenticationToken.class, TestingAuthenticationTokenMixin.class); + setMapper(new JdbcOAuth2AuthorizationService.JacksonDelegate(JsonMapper.builder() + .addMixIn(TestingAuthenticationToken.class, TestingAuthenticationTokenMixin.class))); } } @@ -176,7 +178,8 @@ public class JwkSetTests { ParametersMapper() { super(); - getObjectMapper().addMixIn(TestingAuthenticationToken.class, TestingAuthenticationTokenMixin.class); + setMapper(new JdbcOAuth2AuthorizationService.JacksonDelegate(JsonMapper.builder() + .addMixIn(TestingAuthenticationToken.class, TestingAuthenticationTokenMixin.class))); } } diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2AuthorizationCodeGrantTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2AuthorizationCodeGrantTests.java index fc9ef1c7a2..83d2b98250 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2AuthorizationCodeGrantTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2AuthorizationCodeGrantTests.java @@ -46,6 +46,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentCaptor; +import tools.jackson.databind.json.JsonMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; @@ -1300,7 +1301,8 @@ public class OAuth2AuthorizationCodeGrantTests { RowMapper(RegisteredClientRepository registeredClientRepository) { super(registeredClientRepository); - getObjectMapper().addMixIn(TestingAuthenticationToken.class, TestingAuthenticationTokenMixin.class); + setMapper(new JdbcOAuth2AuthorizationService.JacksonDelegate(JsonMapper.builder() + .addMixIn(TestingAuthenticationToken.class, TestingAuthenticationTokenMixin.class))); } } @@ -1309,7 +1311,8 @@ public class OAuth2AuthorizationCodeGrantTests { ParametersMapper() { super(); - getObjectMapper().addMixIn(TestingAuthenticationToken.class, TestingAuthenticationTokenMixin.class); + setMapper(new JdbcOAuth2AuthorizationService.JacksonDelegate(JsonMapper.builder() + .addMixIn(TestingAuthenticationToken.class, TestingAuthenticationTokenMixin.class))); } } diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2ClientCredentialsGrantTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2ClientCredentialsGrantTests.java index 98e277bbff..f60511156b 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2ClientCredentialsGrantTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2ClientCredentialsGrantTests.java @@ -40,6 +40,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentCaptor; +import tools.jackson.databind.json.JsonMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; @@ -573,7 +574,8 @@ public class OAuth2ClientCredentialsGrantTests { RowMapper(RegisteredClientRepository registeredClientRepository) { super(registeredClientRepository); - getObjectMapper().addMixIn(TestingAuthenticationToken.class, TestingAuthenticationTokenMixin.class); + setMapper(new JdbcOAuth2AuthorizationService.JacksonDelegate(JsonMapper.builder() + .addMixIn(TestingAuthenticationToken.class, TestingAuthenticationTokenMixin.class))); } } @@ -582,7 +584,8 @@ public class OAuth2ClientCredentialsGrantTests { ParametersMapper() { super(); - getObjectMapper().addMixIn(TestingAuthenticationToken.class, TestingAuthenticationTokenMixin.class); + setMapper(new JdbcOAuth2AuthorizationService.JacksonDelegate(JsonMapper.builder() + .addMixIn(TestingAuthenticationToken.class, TestingAuthenticationTokenMixin.class))); } } diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2RefreshTokenGrantTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2RefreshTokenGrantTests.java index b785ded4dc..5f01cea6c2 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2RefreshTokenGrantTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2RefreshTokenGrantTests.java @@ -39,6 +39,7 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import tools.jackson.databind.json.JsonMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; @@ -516,7 +517,8 @@ public class OAuth2RefreshTokenGrantTests { RowMapper(RegisteredClientRepository registeredClientRepository) { super(registeredClientRepository); - getObjectMapper().addMixIn(TestingAuthenticationToken.class, TestingAuthenticationTokenMixin.class); + setMapper(new JdbcOAuth2AuthorizationService.JacksonDelegate(JsonMapper.builder() + .addMixIn(TestingAuthenticationToken.class, TestingAuthenticationTokenMixin.class))); } } @@ -525,7 +527,8 @@ public class OAuth2RefreshTokenGrantTests { ParametersMapper() { super(); - getObjectMapper().addMixIn(TestingAuthenticationToken.class, TestingAuthenticationTokenMixin.class); + setMapper(new JdbcOAuth2AuthorizationService.JacksonDelegate(JsonMapper.builder() + .addMixIn(TestingAuthenticationToken.class, TestingAuthenticationTokenMixin.class))); } } diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2TokenIntrospectionTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2TokenIntrospectionTests.java index c01a8bd3e0..018ca4ad36 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2TokenIntrospectionTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2TokenIntrospectionTests.java @@ -35,6 +35,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentCaptor; +import tools.jackson.databind.json.JsonMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; @@ -553,7 +554,8 @@ public class OAuth2TokenIntrospectionTests { RowMapper(RegisteredClientRepository registeredClientRepository) { super(registeredClientRepository); - getObjectMapper().addMixIn(TestingAuthenticationToken.class, TestingAuthenticationTokenMixin.class); + setMapper(new JdbcOAuth2AuthorizationService.JacksonDelegate(JsonMapper.builder() + .addMixIn(TestingAuthenticationToken.class, TestingAuthenticationTokenMixin.class))); } } @@ -562,7 +564,8 @@ public class OAuth2TokenIntrospectionTests { ParametersMapper() { super(); - getObjectMapper().addMixIn(TestingAuthenticationToken.class, TestingAuthenticationTokenMixin.class); + setMapper(new JdbcOAuth2AuthorizationService.JacksonDelegate(JsonMapper.builder() + .addMixIn(TestingAuthenticationToken.class, TestingAuthenticationTokenMixin.class))); } } diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2TokenRevocationTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2TokenRevocationTests.java index 5a910c2b0f..5506c81607 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2TokenRevocationTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OAuth2TokenRevocationTests.java @@ -31,6 +31,7 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentCaptor; +import tools.jackson.databind.json.JsonMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; @@ -351,7 +352,8 @@ public class OAuth2TokenRevocationTests { RowMapper(RegisteredClientRepository registeredClientRepository) { super(registeredClientRepository); - getObjectMapper().addMixIn(TestingAuthenticationToken.class, TestingAuthenticationTokenMixin.class); + setMapper(new JdbcOAuth2AuthorizationService.JacksonDelegate(JsonMapper.builder() + .addMixIn(TestingAuthenticationToken.class, TestingAuthenticationTokenMixin.class))); } } @@ -360,7 +362,8 @@ public class OAuth2TokenRevocationTests { ParametersMapper() { super(); - getObjectMapper().addMixIn(TestingAuthenticationToken.class, TestingAuthenticationTokenMixin.class); + setMapper(new JdbcOAuth2AuthorizationService.JacksonDelegate(JsonMapper.builder() + .addMixIn(TestingAuthenticationToken.class, TestingAuthenticationTokenMixin.class))); } } diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OidcTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OidcTests.java index e9109cec34..354c3a3926 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OidcTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/oauth2/server/authorization/OidcTests.java @@ -36,6 +36,7 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import tools.jackson.databind.json.JsonMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; @@ -695,7 +696,8 @@ public class OidcTests { RowMapper(RegisteredClientRepository registeredClientRepository) { super(registeredClientRepository); - getObjectMapper().addMixIn(TestingAuthenticationToken.class, TestingAuthenticationTokenMixin.class); + setMapper(new JdbcOAuth2AuthorizationService.JacksonDelegate(JsonMapper.builder() + .addMixIn(TestingAuthenticationToken.class, TestingAuthenticationTokenMixin.class))); } } @@ -704,7 +706,8 @@ public class OidcTests { ParametersMapper() { super(); - getObjectMapper().addMixIn(TestingAuthenticationToken.class, TestingAuthenticationTokenMixin.class); + setMapper(new JdbcOAuth2AuthorizationService.JacksonDelegate(JsonMapper.builder() + .addMixIn(TestingAuthenticationToken.class, TestingAuthenticationTokenMixin.class))); } } diff --git a/core/spring-security-core.gradle b/core/spring-security-core.gradle index bc28ffe604..23fe149d7d 100644 --- a/core/spring-security-core.gradle +++ b/core/spring-security-core.gradle @@ -25,6 +25,7 @@ dependencies { optional 'org.springframework:spring-jdbc' optional 'org.springframework:spring-tx' optional 'org.jetbrains.kotlinx:kotlinx-coroutines-reactor' + optional 'tools.jackson.core:jackson-databind' testImplementation 'commons-collections:commons-collections' testImplementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310' diff --git a/core/src/main/java/org/springframework/security/jackson/AnonymousAuthenticationTokenMixin.java b/core/src/main/java/org/springframework/security/jackson/AnonymousAuthenticationTokenMixin.java new file mode 100644 index 0000000000..98044d5e42 --- /dev/null +++ b/core/src/main/java/org/springframework/security/jackson/AnonymousAuthenticationTokenMixin.java @@ -0,0 +1,57 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.jackson; + +import java.util.Collection; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +import org.springframework.security.core.GrantedAuthority; + +/** + * This is a Jackson mixin class helps in serialize/deserialize + * {@link org.springframework.security.authentication.AnonymousAuthenticationToken} class. + * + * @author Sebastien Deleuze + * @author Jitendra Singh + * @since 7.0 + * @see CoreJacksonModule + * @see SecurityJacksonModules + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) +@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, isGetterVisibility = JsonAutoDetect.Visibility.NONE, + getterVisibility = JsonAutoDetect.Visibility.NONE, creatorVisibility = JsonAutoDetect.Visibility.ANY) +class AnonymousAuthenticationTokenMixin { + + /** + * Constructor used by Jackson to create object of + * {@link org.springframework.security.authentication.AnonymousAuthenticationToken}. + * @param keyHash hashCode of key provided at the time of token creation by using + * {@link org.springframework.security.authentication.AnonymousAuthenticationToken#AnonymousAuthenticationToken(String, Object, Collection)} + * @param principal the principal (typically a UserDetails) + * @param authorities the authorities granted to the principal + */ + @JsonCreator + AnonymousAuthenticationTokenMixin(@JsonProperty("keyHash") Integer keyHash, + @JsonProperty("principal") Object principal, + @JsonProperty("authorities") Collection authorities) { + } + +} diff --git a/core/src/main/java/org/springframework/security/jackson/BadCredentialsExceptionMixin.java b/core/src/main/java/org/springframework/security/jackson/BadCredentialsExceptionMixin.java new file mode 100644 index 0000000000..2b64bed3ed --- /dev/null +++ b/core/src/main/java/org/springframework/security/jackson/BadCredentialsExceptionMixin.java @@ -0,0 +1,46 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.jackson; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +/** + * This mixin class helps in serialize/deserialize + * {@link org.springframework.security.authentication.BadCredentialsException} class. + * + * @author Sebastien Deleuze + * @author Yannick Lombardi + * @since 7.0 + * @see CoreJacksonModule + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) +@JsonIgnoreProperties({ "cause", "stackTrace", "authenticationRequest" }) +class BadCredentialsExceptionMixin { + + /** + * Constructor used by Jackson to create + * {@link org.springframework.security.authentication.BadCredentialsException} object. + * @param message the detail message + */ + @JsonCreator + BadCredentialsExceptionMixin(@JsonProperty("message") String message) { + } + +} diff --git a/core/src/main/java/org/springframework/security/jackson/CoreJacksonModule.java b/core/src/main/java/org/springframework/security/jackson/CoreJacksonModule.java new file mode 100644 index 0000000000..58293a1797 --- /dev/null +++ b/core/src/main/java/org/springframework/security/jackson/CoreJacksonModule.java @@ -0,0 +1,113 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.jackson; + +import java.time.Duration; +import java.time.Instant; + +import tools.jackson.core.Version; +import tools.jackson.databind.cfg.DateTimeFeature; +import tools.jackson.databind.cfg.MapperBuilder; +import tools.jackson.databind.jsontype.BasicPolymorphicTypeValidator; + +import org.springframework.security.authentication.AnonymousAuthenticationToken; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.authentication.RememberMeAuthenticationToken; +import org.springframework.security.authentication.TestingAuthenticationToken; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.authority.FactorGrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.SecurityContextImpl; +import org.springframework.security.core.userdetails.User; + +/** + * Jackson module for spring-security-core. This module register + * {@link AnonymousAuthenticationTokenMixin}, {@link RememberMeAuthenticationTokenMixin}, + * {@link SimpleGrantedAuthorityMixin}, {@link FactorGrantedAuthorityMixin}, + * {{@link UserMixin}, {@link UsernamePasswordAuthenticationTokenMixin} and + * {@link UsernamePasswordAuthenticationTokenMixin}. + * + *

+ * The recommended way to configure it is to use {@link SecurityJacksonModules} in order + * to enable properly automatic inclusion of type information with related validation. + * + *

+ *     ClassLoader loader = getClass().getClassLoader();
+ *     JsonMapper mapper = JsonMapper.builder()
+ * 				.addModules(SecurityJacksonModules.getModules(loader))
+ * 				.build();
+ * 
+ * + * @author Sebastien Deleuze + * @author Jitendra Singh + * @since 7.O + * @see SecurityJacksonModules + */ +@SuppressWarnings("serial") + +public class CoreJacksonModule extends SecurityJacksonModule { + + public CoreJacksonModule() { + super(CoreJacksonModule.class.getName(), new Version(1, 0, 0, null, null, null)); + } + + protected CoreJacksonModule(String name, Version version) { + super(name, version); + } + + @Override + public void configurePolymorphicTypeValidator(BasicPolymorphicTypeValidator.Builder builder) { + builder.allowIfSubType(Instant.class) + .allowIfSubType(Duration.class) + .allowIfSubType(SimpleGrantedAuthority.class) + .allowIfSubType(FactorGrantedAuthority.class) + .allowIfSubType(UsernamePasswordAuthenticationToken.class) + .allowIfSubType(RememberMeAuthenticationToken.class) + .allowIfSubType(AnonymousAuthenticationToken.class) + .allowIfSubType(User.class) + .allowIfSubType(BadCredentialsException.class) + .allowIfSubType(SecurityContextImpl.class) + .allowIfSubType(TestingAuthenticationToken.class) + .allowIfSubType("java.util.Collections$UnmodifiableSet") + .allowIfSubType("java.util.Collections$UnmodifiableRandomAccessList") + .allowIfSubType("java.util.Collections$EmptyList") + .allowIfSubType("java.util.ArrayList") + .allowIfSubType("java.util.HashMap") + .allowIfSubType("java.util.Collections$EmptyMap") + .allowIfSubType("java.util.Date") + .allowIfSubType("java.util.Arrays$ArrayList") + .allowIfSubType("java.util.Collections$UnmodifiableMap") + .allowIfSubType("java.util.LinkedHashMap") + .allowIfSubType("java.util.Collections$SingletonList") + .allowIfSubType("java.util.TreeMap") + .allowIfSubType("java.util.HashSet") + .allowIfSubType("java.util.LinkedHashSet"); + } + + @Override + public void setupModule(SetupContext context) { + ((MapperBuilder) context.getOwner()).enable(DateTimeFeature.WRITE_DATES_AS_TIMESTAMPS); + context.setMixIn(AnonymousAuthenticationToken.class, AnonymousAuthenticationTokenMixin.class); + context.setMixIn(RememberMeAuthenticationToken.class, RememberMeAuthenticationTokenMixin.class); + context.setMixIn(SimpleGrantedAuthority.class, SimpleGrantedAuthorityMixin.class); + context.setMixIn(FactorGrantedAuthority.class, FactorGrantedAuthorityMixin.class); + context.setMixIn(User.class, UserMixin.class); + context.setMixIn(UsernamePasswordAuthenticationToken.class, UsernamePasswordAuthenticationTokenMixin.class); + context.setMixIn(BadCredentialsException.class, BadCredentialsExceptionMixin.class); + } + +} diff --git a/core/src/main/java/org/springframework/security/jackson/FactorGrantedAuthorityMixin.java b/core/src/main/java/org/springframework/security/jackson/FactorGrantedAuthorityMixin.java new file mode 100644 index 0000000000..3e1db9cc6a --- /dev/null +++ b/core/src/main/java/org/springframework/security/jackson/FactorGrantedAuthorityMixin.java @@ -0,0 +1,50 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.jackson; + +import java.time.Instant; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +/** + * Jackson Mixin class helps in serialize/deserialize + * {@link org.springframework.security.core.authority.SimpleGrantedAuthority}. + * + * @author Sebastien Deleuze + * @author Rob Winch + * @since 7.0 + * @see CoreJacksonModule + * @see SecurityJacksonModules + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) +@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.NONE, + getterVisibility = JsonAutoDetect.Visibility.PUBLIC_ONLY, isGetterVisibility = JsonAutoDetect.Visibility.NONE) +abstract class FactorGrantedAuthorityMixin { + + /** + * Mixin Constructor. + * @param authority the authority + */ + @JsonCreator + FactorGrantedAuthorityMixin(@JsonProperty("authority") String authority, + @JsonProperty("issuedAt") Instant issuedAt) { + } + +} diff --git a/core/src/main/java/org/springframework/security/jackson/RememberMeAuthenticationTokenMixin.java b/core/src/main/java/org/springframework/security/jackson/RememberMeAuthenticationTokenMixin.java new file mode 100644 index 0000000000..7def13e632 --- /dev/null +++ b/core/src/main/java/org/springframework/security/jackson/RememberMeAuthenticationTokenMixin.java @@ -0,0 +1,60 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.jackson; + +import java.util.Collection; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +import org.springframework.security.core.GrantedAuthority; + +/** + * This mixin class helps in serialize/deserialize + * {@link org.springframework.security.authentication.RememberMeAuthenticationToken} + * class. + * + * @author Sebastien Deleuze + * @author Jitendra Singh + * @since 7.0 + * @see CoreJacksonModule + * @see SecurityJacksonModules + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY) +@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE, + isGetterVisibility = JsonAutoDetect.Visibility.NONE, creatorVisibility = JsonAutoDetect.Visibility.ANY) +@JsonIgnoreProperties(ignoreUnknown = true) +class RememberMeAuthenticationTokenMixin { + + /** + * Constructor used by Jackson to create + * {@link org.springframework.security.authentication.RememberMeAuthenticationToken} + * object. + * @param keyHash hashCode of above given key. + * @param principal the principal (typically a UserDetails) + * @param authorities the authorities granted to the principal + */ + @JsonCreator + RememberMeAuthenticationTokenMixin(@JsonProperty("keyHash") Integer keyHash, + @JsonProperty("principal") Object principal, + @JsonProperty("authorities") Collection authorities) { + } + +} diff --git a/core/src/main/java/org/springframework/security/jackson/SecurityJacksonModule.java b/core/src/main/java/org/springframework/security/jackson/SecurityJacksonModule.java new file mode 100644 index 0000000000..d2504a872d --- /dev/null +++ b/core/src/main/java/org/springframework/security/jackson/SecurityJacksonModule.java @@ -0,0 +1,42 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.jackson; + +import tools.jackson.core.Version; +import tools.jackson.databind.jsontype.BasicPolymorphicTypeValidator; +import tools.jackson.databind.jsontype.PolymorphicTypeValidator; +import tools.jackson.databind.module.SimpleModule; + +/** + * Jackson module allowing to contribute {@link PolymorphicTypeValidator} configuration. + * + * @author Sebastien Deleuze + * @since 7.0 + */ +public abstract class SecurityJacksonModule extends SimpleModule { + + public SecurityJacksonModule() { + super(); + } + + public SecurityJacksonModule(String name, Version version) { + super(name, version, null); + } + + public abstract void configurePolymorphicTypeValidator(BasicPolymorphicTypeValidator.Builder builder); + +} diff --git a/core/src/main/java/org/springframework/security/jackson/SecurityJacksonModules.java b/core/src/main/java/org/springframework/security/jackson/SecurityJacksonModules.java new file mode 100644 index 0000000000..4c5b484b65 --- /dev/null +++ b/core/src/main/java/org/springframework/security/jackson/SecurityJacksonModules.java @@ -0,0 +1,203 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.jackson; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; +import tools.jackson.databind.DefaultTyping; +import tools.jackson.databind.JacksonModule; +import tools.jackson.databind.cfg.MapperBuilder; +import tools.jackson.databind.jsontype.BasicPolymorphicTypeValidator; +import tools.jackson.databind.jsontype.PolymorphicTypeValidator; +import tools.jackson.databind.module.SimpleModule; + +import org.springframework.core.log.LogMessage; +import org.springframework.util.ClassUtils; + +/** + * This utility class will find all the Jackson modules contributed by Spring Security in + * the classpath (except {@code OAuth2AuthorizationServerJacksonModule} and + * {@code WebauthnJacksonModule}), enable automatic inclusion of type information and + * configure a {@link PolymorphicTypeValidator} that handles the validation of class + * names. + * + *

+ *

+ *     ClassLoader loader = getClass().getClassLoader();
+ *     JsonMapper mapper = JsonMapper.builder()
+ * 				.addModules(SecurityJacksonModules.getModules(loader))
+ * 				.build();
+ * 
+ * + * If needed, you can add custom classes to the validation handling. + *

+ *

+ *     ClassLoader loader = getClass().getClassLoader();
+ *     BasicPolymorphicTypeValidator.Builder builder = BasicPolymorphicTypeValidator.builder()
+ *     			.allowIfSubType(MyCustomType.class);
+ *     JsonMapper mapper = JsonMapper.builder()
+ * 				.addModules(SecurityJacksonModules.getModules(loader, builder))
+ * 	   			.build();
+ * 
+ * + * @author Sebastien Deleuze + * @author Jitendra Singh + * @since 7.0 + */ +public final class SecurityJacksonModules { + + private static final Log logger = LogFactory.getLog(SecurityJacksonModules.class); + + private static final List securityJacksonModuleClasses = Arrays.asList( + "org.springframework.security.jackson.CoreJacksonModule", + "org.springframework.security.web.jackson.WebJacksonModule", + "org.springframework.security.web.server.jackson.WebServerJacksonModule"); + + private static final String webServletJacksonModuleClass = "org.springframework.security.web.jackson.WebServletJacksonModule"; + + private static final String oauth2ClientJacksonModuleClass = "org.springframework.security.oauth2.client.jackson.OAuth2ClientJacksonModule"; + + private static final String ldapJacksonModuleClass = "org.springframework.security.ldap.jackson.LdapJacksonModule"; + + private static final String saml2JacksonModuleClass = "org.springframework.security.saml2.jackson.Saml2JacksonModule"; + + private static final String casJacksonModuleClass = "org.springframework.security.cas.jackson.CasJacksonModule"; + + private static final boolean webServletPresent; + + private static final boolean oauth2ClientPresent; + + private static final boolean ldapJacksonPresent; + + private static final boolean saml2JacksonPresent; + + private static final boolean casJacksonPresent; + + static { + + ClassLoader classLoader = SecurityJacksonModules.class.getClassLoader(); + webServletPresent = ClassUtils.isPresent("jakarta.servlet.http.Cookie", classLoader); + oauth2ClientPresent = ClassUtils.isPresent("org.springframework.security.oauth2.client.OAuth2AuthorizedClient", + classLoader); + ldapJacksonPresent = ClassUtils.isPresent(ldapJacksonModuleClass, classLoader); + saml2JacksonPresent = ClassUtils.isPresent(saml2JacksonModuleClass, classLoader); + casJacksonPresent = ClassUtils.isPresent(casJacksonModuleClass, classLoader); + } + + private SecurityJacksonModules() { + } + + @SuppressWarnings("unchecked") + private static @Nullable SecurityJacksonModule loadAndGetInstance(String className, ClassLoader loader) { + try { + Class securityModule = (Class) ClassUtils + .forName(className, loader); + logger.debug(LogMessage.format("Loaded module %s, now registering", className)); + return securityModule.getConstructor().newInstance(); + } + catch (Exception ex) { + logger.debug(LogMessage.format("Cannot load module %s", className), ex); + } + return null; + } + + /** + * Return the list of available security modules in classpath, enable automatic + * inclusion of type information and configure a default + * {@link PolymorphicTypeValidator} that handles the validation of class names. + * @param loader the ClassLoader to use + * @return List of available security modules in classpath + * @see #getModules(ClassLoader, BasicPolymorphicTypeValidator.Builder) + */ + public static List getModules(ClassLoader loader) { + return getModules(loader, null); + } + + /** + * Return the list of available security modules in classpath, enable automatic + * inclusion of type information and configure a default + * {@link PolymorphicTypeValidator} customizable with the provided builder that + * handles the validation of class names. + * @param loader the ClassLoader to use + * @param typeValidatorBuilder the builder to configure custom types allowed in + * addition to Spring Security ones + * @return List of available security modules in classpath. + */ + public static List getModules(ClassLoader loader, + BasicPolymorphicTypeValidator.@Nullable Builder typeValidatorBuilder) { + + List modules = new ArrayList<>(); + for (String className : securityJacksonModuleClasses) { + addToModulesList(loader, modules, className); + } + if (webServletPresent) { + addToModulesList(loader, modules, webServletJacksonModuleClass); + } + if (oauth2ClientPresent) { + addToModulesList(loader, modules, oauth2ClientJacksonModuleClass); + } + if (ldapJacksonPresent) { + addToModulesList(loader, modules, ldapJacksonModuleClass); + } + if (saml2JacksonPresent) { + addToModulesList(loader, modules, saml2JacksonModuleClass); + } + if (casJacksonPresent) { + addToModulesList(loader, modules, casJacksonModuleClass); + } + applyPolymorphicTypeValidator(modules, typeValidatorBuilder); + return modules; + } + + private static void applyPolymorphicTypeValidator(List modules, + BasicPolymorphicTypeValidator.@Nullable Builder typeValidatorBuilder) { + + BasicPolymorphicTypeValidator.Builder builder = (typeValidatorBuilder != null) ? typeValidatorBuilder + : BasicPolymorphicTypeValidator.builder(); + for (JacksonModule module : modules) { + if (module instanceof SecurityJacksonModule securityModule) { + securityModule.configurePolymorphicTypeValidator(builder); + } + } + modules.add(new SimpleModule() { + @Override + public void setupModule(SetupContext context) { + ((MapperBuilder) context.getOwner()).activateDefaultTyping(builder.build(), + DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY); + } + }); + } + + /** + * @param loader the ClassLoader to use + * @param modules list of the modules to add + * @param className name of the class to instantiate + */ + private static void addToModulesList(ClassLoader loader, List modules, String className) { + SecurityJacksonModule module = loadAndGetInstance(className, loader); + if (module != null) { + modules.add(module); + } + } + +} diff --git a/core/src/main/java/org/springframework/security/jackson/SimpleGrantedAuthorityMixin.java b/core/src/main/java/org/springframework/security/jackson/SimpleGrantedAuthorityMixin.java new file mode 100644 index 0000000000..17434689ba --- /dev/null +++ b/core/src/main/java/org/springframework/security/jackson/SimpleGrantedAuthorityMixin.java @@ -0,0 +1,47 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.jackson; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +/** + * Jackson Mixin class helps in serialize/deserialize + * {@link org.springframework.security.core.authority.SimpleGrantedAuthority}. + * + * @author Sebastien Deleuze + * @author Jitendra Singh + * @since 7.0 + * @see CoreJacksonModule + * @see SecurityJacksonModules + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) +@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.NONE, + getterVisibility = JsonAutoDetect.Visibility.PUBLIC_ONLY, isGetterVisibility = JsonAutoDetect.Visibility.NONE) +public abstract class SimpleGrantedAuthorityMixin { + + /** + * Mixin Constructor. + * @param role the role + */ + @JsonCreator + public SimpleGrantedAuthorityMixin(@JsonProperty("authority") String role) { + } + +} diff --git a/core/src/main/java/org/springframework/security/jackson/UserDeserializer.java b/core/src/main/java/org/springframework/security/jackson/UserDeserializer.java new file mode 100644 index 0000000000..daf267ee13 --- /dev/null +++ b/core/src/main/java/org/springframework/security/jackson/UserDeserializer.java @@ -0,0 +1,81 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.jackson; + +import java.util.Set; + +import tools.jackson.core.JacksonException; +import tools.jackson.core.JsonParser; +import tools.jackson.core.type.TypeReference; +import tools.jackson.databind.DeserializationContext; +import tools.jackson.databind.JsonNode; +import tools.jackson.databind.ValueDeserializer; +import tools.jackson.databind.node.MissingNode; + +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.User; + +/** + * Custom Deserializer for {@link User} class. This is already registered with + * {@link UserMixin}. You can also use it directly with your mixin class. + * + * @author Sebastien Deleuze + * @author Jitendra Singh + * @since 7.0 + * @see UserMixin + */ +class UserDeserializer extends ValueDeserializer { + + private static final TypeReference> GRANTED_AUTHORITY_SET = new TypeReference<>() { + }; + + /** + * This method will create {@link User} object. It will ensure successful object + * creation even if password key is null in serialized json, because credentials may + * be removed from the {@link User} by invoking {@link User#eraseCredentials()}. In + * that case there won't be any password key in serialized json. + * @param jp the JsonParser + * @param ctxt the DeserializationContext + * @return the user + * @throws JacksonException if an error during JSON processing occurs + */ + @Override + public User deserialize(JsonParser jp, DeserializationContext ctxt) throws JacksonException { + JsonNode jsonNode = ctxt.readTree(jp); + JsonNode authoritiesNode = readJsonNode(jsonNode, "authorities"); + Set authorities = ctxt.readTreeAsValue(authoritiesNode, + ctxt.getTypeFactory().constructType(GRANTED_AUTHORITY_SET)); + JsonNode passwordNode = readJsonNode(jsonNode, "password"); + String username = readJsonNode(jsonNode, "username").asString(); + String password = (passwordNode.isMissingNode()) ? null : passwordNode.stringValue(); + boolean enabled = readJsonNode(jsonNode, "enabled").asBoolean(); + boolean accountNonExpired = readJsonNode(jsonNode, "accountNonExpired").asBoolean(); + boolean credentialsNonExpired = readJsonNode(jsonNode, "credentialsNonExpired").asBoolean(); + boolean accountNonLocked = readJsonNode(jsonNode, "accountNonLocked").asBoolean(); + User result = new User(username, password, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, + authorities); + if (passwordNode.asString(null) == null) { + result.eraseCredentials(); + } + return result; + } + + private JsonNode readJsonNode(JsonNode jsonNode, String field) { + return jsonNode.has(field) ? jsonNode.get(field) : MissingNode.getInstance(); + } + +} diff --git a/core/src/main/java/org/springframework/security/jackson/UserMixin.java b/core/src/main/java/org/springframework/security/jackson/UserMixin.java new file mode 100644 index 0000000000..f0bede4e2d --- /dev/null +++ b/core/src/main/java/org/springframework/security/jackson/UserMixin.java @@ -0,0 +1,41 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.jackson; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import tools.jackson.databind.annotation.JsonDeserialize; + +/** + * This mixin class helps in serialize/deserialize + * {@link org.springframework.security.core.userdetails.User}. This class also register a + * custom deserializer {@link UserDeserializer} to deserialize User object successfully. + * + * @author Sebastien Deleuze + * @author Jitendra Singh + * @since 7.0 + * @see UserDeserializer + * @see CoreJacksonModule + * @see SecurityJacksonModules + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) +@JsonDeserialize(using = UserDeserializer.class) +@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE, + isGetterVisibility = JsonAutoDetect.Visibility.NONE) +abstract class UserMixin { + +} diff --git a/core/src/main/java/org/springframework/security/jackson/UsernamePasswordAuthenticationTokenDeserializer.java b/core/src/main/java/org/springframework/security/jackson/UsernamePasswordAuthenticationTokenDeserializer.java new file mode 100644 index 0000000000..618099d551 --- /dev/null +++ b/core/src/main/java/org/springframework/security/jackson/UsernamePasswordAuthenticationTokenDeserializer.java @@ -0,0 +1,105 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.jackson; + +import java.util.List; + +import org.jspecify.annotations.Nullable; +import tools.jackson.core.JacksonException; +import tools.jackson.core.JsonParser; +import tools.jackson.core.exc.StreamReadException; +import tools.jackson.core.type.TypeReference; +import tools.jackson.databind.DatabindException; +import tools.jackson.databind.DeserializationContext; +import tools.jackson.databind.JsonNode; +import tools.jackson.databind.ValueDeserializer; +import tools.jackson.databind.node.MissingNode; + +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.GrantedAuthority; + +/** + * Custom deserializer for {@link UsernamePasswordAuthenticationToken}. At the time of + * deserialization it will invoke suitable constructor depending on the value of + * authenticated property. It will ensure that the token's state must not change. + * + * @author Sebastien Deleuze + * @author Jitendra Singh + * @author Greg Turnquist + * @author Onur Kagan Ozcan + * @since 7.0 + * @see UsernamePasswordAuthenticationTokenMixin + */ +class UsernamePasswordAuthenticationTokenDeserializer extends ValueDeserializer { + + private static final TypeReference> GRANTED_AUTHORITY_LIST = new TypeReference<>() { + }; + + /** + * This method construct {@link UsernamePasswordAuthenticationToken} object from + * serialized json. + * @param jp the JsonParser + * @param ctxt the DeserializationContext + * @return the user + * @throws JacksonException if an error during JSON processing occurs + */ + @Override + public UsernamePasswordAuthenticationToken deserialize(JsonParser jp, DeserializationContext ctxt) + throws JacksonException { + JsonNode jsonNode = ctxt.readTree(jp); + boolean authenticated = readJsonNode(jsonNode, "authenticated").asBoolean(); + JsonNode principalNode = readJsonNode(jsonNode, "principal"); + Object principal = getPrincipal(ctxt, principalNode); + JsonNode credentialsNode = readJsonNode(jsonNode, "credentials"); + Object credentials = getCredentials(credentialsNode); + JsonNode authoritiesNode = readJsonNode(jsonNode, "authorities"); + List authorities = ctxt.readTreeAsValue(authoritiesNode, + ctxt.getTypeFactory().constructType(GRANTED_AUTHORITY_LIST)); + UsernamePasswordAuthenticationToken token = (!authenticated) + ? UsernamePasswordAuthenticationToken.unauthenticated(principal, credentials) + : UsernamePasswordAuthenticationToken.authenticated(principal, credentials, authorities); + JsonNode detailsNode = readJsonNode(jsonNode, "details"); + if (detailsNode.isNull() || detailsNode.isMissingNode()) { + token.setDetails(null); + } + else { + Object details = ctxt.readTreeAsValue(detailsNode, Object.class); + token.setDetails(details); + } + return token; + } + + private @Nullable Object getCredentials(JsonNode credentialsNode) { + if (credentialsNode.isNull() || credentialsNode.isMissingNode()) { + return null; + } + return credentialsNode.asString(); + } + + private Object getPrincipal(DeserializationContext ctxt, JsonNode principalNode) + throws StreamReadException, DatabindException { + if (principalNode.isObject()) { + return ctxt.readTreeAsValue(principalNode, Object.class); + } + return principalNode.asString(); + } + + private JsonNode readJsonNode(JsonNode jsonNode, String field) { + return jsonNode.has(field) ? jsonNode.get(field) : MissingNode.getInstance(); + } + +} diff --git a/core/src/main/java/org/springframework/security/jackson/UsernamePasswordAuthenticationTokenMixin.java b/core/src/main/java/org/springframework/security/jackson/UsernamePasswordAuthenticationTokenMixin.java new file mode 100644 index 0000000000..92b79eaac3 --- /dev/null +++ b/core/src/main/java/org/springframework/security/jackson/UsernamePasswordAuthenticationTokenMixin.java @@ -0,0 +1,41 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.jackson; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import tools.jackson.databind.annotation.JsonDeserialize; + +/** + * This mixin class is used to serialize / deserialize + * {@link org.springframework.security.authentication.UsernamePasswordAuthenticationToken}. + * This class register a custom deserializer + * {@link UsernamePasswordAuthenticationTokenDeserializer}. + * + * @author Sebastien Deleuze + * @author Jitendra Singh + * @since 7.0 + * @see CoreJacksonModule + * @see SecurityJacksonModules + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) +@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE, + isGetterVisibility = JsonAutoDetect.Visibility.NONE) +@JsonDeserialize(using = UsernamePasswordAuthenticationTokenDeserializer.class) +abstract class UsernamePasswordAuthenticationTokenMixin { + +} diff --git a/core/src/main/java/org/springframework/security/jackson/package-info.java b/core/src/main/java/org/springframework/security/jackson/package-info.java new file mode 100644 index 0000000000..ede43c2eda --- /dev/null +++ b/core/src/main/java/org/springframework/security/jackson/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Jackson 3+ serialization support. + */ +@NullMarked +package org.springframework.security.jackson; + +import org.jspecify.annotations.NullMarked; diff --git a/core/src/main/java/org/springframework/security/jackson2/UsernamePasswordAuthenticationTokenDeserializer.java b/core/src/main/java/org/springframework/security/jackson2/UsernamePasswordAuthenticationTokenDeserializer.java index b67d82ca19..fe10b1e703 100644 --- a/core/src/main/java/org/springframework/security/jackson2/UsernamePasswordAuthenticationTokenDeserializer.java +++ b/core/src/main/java/org/springframework/security/jackson2/UsernamePasswordAuthenticationTokenDeserializer.java @@ -71,7 +71,7 @@ class UsernamePasswordAuthenticationTokenDeserializer extends JsonDeserializer properties = mapper.readValue(serialized, Map.class); assertThat(properties).hasSize(3).containsKeys("id", "firstName", "lastName"); diff --git a/core/src/test/java/org/springframework/security/jackson/AbstractMixinTests.java b/core/src/test/java/org/springframework/security/jackson/AbstractMixinTests.java new file mode 100644 index 0000000000..e763f35eab --- /dev/null +++ b/core/src/test/java/org/springframework/security/jackson/AbstractMixinTests.java @@ -0,0 +1,51 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.jackson; + +import org.junit.jupiter.api.BeforeEach; +import tools.jackson.databind.json.JsonMapper; +import tools.jackson.databind.jsontype.BasicPolymorphicTypeValidator; + +import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.security.core.userdetails.User; + +/** + * @author Jitenra Singh + * @since 4.2 + */ +public abstract class AbstractMixinTests { + + protected JsonMapper mapper; + + @BeforeEach + public void setup() { + ClassLoader loader = getClass().getClassLoader(); + BasicPolymorphicTypeValidator.Builder builder = BasicPolymorphicTypeValidator.builder() + .allowIfSubType( + "org.springframework.security.jackson.UsernamePasswordAuthenticationTokenMixinTests$NonUserPrincipal"); + this.mapper = JsonMapper.builder().addModules(SecurityJacksonModules.getModules(loader, builder)).build(); + } + + User createDefaultUser() { + return createUser("admin", "1234", "ROLE_USER"); + } + + User createUser(String username, String password, String authority) { + return new User(username, password, AuthorityUtils.createAuthorityList(authority)); + } + +} diff --git a/core/src/test/java/org/springframework/security/jackson/AnonymousAuthenticationTokenMixinTests.java b/core/src/test/java/org/springframework/security/jackson/AnonymousAuthenticationTokenMixinTests.java new file mode 100644 index 0000000000..b084c8e6e9 --- /dev/null +++ b/core/src/test/java/org/springframework/security/jackson/AnonymousAuthenticationTokenMixinTests.java @@ -0,0 +1,83 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.jackson; + +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; +import tools.jackson.databind.exc.ValueInstantiationException; + +import org.springframework.security.authentication.AnonymousAuthenticationToken; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.User; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * @author Jitendra Singh + * @since 4.2 + */ +public class AnonymousAuthenticationTokenMixinTests extends AbstractMixinTests { + + private static final String HASH_KEY = "key"; + + // @formatter:off + private static final String ANONYMOUS_JSON = "{" + + "\"@class\": \"org.springframework.security.authentication.AnonymousAuthenticationToken\", " + + "\"details\": null," + + "\"principal\": " + UserDeserializerTests.USER_JSON + "," + + "\"authenticated\": true, " + + "\"keyHash\": " + HASH_KEY.hashCode() + "," + + "\"authorities\": " + SimpleGrantedAuthorityMixinTests.AUTHORITIES_ARRAYLIST_JSON + + "}"; + // @formatter:on + @Test + public void serializeAnonymousAuthenticationTokenTest() throws JSONException { + User user = createDefaultUser(); + AnonymousAuthenticationToken token = new AnonymousAuthenticationToken(HASH_KEY, user, user.getAuthorities()); + String actualJson = this.mapper.writeValueAsString(token); + JSONAssert.assertEquals(ANONYMOUS_JSON, actualJson, true); + } + + @Test + public void deserializeAnonymousAuthenticationTokenTest() { + AnonymousAuthenticationToken token = this.mapper.readValue(ANONYMOUS_JSON, AnonymousAuthenticationToken.class); + assertThat(token).isNotNull(); + assertThat(token.getKeyHash()).isEqualTo(HASH_KEY.hashCode()); + assertThat(token.getAuthorities()).isNotNull().hasSize(1).contains(new SimpleGrantedAuthority("ROLE_USER")); + } + + @Test + public void deserializeAnonymousAuthenticationTokenWithoutAuthoritiesTest() { + String jsonString = "{\"@class\": \"org.springframework.security.authentication.AnonymousAuthenticationToken\", \"details\": null," + + "\"principal\": \"user\", \"authenticated\": true, \"keyHash\": " + HASH_KEY.hashCode() + "," + + "\"authorities\": [\"java.util.ArrayList\", []]}"; + assertThatExceptionOfType(ValueInstantiationException.class) + .isThrownBy(() -> this.mapper.readValue(jsonString, AnonymousAuthenticationToken.class)); + } + + @Test + public void serializeAnonymousAuthenticationTokenMixinAfterEraseCredentialTest() throws JSONException { + User user = createDefaultUser(); + AnonymousAuthenticationToken token = new AnonymousAuthenticationToken(HASH_KEY, user, user.getAuthorities()); + token.eraseCredentials(); + String actualJson = this.mapper.writeValueAsString(token); + JSONAssert.assertEquals(ANONYMOUS_JSON.replace(UserDeserializerTests.USER_PASSWORD, "null"), actualJson, true); + } + +} diff --git a/core/src/test/java/org/springframework/security/jackson/BadCredentialsExceptionMixinTests.java b/core/src/test/java/org/springframework/security/jackson/BadCredentialsExceptionMixinTests.java new file mode 100644 index 0000000000..88636d4609 --- /dev/null +++ b/core/src/test/java/org/springframework/security/jackson/BadCredentialsExceptionMixinTests.java @@ -0,0 +1,60 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.jackson; + +import java.io.IOException; + +import com.fasterxml.jackson.core.JsonProcessingException; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; + +import org.springframework.security.authentication.BadCredentialsException; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Yannick Lombardi + * @since 5.0 + */ +public class BadCredentialsExceptionMixinTests extends AbstractMixinTests { + + // @formatter:off + private static final String EXCEPTION_JSON = "{" + + "\"@class\": \"org.springframework.security.authentication.BadCredentialsException\"," + + "\"localizedMessage\": \"message\", " + + "\"message\": \"message\", " + + "\"suppressed\": [\"[Ljava.lang.Throwable;\",[]]" + + "}"; + // @formatter:on + @Test + public void serializeBadCredentialsExceptionMixinTest() throws JsonProcessingException, JSONException { + BadCredentialsException exception = new BadCredentialsException("message"); + String serializedJson = this.mapper.writeValueAsString(exception); + JSONAssert.assertEquals(EXCEPTION_JSON, serializedJson, true); + } + + @Test + public void deserializeBadCredentialsExceptionMixinTest() throws IOException { + BadCredentialsException exception = this.mapper.readValue(EXCEPTION_JSON, BadCredentialsException.class); + assertThat(exception).isNotNull(); + assertThat(exception.getCause()).isNull(); + assertThat(exception.getMessage()).isEqualTo("message"); + assertThat(exception.getLocalizedMessage()).isEqualTo("message"); + } + +} diff --git a/core/src/test/java/org/springframework/security/jackson/FactorGrantedAuthorityMixinTests.java b/core/src/test/java/org/springframework/security/jackson/FactorGrantedAuthorityMixinTests.java new file mode 100644 index 0000000000..865a1be9a7 --- /dev/null +++ b/core/src/test/java/org/springframework/security/jackson/FactorGrantedAuthorityMixinTests.java @@ -0,0 +1,60 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.jackson; + +import java.time.Instant; + +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; + +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.FactorGrantedAuthority; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Rob Winch + * @since 7.0 + */ +class FactorGrantedAuthorityMixinTests extends AbstractMixinTests { + + // @formatter:off + public static final String AUTHORITY_JSON = "{\"@class\": \"org.springframework.security.core.authority.FactorGrantedAuthority\", \"authority\": \"FACTOR_PASSWORD\", \"issuedAt\": 1759177143.043000000 }"; + + private Instant issuedAt = Instant.ofEpochMilli(1759177143043L); + + // @formatter:on + + @Test + void serializeSimpleGrantedAuthorityTest() throws JSONException { + GrantedAuthority authority = FactorGrantedAuthority.withAuthority("FACTOR_PASSWORD") + .issuedAt(this.issuedAt) + .build(); + String serializeJson = this.mapper.writeValueAsString(authority); + JSONAssert.assertEquals(AUTHORITY_JSON, serializeJson, true); + } + + @Test + void deserializeGrantedAuthorityTest() { + FactorGrantedAuthority authority = (FactorGrantedAuthority) this.mapper.readValue(AUTHORITY_JSON, Object.class); + assertThat(authority).isNotNull(); + assertThat(authority.getAuthority()).isEqualTo("FACTOR_PASSWORD"); + assertThat(authority.getIssuedAt()).isEqualTo(this.issuedAt); + } + +} diff --git a/core/src/test/java/org/springframework/security/jackson/RememberMeAuthenticationTokenMixinTests.java b/core/src/test/java/org/springframework/security/jackson/RememberMeAuthenticationTokenMixinTests.java new file mode 100644 index 0000000000..0957e30fa3 --- /dev/null +++ b/core/src/test/java/org/springframework/security/jackson/RememberMeAuthenticationTokenMixinTests.java @@ -0,0 +1,128 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.jackson; + +import java.io.IOException; +import java.util.Collections; + +import com.fasterxml.jackson.core.JsonProcessingException; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; + +import org.springframework.security.authentication.RememberMeAuthenticationToken; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.User; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * @author Jitendra Singh + * @since 4.2 + */ +public class RememberMeAuthenticationTokenMixinTests extends AbstractMixinTests { + + private static final String REMEMBERME_KEY = "rememberMe"; + + // @formatter:off + private static final String REMEMBERME_AUTH_JSON = "{" + + "\"@class\": \"org.springframework.security.authentication.RememberMeAuthenticationToken\", " + + "\"keyHash\": " + REMEMBERME_KEY.hashCode() + ", " + + "\"authenticated\": true, \"details\": null" + ", " + + "\"principal\": " + UserDeserializerTests.USER_JSON + ", " + + "\"authorities\": " + SimpleGrantedAuthorityMixinTests.AUTHORITIES_ARRAYLIST_JSON + + "}"; + // @formatter:on + + // @formatter:off + private static final String REMEMBERME_AUTH_STRINGPRINCIPAL_JSON = "{" + + "\"@class\": \"org.springframework.security.authentication.RememberMeAuthenticationToken\"," + + "\"keyHash\": " + REMEMBERME_KEY.hashCode() + ", " + + "\"authenticated\": true, " + + "\"details\": null," + + "\"principal\": \"admin\", " + + "\"authorities\": " + SimpleGrantedAuthorityMixinTests.AUTHORITIES_ARRAYLIST_JSON + + "}"; + // @formatter:on + + @Test + public void testWithNullPrincipal() { + assertThatIllegalArgumentException().isThrownBy( + () -> new RememberMeAuthenticationToken("key", null, Collections.emptyList())); + } + + @Test + public void testWithNullKey() { + assertThatIllegalArgumentException().isThrownBy( + () -> new RememberMeAuthenticationToken(null, "principal", Collections.emptyList())); + } + + @Test + public void serializeRememberMeAuthenticationToken() throws JsonProcessingException, JSONException { + RememberMeAuthenticationToken token = new RememberMeAuthenticationToken(REMEMBERME_KEY, "admin", + Collections.singleton(new SimpleGrantedAuthority("ROLE_USER"))); + String actualJson = this.mapper.writeValueAsString(token); + JSONAssert.assertEquals(REMEMBERME_AUTH_STRINGPRINCIPAL_JSON, actualJson, true); + } + + @Test + public void serializeRememberMeAuthenticationWithUserToken() throws JsonProcessingException, JSONException { + User user = createDefaultUser(); + RememberMeAuthenticationToken token = new RememberMeAuthenticationToken(REMEMBERME_KEY, user, + user.getAuthorities()); + String actualJson = this.mapper.writeValueAsString(token); + JSONAssert.assertEquals(String.format(REMEMBERME_AUTH_JSON, "\"password\""), actualJson, true); + } + + @Test + public void serializeRememberMeAuthenticationWithUserTokenAfterEraseCredential() + throws JsonProcessingException, JSONException { + User user = createDefaultUser(); + RememberMeAuthenticationToken token = new RememberMeAuthenticationToken(REMEMBERME_KEY, user, + user.getAuthorities()); + token.eraseCredentials(); + String actualJson = this.mapper.writeValueAsString(token); + JSONAssert.assertEquals(REMEMBERME_AUTH_JSON.replace(UserDeserializerTests.USER_PASSWORD, "null"), actualJson, + true); + } + + @Test + public void deserializeRememberMeAuthenticationToken() throws IOException { + RememberMeAuthenticationToken token = this.mapper.readValue(REMEMBERME_AUTH_STRINGPRINCIPAL_JSON, + RememberMeAuthenticationToken.class); + assertThat(token).isNotNull(); + assertThat(token.getPrincipal()).isNotNull().isEqualTo("admin").isEqualTo(token.getName()); + assertThat(token.getAuthorities()).hasSize(1).contains(new SimpleGrantedAuthority("ROLE_USER")); + } + + @Test + public void deserializeRememberMeAuthenticationTokenWithUserTest() throws IOException { + RememberMeAuthenticationToken token = this.mapper.readValue(String.format(REMEMBERME_AUTH_JSON, "\"password\""), + RememberMeAuthenticationToken.class); + assertThat(token).isNotNull(); + assertThat(token.getPrincipal()).isNotNull().isInstanceOf(User.class); + assertThat(((User) token.getPrincipal()).getUsername()).isEqualTo("admin"); + assertThat(((User) token.getPrincipal()).getPassword()).isEqualTo("1234"); + assertThat(((User) token.getPrincipal()).getAuthorities()).hasSize(1) + .contains(new SimpleGrantedAuthority("ROLE_USER")); + assertThat(token.getAuthorities()).hasSize(1).contains(new SimpleGrantedAuthority("ROLE_USER")); + assertThat(((User) token.getPrincipal()).isEnabled()).isEqualTo(true); + } + +} diff --git a/core/src/test/java/org/springframework/security/jackson/SecurityContextMixinTests.java b/core/src/test/java/org/springframework/security/jackson/SecurityContextMixinTests.java new file mode 100644 index 0000000000..a782ecdedf --- /dev/null +++ b/core/src/test/java/org/springframework/security/jackson/SecurityContextMixinTests.java @@ -0,0 +1,69 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.jackson; + +import java.io.IOException; +import java.util.Collection; +import java.util.Collections; + +import com.fasterxml.jackson.core.JsonProcessingException; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; + +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextImpl; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Jitendra Singh + * @since 4.2 + */ +public class SecurityContextMixinTests extends AbstractMixinTests { + + // @formatter:off + public static final String SECURITY_CONTEXT_JSON = "{" + + "\"@class\": \"org.springframework.security.core.context.SecurityContextImpl\", " + + "\"authentication\": " + UsernamePasswordAuthenticationTokenMixinTests.AUTHENTICATED_STRINGPRINCIPAL_JSON + + "}"; + // @formatter:on + @Test + public void securityContextSerializeTest() throws JsonProcessingException, JSONException { + SecurityContext context = new SecurityContextImpl(); + context.setAuthentication(UsernamePasswordAuthenticationToken.authenticated("admin", "1234", + Collections.singleton(new SimpleGrantedAuthority("ROLE_USER")))); + String actualJson = this.mapper.writeValueAsString(context); + JSONAssert.assertEquals(SECURITY_CONTEXT_JSON, actualJson, true); + } + + @Test + public void securityContextDeserializeTest() throws IOException { + SecurityContext context = this.mapper.readValue(SECURITY_CONTEXT_JSON, SecurityContextImpl.class); + assertThat(context).isNotNull(); + assertThat(context.getAuthentication()).isNotNull().isInstanceOf(UsernamePasswordAuthenticationToken.class); + assertThat(context.getAuthentication().getPrincipal()).isEqualTo("admin"); + assertThat(context.getAuthentication().getCredentials()).isEqualTo("1234"); + assertThat(context.getAuthentication().isAuthenticated()).isTrue(); + Collection authorities = context.getAuthentication().getAuthorities(); + assertThat(authorities).hasSize(1); + assertThat(authorities).contains(new SimpleGrantedAuthority("ROLE_USER")); + } + +} diff --git a/core/src/test/java/org/springframework/security/jackson/SecurityJacksonModulesTests.java b/core/src/test/java/org/springframework/security/jackson/SecurityJacksonModulesTests.java new file mode 100644 index 0000000000..b8cf960588 --- /dev/null +++ b/core/src/test/java/org/springframework/security/jackson/SecurityJacksonModulesTests.java @@ -0,0 +1,85 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.jackson; + +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import org.junit.jupiter.api.Test; +import tools.jackson.databind.JacksonModule; +import tools.jackson.databind.json.JsonMapper; +import tools.jackson.databind.jsontype.BasicPolymorphicTypeValidator; + +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.User; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Rob Winch + * @since 5.0 + */ +public class SecurityJacksonModulesTests { + + @Test + public void addModulesWithNoTypeValidatorBuilder() { + ClassLoader loader = getClass().getClassLoader(); + List modules = SecurityJacksonModules.getModules(loader); + JsonMapper mapper = JsonMapper.builder().addModules(modules).build(); + User user = new User("user", null, List.of(new SimpleGrantedAuthority("SCOPE_message:read"))); + String json = mapper.writeValueAsString(user); + User deserializedUer = mapper.readerFor(User.class).readValue(json); + assertThat(deserializedUer).isEqualTo(user); + } + + @Test + public void addModulesWithDefaultTypeValidatorBuilder() { + ClassLoader loader = getClass().getClassLoader(); + List modules = SecurityJacksonModules.getModules(loader, + BasicPolymorphicTypeValidator.builder()); + JsonMapper mapper = JsonMapper.builder().addModules(modules).build(); + User user = new User("user", null, List.of(new SimpleGrantedAuthority("SCOPE_message:read"))); + String json = mapper.writeValueAsString(user); + User deserializedUer = mapper.readerFor(User.class).readValue(json); + assertThat(deserializedUer).isEqualTo(user); + } + + @Test + public void addModulesWithCustomTypeValidator() { + ClassLoader loader = getClass().getClassLoader(); + BasicPolymorphicTypeValidator.Builder builder = BasicPolymorphicTypeValidator.builder() + .allowIfSubType(TestGrantedAuthority.class); + List modules = SecurityJacksonModules.getModules(loader, builder); + JsonMapper mapper = JsonMapper.builder().addModules(modules).build(); + User user = new User("user", null, List.of(new TestGrantedAuthority())); + String json = mapper.writeValueAsString(user); + User deserializedUer = mapper.readerFor(User.class).readValue(json); + assertThat(deserializedUer).isEqualTo(user); + } + + @JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) + private static class TestGrantedAuthority implements GrantedAuthority { + + @Override + public String getAuthority() { + return "test"; + } + + } + +} diff --git a/core/src/test/java/org/springframework/security/jackson/SimpleGrantedAuthorityMixinTests.java b/core/src/test/java/org/springframework/security/jackson/SimpleGrantedAuthorityMixinTests.java new file mode 100644 index 0000000000..f6db6ade98 --- /dev/null +++ b/core/src/test/java/org/springframework/security/jackson/SimpleGrantedAuthorityMixinTests.java @@ -0,0 +1,67 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.jackson; + +import java.io.IOException; + +import com.fasterxml.jackson.core.JsonProcessingException; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; +import tools.jackson.databind.exc.ValueInstantiationException; + +import org.springframework.security.core.authority.SimpleGrantedAuthority; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * @author Jitendra Singh + * @since 4.2 + */ +public class SimpleGrantedAuthorityMixinTests extends AbstractMixinTests { + + // @formatter:off + public static final String AUTHORITY_JSON = "{\"@class\": \"org.springframework.security.core.authority.SimpleGrantedAuthority\", \"authority\": \"ROLE_USER\"}"; + public static final String AUTHORITIES_ARRAYLIST_JSON = "[\"java.util.Collections$UnmodifiableRandomAccessList\", [" + AUTHORITY_JSON + "]]"; + public static final String AUTHORITIES_SET_JSON = "[\"java.util.Collections$UnmodifiableSet\", [" + AUTHORITY_JSON + "]]"; + public static final String NO_AUTHORITIES_ARRAYLIST_JSON = "[\"java.util.Collections$UnmodifiableRandomAccessList\", []]"; + public static final String EMPTY_AUTHORITIES_ARRAYLIST_JSON = "[\"java.util.Collections$EmptyList\", []]"; + public static final String NO_AUTHORITIES_SET_JSON = "[\"java.util.Collections$UnmodifiableSet\", []]"; + // @formatter:on + @Test + public void serializeSimpleGrantedAuthorityTest() throws JsonProcessingException, JSONException { + SimpleGrantedAuthority authority = new SimpleGrantedAuthority("ROLE_USER"); + String serializeJson = this.mapper.writeValueAsString(authority); + JSONAssert.assertEquals(AUTHORITY_JSON, serializeJson, true); + } + + @Test + public void deserializeGrantedAuthorityTest() throws IOException { + SimpleGrantedAuthority authority = this.mapper.readValue(AUTHORITY_JSON, SimpleGrantedAuthority.class); + assertThat(authority).isNotNull(); + assertThat(authority.getAuthority()).isNotNull().isEqualTo("ROLE_USER"); + } + + @Test + public void deserializeGrantedAuthorityWithoutRoleTest() throws IOException { + String json = "{\"@class\": \"org.springframework.security.core.authority.SimpleGrantedAuthority\"}"; + assertThatExceptionOfType(ValueInstantiationException.class) + .isThrownBy(() -> this.mapper.readValue(json, SimpleGrantedAuthority.class)); + } + +} diff --git a/core/src/test/java/org/springframework/security/jackson/UnmodifiableMapTests.java b/core/src/test/java/org/springframework/security/jackson/UnmodifiableMapTests.java new file mode 100644 index 0000000000..046b5aebf8 --- /dev/null +++ b/core/src/test/java/org/springframework/security/jackson/UnmodifiableMapTests.java @@ -0,0 +1,54 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.jackson; + +import java.util.Collections; +import java.util.Map; + +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; + +import static org.assertj.core.api.Assertions.assertThat; + +class UnmodifiableMapTests extends AbstractMixinTests { + + // @formatter:off + private static final String DEFAULT_MAP_JSON = "{" + + "\"@class\": \"java.util.Collections$UnmodifiableMap\"," + + "\"Key\": \"Value\"" + + "}"; + // @formatter:on + + @Test + void shouldSerialize() throws Exception { + String mapJson = mapper + .writeValueAsString(Collections.unmodifiableMap(Collections.singletonMap("Key", "Value"))); + + JSONAssert.assertEquals(DEFAULT_MAP_JSON, mapJson, true); + } + + @Test + void shouldDeserialize() throws Exception { + Map map = mapper.readValue(DEFAULT_MAP_JSON, + Collections.unmodifiableMap(Collections.emptyMap()).getClass()); + + assertThat(map).isNotNull() + .isInstanceOf(Collections.unmodifiableMap(Collections.emptyMap()).getClass()) + .containsAllEntriesOf(Collections.singletonMap("Key", "Value")); + } + +} diff --git a/core/src/test/java/org/springframework/security/jackson/UserDeserializerTests.java b/core/src/test/java/org/springframework/security/jackson/UserDeserializerTests.java new file mode 100644 index 0000000000..634e5e0e1e --- /dev/null +++ b/core/src/test/java/org/springframework/security/jackson/UserDeserializerTests.java @@ -0,0 +1,129 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.jackson; + +import java.io.IOException; +import java.util.Collections; +import java.util.regex.Pattern; + +import com.fasterxml.jackson.core.JsonProcessingException; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; +import tools.jackson.databind.exc.MismatchedInputException; +import tools.jackson.databind.json.JsonMapper; +import tools.jackson.databind.node.ObjectNode; + +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.User; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * @author Jitendra Singh + * @since 4.2 + */ +public class UserDeserializerTests extends AbstractMixinTests { + + public static final String USER_PASSWORD = "\"1234\""; + + // @formatter:off + public static final String USER_JSON = "{" + + "\"@class\": \"org.springframework.security.core.userdetails.User\", " + + "\"username\": \"admin\"," + + " \"password\": " + USER_PASSWORD + ", " + + "\"accountNonExpired\": true, " + + "\"accountNonLocked\": true, " + + "\"credentialsNonExpired\": true, " + + "\"enabled\": true, " + + "\"authorities\": " + SimpleGrantedAuthorityMixinTests.AUTHORITIES_SET_JSON + + "}"; + // @formatter:on + @Test + public void serializeUserTest() throws JsonProcessingException, JSONException { + User user = createDefaultUser(); + String userJson = this.mapper.writeValueAsString(user); + JSONAssert.assertEquals(userWithPasswordJson(user.getPassword()), userJson, true); + } + + @Test + public void serializeUserWithoutAuthority() throws JsonProcessingException, JSONException { + User user = new User("admin", "1234", Collections.emptyList()); + String userJson = this.mapper.writeValueAsString(user); + JSONAssert.assertEquals(userWithNoAuthoritiesJson(), userJson, true); + } + + @Test + public void deserializeUserWithNullPasswordEmptyAuthorityTest() throws IOException { + String userJsonWithoutPasswordString = USER_JSON.replace(SimpleGrantedAuthorityMixinTests.AUTHORITIES_SET_JSON, + "[]"); + assertThatExceptionOfType(MismatchedInputException.class) + .isThrownBy(() -> this.mapper.readValue(userJsonWithoutPasswordString, User.class)); + } + + @Test + public void deserializeUserWithNullPasswordNoAuthorityTest() throws Exception { + String userJsonWithoutPasswordString = removeNode(userWithNoAuthoritiesJson(), this.mapper, "password"); + User user = this.mapper.readValue(userJsonWithoutPasswordString, User.class); + assertThat(user).isNotNull(); + assertThat(user.getUsername()).isEqualTo("admin"); + assertThat(user.getPassword()).isNull(); + assertThat(user.getAuthorities()).isEmpty(); + assertThat(user.isEnabled()).isEqualTo(true); + } + + @Test + public void deserializeUserWithNoClassIdInAuthoritiesTest() throws Exception { + String userJson = USER_JSON.replace(SimpleGrantedAuthorityMixinTests.AUTHORITIES_SET_JSON, + "[{\"authority\": \"ROLE_USER\"}]"); + assertThatExceptionOfType(MismatchedInputException.class) + .isThrownBy(() -> this.mapper.readValue(userJson, User.class)); + } + + @Test + public void deserializeUserWithClassIdInAuthoritiesTest() { + User user = this.mapper.readValue(userJson(), User.class); + assertThat(user).isNotNull(); + assertThat(user.getUsername()).isEqualTo("admin"); + assertThat(user.getPassword()).isEqualTo("1234"); + assertThat(user.getAuthorities()).hasSize(1).contains(new SimpleGrantedAuthority("ROLE_USER")); + } + + private String removeNode(String json, JsonMapper mapper, String toRemove) throws Exception { + ObjectNode node = mapper.createParser(json).readValueAsTree(); + node.remove(toRemove); + String result = mapper.writeValueAsString(node); + JSONAssert.assertNotEquals(json, result, false); + return result; + } + + public static String userJson() { + return USER_JSON; + } + + public static String userWithPasswordJson(String password) { + return userJson().replaceAll(Pattern.quote(USER_PASSWORD), "\"" + password + "\""); + } + + public static String userWithNoAuthoritiesJson() { + return userJson().replace(SimpleGrantedAuthorityMixinTests.AUTHORITIES_SET_JSON, + SimpleGrantedAuthorityMixinTests.NO_AUTHORITIES_SET_JSON); + } + +} diff --git a/core/src/test/java/org/springframework/security/jackson/UsernamePasswordAuthenticationTokenMixinTests.java b/core/src/test/java/org/springframework/security/jackson/UsernamePasswordAuthenticationTokenMixinTests.java new file mode 100644 index 0000000000..ae9228087d --- /dev/null +++ b/core/src/test/java/org/springframework/security/jackson/UsernamePasswordAuthenticationTokenMixinTests.java @@ -0,0 +1,221 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.jackson; + +import java.io.IOException; +import java.util.ArrayList; + +import com.fasterxml.jackson.annotation.JsonClassDescription; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.annotation.JsonInclude.Value; +import com.fasterxml.jackson.core.JsonProcessingException; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; +import tools.jackson.databind.json.JsonMapper; + +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.User; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Jitendra Singh + * @author Greg Turnquist + * @author Onur Kagan Ozcan + * @since 4.2 + */ +public class UsernamePasswordAuthenticationTokenMixinTests extends AbstractMixinTests { + + private static final String AUTHENTICATED_JSON = "{" + + "\"@class\": \"org.springframework.security.authentication.UsernamePasswordAuthenticationToken\"," + + "\"principal\": " + UserDeserializerTests.USER_JSON + ", " + "\"credentials\": \"1234\", " + + "\"authenticated\": true, " + "\"details\": null, " + "\"authorities\": " + + SimpleGrantedAuthorityMixinTests.AUTHORITIES_ARRAYLIST_JSON + "}"; + + public static final String AUTHENTICATED_STRINGPRINCIPAL_JSON = AUTHENTICATED_JSON + .replace(UserDeserializerTests.USER_JSON, "\"admin\""); + + private static final String NON_USER_PRINCIPAL_JSON = "{" + + "\"@class\": \"org.springframework.security.jackson.UsernamePasswordAuthenticationTokenMixinTests$NonUserPrincipal\", " + + "\"username\": \"admin\"" + "}"; + + private static final String AUTHENTICATED_STRINGDETAILS_JSON = AUTHENTICATED_JSON.replace("\"details\": null, ", + "\"details\": \"details\", "); + + private static final String AUTHENTICATED_NON_USER_PRINCIPAL_JSON = AUTHENTICATED_JSON + .replace(UserDeserializerTests.USER_JSON, NON_USER_PRINCIPAL_JSON) + .replaceAll(UserDeserializerTests.USER_PASSWORD, "null") + .replace(SimpleGrantedAuthorityMixinTests.AUTHORITIES_ARRAYLIST_JSON, + SimpleGrantedAuthorityMixinTests.NO_AUTHORITIES_ARRAYLIST_JSON); + + private static final String UNAUTHENTICATED_STRINGPRINCIPAL_JSON = AUTHENTICATED_STRINGPRINCIPAL_JSON + .replace("\"authenticated\": true, ", "\"authenticated\": false, ") + .replace(SimpleGrantedAuthorityMixinTests.AUTHORITIES_ARRAYLIST_JSON, + SimpleGrantedAuthorityMixinTests.EMPTY_AUTHORITIES_ARRAYLIST_JSON); + + @Test + public void serializeUnauthenticatedUsernamePasswordAuthenticationTokenMixinTest() + throws JsonProcessingException, JSONException { + UsernamePasswordAuthenticationToken token = UsernamePasswordAuthenticationToken.unauthenticated("admin", + "1234"); + String serializedJson = this.mapper.writeValueAsString(token); + JSONAssert.assertEquals(UNAUTHENTICATED_STRINGPRINCIPAL_JSON, serializedJson, true); + } + + @Test + public void serializeAuthenticatedUsernamePasswordAuthenticationTokenMixinTest() + throws JsonProcessingException, JSONException { + User user = createDefaultUser(); + UsernamePasswordAuthenticationToken token = UsernamePasswordAuthenticationToken + .authenticated(user.getUsername(), user.getPassword(), user.getAuthorities()); + String serializedJson = this.mapper.writeValueAsString(token); + JSONAssert.assertEquals(AUTHENTICATED_STRINGPRINCIPAL_JSON, serializedJson, true); + } + + @Test + public void deserializeUnauthenticatedUsernamePasswordAuthenticationTokenMixinTest() { + UsernamePasswordAuthenticationToken token = this.mapper.readValue(UNAUTHENTICATED_STRINGPRINCIPAL_JSON, + UsernamePasswordAuthenticationToken.class); + assertThat(token).isNotNull(); + assertThat(token.isAuthenticated()).isEqualTo(false); + assertThat(token.getAuthorities()).isNotNull().hasSize(0); + } + + @Test + public void deserializeAuthenticatedUsernamePasswordAuthenticationTokenMixinTest() { + UsernamePasswordAuthenticationToken expectedToken = createToken(); + UsernamePasswordAuthenticationToken token = this.mapper.readValue(AUTHENTICATED_STRINGPRINCIPAL_JSON, + UsernamePasswordAuthenticationToken.class); + assertThat(token).isNotNull(); + assertThat(token.isAuthenticated()).isTrue(); + assertThat(token.getAuthorities()).isEqualTo(expectedToken.getAuthorities()); + } + + @Test + public void serializeAuthenticatedUsernamePasswordAuthenticationTokenMixinWithUserTest() + throws JsonProcessingException, JSONException { + UsernamePasswordAuthenticationToken token = createToken(); + String actualJson = this.mapper.writeValueAsString(token); + JSONAssert.assertEquals(AUTHENTICATED_JSON, actualJson, true); + } + + @Test + public void deserializeAuthenticatedUsernamePasswordAuthenticationTokenWithUserTest() throws IOException { + UsernamePasswordAuthenticationToken token = this.mapper.readValue(AUTHENTICATED_JSON, + UsernamePasswordAuthenticationToken.class); + assertThat(token).isNotNull(); + assertThat(token.getPrincipal()).isNotNull().isInstanceOf(User.class); + assertThat(((User) token.getPrincipal()).getAuthorities()).isNotNull() + .hasSize(1) + .contains(new SimpleGrantedAuthority("ROLE_USER")); + assertThat(token.isAuthenticated()).isEqualTo(true); + assertThat(token.getAuthorities()).hasSize(1).contains(new SimpleGrantedAuthority("ROLE_USER")); + } + + @Test + public void serializeAuthenticatedUsernamePasswordAuthenticationTokenMixinAfterEraseCredentialInvoked() + throws JsonProcessingException, JSONException { + UsernamePasswordAuthenticationToken token = createToken(); + token.eraseCredentials(); + String actualJson = this.mapper.writeValueAsString(token); + JSONAssert.assertEquals(AUTHENTICATED_JSON.replaceAll(UserDeserializerTests.USER_PASSWORD, "null"), actualJson, + true); + } + + @Test + public void serializeAuthenticatedUsernamePasswordAuthenticationTokenMixinWithNonUserPrincipalTest() + throws JsonProcessingException, JSONException { + NonUserPrincipal principal = new NonUserPrincipal(); + principal.setUsername("admin"); + UsernamePasswordAuthenticationToken token = UsernamePasswordAuthenticationToken.authenticated(principal, null, + new ArrayList<>()); + String actualJson = this.mapper.writeValueAsString(token); + JSONAssert.assertEquals(AUTHENTICATED_NON_USER_PRINCIPAL_JSON, actualJson, true); + } + + @Test + public void deserializeAuthenticatedUsernamePasswordAuthenticationTokenWithNonUserPrincipalTest() + throws IOException { + UsernamePasswordAuthenticationToken token = this.mapper.readValue(AUTHENTICATED_NON_USER_PRINCIPAL_JSON, + UsernamePasswordAuthenticationToken.class); + assertThat(token).isNotNull(); + assertThat(token.getPrincipal()).isNotNull().isInstanceOf(NonUserPrincipal.class); + } + + @Test + public void deserializeAuthenticatedUsernamePasswordAuthenticationTokenWithDetailsTest() { + UsernamePasswordAuthenticationToken token = this.mapper.readValue(AUTHENTICATED_STRINGDETAILS_JSON, + UsernamePasswordAuthenticationToken.class); + assertThat(token).isNotNull(); + assertThat(token.getPrincipal()).isNotNull().isInstanceOf(User.class); + assertThat(((User) token.getPrincipal()).getAuthorities()).isNotNull() + .hasSize(1) + .contains(new SimpleGrantedAuthority("ROLE_USER")); + assertThat(token.isAuthenticated()).isEqualTo(true); + assertThat(token.getAuthorities()).hasSize(1).contains(new SimpleGrantedAuthority("ROLE_USER")); + assertThat(token.getDetails()).isExactlyInstanceOf(String.class).isEqualTo("details"); + } + + @Test + public void serializingThenDeserializingWithNoCredentialsOrDetailsShouldWork() { + UsernamePasswordAuthenticationToken original = UsernamePasswordAuthenticationToken.unauthenticated("Frodo", + null); + String serialized = this.mapper.writeValueAsString(original); + UsernamePasswordAuthenticationToken deserialized = this.mapper.readValue(serialized, + UsernamePasswordAuthenticationToken.class); + assertThat(deserialized).isEqualTo(original); + } + + @Test + public void serializingThenDeserializingWithConfiguredJsontMapperShouldWork() { + JsonMapper jsonMapper = this.mapper.rebuild() + .changeDefaultPropertyInclusion((p) -> Value.construct(Include.NON_ABSENT, Include.NON_ABSENT)) + .build(); + + UsernamePasswordAuthenticationToken original = UsernamePasswordAuthenticationToken.unauthenticated("Frodo", + null); + String serialized = jsonMapper.writeValueAsString(original); + UsernamePasswordAuthenticationToken deserialized = jsonMapper.readValue(serialized, + UsernamePasswordAuthenticationToken.class); + assertThat(deserialized).isEqualTo(original); + } + + private UsernamePasswordAuthenticationToken createToken() { + User user = createDefaultUser(); + UsernamePasswordAuthenticationToken token = UsernamePasswordAuthenticationToken.authenticated(user, + user.getPassword(), user.getAuthorities()); + return token; + } + + @JsonClassDescription + public static class NonUserPrincipal { + + private String username; + + public String getUsername() { + return this.username; + } + + public void setUsername(String username) { + this.username = username; + } + + } + +} diff --git a/docs/modules/ROOT/pages/features/integrations/jackson.adoc b/docs/modules/ROOT/pages/features/integrations/jackson.adoc index 561d23ec6d..d9cfbe6e75 100644 --- a/docs/modules/ROOT/pages/features/integrations/jackson.adoc +++ b/docs/modules/ROOT/pages/features/integrations/jackson.adoc @@ -4,7 +4,7 @@ Spring Security provides Jackson support for persisting Spring Security related classes. This can improve the performance of serializing Spring Security related classes when working with distributed sessions (i.e. session replication, Spring Session, etc). -To use it, register the `SecurityJackson2Modules.getModules(ClassLoader)` with `ObjectMapper` (https://github.com/FasterXML/jackson-databind[jackson-databind]): +To use it, register the `SecurityJacksonModules.getModules(ClassLoader)` with `JsonMapper.Builder` (https://github.com/FasterXML/jackson-databind[jackson-databind]): [tabs] ====== @@ -12,12 +12,12 @@ Java:: + [source,java,role="primary"] ---- -ObjectMapper mapper = new ObjectMapper(); ClassLoader loader = getClass().getClassLoader(); -List modules = SecurityJackson2Modules.getModules(loader); -mapper.registerModules(modules); +JsonMapper mapper = JsonMapper.builder() + .addModules(SecurityJacksonModules.getModules(loader)) + .build(); -// ... use ObjectMapper as normally ... +// ... use JsonMapper as normally ... SecurityContext context = new SecurityContextImpl(); // ... String json = mapper.writeValueAsString(context); @@ -27,12 +27,12 @@ Kotlin:: + [source,kotlin,role="secondary"] ---- -val mapper = ObjectMapper() val loader = javaClass.classLoader -val modules: MutableList = SecurityJackson2Modules.getModules(loader) -mapper.registerModules(modules) +val mapper = JsonMapper.builder() + .addModules(SecurityJacksonModules.getModules(loader)) + .build() -// ... use ObjectMapper as normally ... +// ... use JsonMapper as normally ... val context: SecurityContext = SecurityContextImpl() // ... val json: String = mapper.writeValueAsString(context) @@ -43,8 +43,8 @@ val json: String = mapper.writeValueAsString(context) ==== The following Spring Security modules provide Jackson support: -- spring-security-core (`CoreJackson2Module`) -- spring-security-web (`WebJackson2Module`, `WebServletJackson2Module`, `WebServerJackson2Module`) -- xref:servlet/oauth2/client/index.adoc#oauth2client[ spring-security-oauth2-client] (`OAuth2ClientJackson2Module`) -- spring-security-cas (`CasJackson2Module`) +- spring-security-core (`CoreJacksonModule`) +- spring-security-web (`WebJacksonModule`, `WebServletJacksonModule`, `WebServerJacksonModule`) +- xref:servlet/oauth2/client/index.adoc#oauth2client[ spring-security-oauth2-client] (`OAuth2ClientJacksonModule`) +- spring-security-cas (`CasJacksonModule`) ==== diff --git a/docs/modules/ROOT/pages/servlet/integrations/jackson.adoc b/docs/modules/ROOT/pages/servlet/integrations/jackson.adoc index d8f7eabd52..dc7016edc0 100644 --- a/docs/modules/ROOT/pages/servlet/integrations/jackson.adoc +++ b/docs/modules/ROOT/pages/servlet/integrations/jackson.adoc @@ -4,16 +4,16 @@ Spring Security provides Jackson support for persisting Spring Security-related classes. This can improve the performance of serializing Spring Security-related classes when working with distributed sessions (session replication, Spring Session, and so on). -To use it, register the `SecurityJackson2Modules.getModules(ClassLoader)` with `ObjectMapper` (https://github.com/FasterXML/jackson-databind[jackson-databind]): +To use it, register the `SecurityJacksonModules.getModules(ClassLoader)` with `JsonMapper.Builder` (https://github.com/FasterXML/jackson-databind[jackson-databind]): [source,java] ---- -ObjectMapper mapper = new ObjectMapper(); ClassLoader loader = getClass().getClassLoader(); -List modules = SecurityJackson2Modules.getModules(loader); -mapper.registerModules(modules); +JsonMapper mapper = JsonMapper.builder() + .addModules(SecurityJacksonModules.getModules(loader)) + .build(); -// ... use ObjectMapper as normally ... +// ... use JsonMapper as normally ... SecurityContext context = new SecurityContextImpl(); // ... String json = mapper.writeValueAsString(context); @@ -23,8 +23,8 @@ String json = mapper.writeValueAsString(context); ==== The following Spring Security modules provide Jackson support: -- spring-security-core (javadoc:org.springframework.security.jackson2.CoreJackson2Module[]) -- spring-security-web (javadoc:org.springframework.security.web.jackson2.WebJackson2Module[], javadoc:org.springframework.security.web.jackson2.WebServletJackson2Module[], javadoc:org.springframework.security.web.server.jackson2.WebServerJackson2Module[]) -- <> (javadoc:org.springframework.security.oauth2.client.jackson2.OAuth2ClientJackson2Module[]) -- spring-security-cas (javadoc:org.springframework.security.cas.jackson2.CasJackson2Module[]) +- spring-security-core (javadoc:org.springframework.security.jackson.CoreJacksonModule[]) +- spring-security-web (javadoc:org.springframework.security.web.jackson.WebJacksonModule[], javadoc:org.springframework.security.web.jackson.WebServletJacksonModule[], javadoc:org.springframework.security.web.server.jackson.WebServerJacksonModule[]) +- <> (javadoc:org.springframework.security.oauth2.client.jackson.OAuth2ClientJacksonModule[]) +- spring-security-cas (javadoc:org.springframework.security.cas.jackson.CasJacksonModule[]) ==== diff --git a/ldap/spring-security-ldap.gradle b/ldap/spring-security-ldap.gradle index f4d6abfb32..8c9b39ce81 100644 --- a/ldap/spring-security-ldap.gradle +++ b/ldap/spring-security-ldap.gradle @@ -11,6 +11,7 @@ dependencies { optional 'com.fasterxml.jackson.core:jackson-databind' optional 'ldapsdk:ldapsdk' optional "com.unboundid:unboundid-ldapsdk" + optional 'tools.jackson.core:jackson-databind' api ('org.springframework.ldap:spring-ldap-core') { exclude(group: 'commons-logging', module: 'commons-logging') exclude(group: 'org.springframework', module: 'spring-beans') diff --git a/ldap/src/main/java/org/springframework/security/ldap/jackson/InetOrgPersonMixin.java b/ldap/src/main/java/org/springframework/security/ldap/jackson/InetOrgPersonMixin.java new file mode 100644 index 0000000000..2de4a6db90 --- /dev/null +++ b/ldap/src/main/java/org/springframework/security/ldap/jackson/InetOrgPersonMixin.java @@ -0,0 +1,38 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.ldap.jackson; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +import org.springframework.security.jackson.SecurityJacksonModules; +import org.springframework.security.ldap.userdetails.InetOrgPerson; + +/** + * This Jackson mixin is used to serialize/deserialize {@link InetOrgPerson}. + * + * @author Sebastien Deleuze + * @since 7.0 + * @see LdapJacksonModule + * @see SecurityJacksonModules + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) +@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE, + isGetterVisibility = JsonAutoDetect.Visibility.NONE) +abstract class InetOrgPersonMixin { + +} diff --git a/ldap/src/main/java/org/springframework/security/ldap/jackson/LdapAuthorityMixin.java b/ldap/src/main/java/org/springframework/security/ldap/jackson/LdapAuthorityMixin.java new file mode 100644 index 0000000000..6ec3a3d477 --- /dev/null +++ b/ldap/src/main/java/org/springframework/security/ldap/jackson/LdapAuthorityMixin.java @@ -0,0 +1,47 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.ldap.jackson; + +import java.util.List; +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +import org.springframework.security.jackson.SecurityJacksonModules; +import org.springframework.security.ldap.userdetails.LdapAuthority; + +/** + * This Jackson mixin is used to serialize/deserialize {@link LdapAuthority}. + * + * @author Sebastien Deleuze + * @since 7.0 + * @see LdapJacksonModule + * @see SecurityJacksonModules + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) +@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE) +abstract class LdapAuthorityMixin { + + @JsonCreator + LdapAuthorityMixin(@JsonProperty("role") String role, @JsonProperty("dn") String dn, + @JsonProperty("attributes") Map> attributes) { + } + +} diff --git a/ldap/src/main/java/org/springframework/security/ldap/jackson/LdapJacksonModule.java b/ldap/src/main/java/org/springframework/security/ldap/jackson/LdapJacksonModule.java new file mode 100644 index 0000000000..b157733f7e --- /dev/null +++ b/ldap/src/main/java/org/springframework/security/ldap/jackson/LdapJacksonModule.java @@ -0,0 +1,71 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.ldap.jackson; + +import tools.jackson.core.Version; +import tools.jackson.databind.jsontype.BasicPolymorphicTypeValidator; + +import org.springframework.security.jackson.SecurityJacksonModule; +import org.springframework.security.jackson.SecurityJacksonModules; +import org.springframework.security.ldap.userdetails.InetOrgPerson; +import org.springframework.security.ldap.userdetails.LdapAuthority; +import org.springframework.security.ldap.userdetails.LdapUserDetailsImpl; +import org.springframework.security.ldap.userdetails.Person; + +/** + * Jackson module for {@code spring-security-ldap}. This module registers + * {@link LdapAuthorityMixin}, {@link LdapUserDetailsImplMixin}, {@link PersonMixin}, + * {@link InetOrgPersonMixin}. + * + *

+ * The recommended way to configure it is to use {@link SecurityJacksonModules} in order + * to enable properly automatic inclusion of type information with related validation. + * + *

+ *     ClassLoader loader = getClass().getClassLoader();
+ *     JsonMapper mapper = JsonMapper.builder()
+ * 				.addModules(SecurityJacksonModules.getModules(loader))
+ * 				.build();
+ * 
+ * + * @author Sebastien Deleuze + * @since 7.0 + * @see SecurityJacksonModules + */ +@SuppressWarnings("serial") +public class LdapJacksonModule extends SecurityJacksonModule { + + public LdapJacksonModule() { + super(LdapJacksonModule.class.getName(), new Version(1, 0, 0, null, null, null)); + } + + @Override + public void configurePolymorphicTypeValidator(BasicPolymorphicTypeValidator.Builder builder) { + builder.allowIfSubType(InetOrgPerson.class) + .allowIfSubType(LdapUserDetailsImpl.class) + .allowIfSubType(Person.class); + } + + @Override + public void setupModule(SetupContext context) { + context.setMixIn(LdapAuthority.class, LdapAuthorityMixin.class); + context.setMixIn(LdapUserDetailsImpl.class, LdapUserDetailsImplMixin.class); + context.setMixIn(Person.class, PersonMixin.class); + context.setMixIn(InetOrgPerson.class, InetOrgPersonMixin.class); + } + +} diff --git a/ldap/src/main/java/org/springframework/security/ldap/jackson/LdapUserDetailsImplMixin.java b/ldap/src/main/java/org/springframework/security/ldap/jackson/LdapUserDetailsImplMixin.java new file mode 100644 index 0000000000..9fb799dca3 --- /dev/null +++ b/ldap/src/main/java/org/springframework/security/ldap/jackson/LdapUserDetailsImplMixin.java @@ -0,0 +1,38 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.ldap.jackson; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +import org.springframework.security.jackson.SecurityJacksonModules; +import org.springframework.security.ldap.userdetails.LdapUserDetailsImpl; + +/** + * This Jackson mixin is used to serialize/deserialize {@link LdapUserDetailsImpl}. + * + * @author Sebastien Deleuze + * @since 7.0 + * @see LdapJacksonModule + * @see SecurityJacksonModules + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) +@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE, + isGetterVisibility = JsonAutoDetect.Visibility.NONE) +abstract class LdapUserDetailsImplMixin { + +} diff --git a/ldap/src/main/java/org/springframework/security/ldap/jackson/PersonMixin.java b/ldap/src/main/java/org/springframework/security/ldap/jackson/PersonMixin.java new file mode 100644 index 0000000000..8f1e1976ca --- /dev/null +++ b/ldap/src/main/java/org/springframework/security/ldap/jackson/PersonMixin.java @@ -0,0 +1,38 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.ldap.jackson; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +import org.springframework.security.jackson.SecurityJacksonModules; +import org.springframework.security.ldap.userdetails.Person; + +/** + * This Jackson mixin is used to serialize/deserialize {@link Person}. + * + * @author Sebastien Deleuze + * @since 7.0 + * @see LdapJacksonModule + * @see SecurityJacksonModules + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) +@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE, + isGetterVisibility = JsonAutoDetect.Visibility.NONE) +abstract class PersonMixin { + +} diff --git a/ldap/src/main/java/org/springframework/security/ldap/jackson/package-info.java b/ldap/src/main/java/org/springframework/security/ldap/jackson/package-info.java new file mode 100644 index 0000000000..1bab3f9573 --- /dev/null +++ b/ldap/src/main/java/org/springframework/security/ldap/jackson/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Jackson 3+ serialization support for LDAP. + */ +package org.springframework.security.ldap.jackson; diff --git a/ldap/src/main/java/org/springframework/security/ldap/jackson2/package-info.java b/ldap/src/main/java/org/springframework/security/ldap/jackson2/package-info.java new file mode 100644 index 0000000000..e8a67d9694 --- /dev/null +++ b/ldap/src/main/java/org/springframework/security/ldap/jackson2/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Jackson 2 serialization support for LDAP. + */ +package org.springframework.security.ldap.jackson2; diff --git a/ldap/src/test/java/org/springframework/security/ldap/jackson/InetOrgPersonMixinTests.java b/ldap/src/test/java/org/springframework/security/ldap/jackson/InetOrgPersonMixinTests.java new file mode 100644 index 0000000000..cbde962dd3 --- /dev/null +++ b/ldap/src/test/java/org/springframework/security/ldap/jackson/InetOrgPersonMixinTests.java @@ -0,0 +1,194 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.ldap.jackson; + +import org.json.JSONException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; +import tools.jackson.core.JacksonException; +import tools.jackson.databind.json.JsonMapper; + +import org.springframework.ldap.core.DirContextAdapter; +import org.springframework.ldap.support.LdapNameBuilder; +import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.security.jackson.SecurityJacksonModules; +import org.springframework.security.ldap.userdetails.InetOrgPerson; +import org.springframework.security.ldap.userdetails.InetOrgPersonContextMapper; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * Tests for {@link org.springframework.security.ldap.jackson.InetOrgPersonMixin}. + */ +public class InetOrgPersonMixinTests { + + private static final String USER_PASSWORD = "Password1234"; + + private static final String AUTHORITIES_ARRAYLIST_JSON = "[\"java.util.Collections$UnmodifiableRandomAccessList\", []]"; + + // @formatter:off + private static final String INET_ORG_PERSON_JSON = "{\n" + + "\"@class\": \"org.springframework.security.ldap.userdetails.InetOrgPerson\"," + + "\"dn\": \"ignored=ignored\"," + + "\"uid\": \"ghengis\"," + + "\"username\": \"ghengis\"," + + "\"password\": \"" + USER_PASSWORD + "\"," + + "\"carLicense\": \"HORS1\"," + + "\"givenName\": \"Ghengis\"," + + "\"destinationIndicator\": \"West\"," + + "\"displayName\": \"Ghengis McCann\"," + + "\"givenName\": \"Ghengis\"," + + "\"homePhone\": \"+467575436521\"," + + "\"initials\": \"G\"," + + "\"employeeNumber\": \"00001\"," + + "\"homePostalAddress\": \"Steppes\"," + + "\"mail\": \"ghengis@mongolia\"," + + "\"mobile\": \"always\"," + + "\"o\": \"Hordes\"," + + "\"ou\": \"Horde1\"," + + "\"postalAddress\": \"On the Move\"," + + "\"postalCode\": \"Changes Frequently\"," + + "\"roomNumber\": \"Yurt 1\"," + + "\"sn\": \"Khan\"," + + "\"street\": \"Westward Avenue\"," + + "\"telephoneNumber\": \"+442075436521\"," + + "\"departmentNumber\": \"5679\"," + + "\"title\": \"T\"," + + "\"cn\": [\"java.util.Arrays$ArrayList\",[\"Ghengis Khan\"]]," + + "\"description\": \"Scary\"," + + "\"accountNonExpired\": true, " + + "\"accountNonLocked\": true, " + + "\"credentialsNonExpired\": true, " + + "\"enabled\": true, " + + "\"authorities\": " + AUTHORITIES_ARRAYLIST_JSON + "," + + "\"graceLoginsRemaining\": " + Integer.MAX_VALUE + "," + + "\"timeBeforeExpiration\": " + Integer.MAX_VALUE + + "}"; + // @formatter:on + + private JsonMapper mapper; + + @BeforeEach + public void setup() { + ClassLoader loader = getClass().getClassLoader(); + this.mapper = JsonMapper.builder().addModules(SecurityJacksonModules.getModules(loader)).build(); + } + + @Test + public void serializeWhenMixinRegisteredThenSerializes() throws Exception { + InetOrgPersonContextMapper mapper = new InetOrgPersonContextMapper(); + InetOrgPerson p = (InetOrgPerson) mapper.mapUserFromContext(createUserContext(), "ghengis", + AuthorityUtils.NO_AUTHORITIES); + + String json = this.mapper.writeValueAsString(p); + JSONAssert.assertEquals(INET_ORG_PERSON_JSON, json, true); + } + + @Test + public void serializeWhenEraseCredentialInvokedThenUserPasswordIsNull() throws JacksonException, JSONException { + InetOrgPersonContextMapper mapper = new InetOrgPersonContextMapper(); + InetOrgPerson p = (InetOrgPerson) mapper.mapUserFromContext(createUserContext(), "ghengis", + AuthorityUtils.NO_AUTHORITIES); + p.eraseCredentials(); + String actualJson = this.mapper.writeValueAsString(p); + JSONAssert.assertEquals(INET_ORG_PERSON_JSON.replaceAll("\"" + USER_PASSWORD + "\"", "null"), actualJson, true); + } + + @Test + public void deserializeWhenMixinNotRegisteredThenThrowJsonProcessingException() { + assertThatExceptionOfType(JacksonException.class) + .isThrownBy(() -> new JsonMapper().readValue(INET_ORG_PERSON_JSON, InetOrgPerson.class)); + } + + @Test + public void deserializeWhenMixinRegisteredThenDeserializes() throws Exception { + InetOrgPersonContextMapper mapper = new InetOrgPersonContextMapper(); + InetOrgPerson expectedAuthentication = (InetOrgPerson) mapper.mapUserFromContext(createUserContext(), "ghengis", + AuthorityUtils.NO_AUTHORITIES); + + InetOrgPerson authentication = this.mapper.readValue(INET_ORG_PERSON_JSON, InetOrgPerson.class); + assertThat(authentication.getAuthorities()).containsExactlyElementsOf(expectedAuthentication.getAuthorities()); + assertThat(authentication.getCarLicense()).isEqualTo(expectedAuthentication.getCarLicense()); + assertThat(authentication.getDepartmentNumber()).isEqualTo(expectedAuthentication.getDepartmentNumber()); + assertThat(authentication.getDestinationIndicator()) + .isEqualTo(expectedAuthentication.getDestinationIndicator()); + assertThat(authentication.getDn()).isEqualTo(expectedAuthentication.getDn()); + assertThat(authentication.getDescription()).isEqualTo(expectedAuthentication.getDescription()); + assertThat(authentication.getDisplayName()).isEqualTo(expectedAuthentication.getDisplayName()); + assertThat(authentication.getUid()).isEqualTo(expectedAuthentication.getUid()); + assertThat(authentication.getUsername()).isEqualTo(expectedAuthentication.getUsername()); + assertThat(authentication.getPassword()).isEqualTo(expectedAuthentication.getPassword()); + assertThat(authentication.getHomePhone()).isEqualTo(expectedAuthentication.getHomePhone()); + assertThat(authentication.getEmployeeNumber()).isEqualTo(expectedAuthentication.getEmployeeNumber()); + assertThat(authentication.getHomePostalAddress()).isEqualTo(expectedAuthentication.getHomePostalAddress()); + assertThat(authentication.getInitials()).isEqualTo(expectedAuthentication.getInitials()); + assertThat(authentication.getMail()).isEqualTo(expectedAuthentication.getMail()); + assertThat(authentication.getMobile()).isEqualTo(expectedAuthentication.getMobile()); + assertThat(authentication.getO()).isEqualTo(expectedAuthentication.getO()); + assertThat(authentication.getOu()).isEqualTo(expectedAuthentication.getOu()); + assertThat(authentication.getPostalAddress()).isEqualTo(expectedAuthentication.getPostalAddress()); + assertThat(authentication.getPostalCode()).isEqualTo(expectedAuthentication.getPostalCode()); + assertThat(authentication.getRoomNumber()).isEqualTo(expectedAuthentication.getRoomNumber()); + assertThat(authentication.getStreet()).isEqualTo(expectedAuthentication.getStreet()); + assertThat(authentication.getSn()).isEqualTo(expectedAuthentication.getSn()); + assertThat(authentication.getTitle()).isEqualTo(expectedAuthentication.getTitle()); + assertThat(authentication.getGivenName()).isEqualTo(expectedAuthentication.getGivenName()); + assertThat(authentication.getTelephoneNumber()).isEqualTo(expectedAuthentication.getTelephoneNumber()); + assertThat(authentication.getGraceLoginsRemaining()) + .isEqualTo(expectedAuthentication.getGraceLoginsRemaining()); + assertThat(authentication.getTimeBeforeExpiration()) + .isEqualTo(expectedAuthentication.getTimeBeforeExpiration()); + assertThat(authentication.isAccountNonExpired()).isEqualTo(expectedAuthentication.isAccountNonExpired()); + assertThat(authentication.isAccountNonLocked()).isEqualTo(expectedAuthentication.isAccountNonLocked()); + assertThat(authentication.isEnabled()).isEqualTo(expectedAuthentication.isEnabled()); + assertThat(authentication.isCredentialsNonExpired()) + .isEqualTo(expectedAuthentication.isCredentialsNonExpired()); + } + + private DirContextAdapter createUserContext() { + DirContextAdapter ctx = new DirContextAdapter(); + ctx.setDn(LdapNameBuilder.newInstance("ignored=ignored").build()); + ctx.setAttributeValue("uid", "ghengis"); + ctx.setAttributeValue("userPassword", USER_PASSWORD); + ctx.setAttributeValue("carLicense", "HORS1"); + ctx.setAttributeValue("cn", "Ghengis Khan"); + ctx.setAttributeValue("description", "Scary"); + ctx.setAttributeValue("destinationIndicator", "West"); + ctx.setAttributeValue("displayName", "Ghengis McCann"); + ctx.setAttributeValue("givenName", "Ghengis"); + ctx.setAttributeValue("homePhone", "+467575436521"); + ctx.setAttributeValue("initials", "G"); + ctx.setAttributeValue("employeeNumber", "00001"); + ctx.setAttributeValue("homePostalAddress", "Steppes"); + ctx.setAttributeValue("mail", "ghengis@mongolia"); + ctx.setAttributeValue("mobile", "always"); + ctx.setAttributeValue("o", "Hordes"); + ctx.setAttributeValue("ou", "Horde1"); + ctx.setAttributeValue("postalAddress", "On the Move"); + ctx.setAttributeValue("postalCode", "Changes Frequently"); + ctx.setAttributeValue("roomNumber", "Yurt 1"); + ctx.setAttributeValue("sn", "Khan"); + ctx.setAttributeValue("street", "Westward Avenue"); + ctx.setAttributeValue("telephoneNumber", "+442075436521"); + ctx.setAttributeValue("departmentNumber", "5679"); + ctx.setAttributeValue("title", "T"); + return ctx; + } + +} diff --git a/ldap/src/test/java/org/springframework/security/ldap/jackson/LdapUserDetailsImplMixinTests.java b/ldap/src/test/java/org/springframework/security/ldap/jackson/LdapUserDetailsImplMixinTests.java new file mode 100644 index 0000000000..26cd4b3f3a --- /dev/null +++ b/ldap/src/test/java/org/springframework/security/ldap/jackson/LdapUserDetailsImplMixinTests.java @@ -0,0 +1,124 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.ldap.jackson; + +import org.json.JSONException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; +import tools.jackson.core.JacksonException; +import tools.jackson.databind.json.JsonMapper; + +import org.springframework.ldap.core.DirContextAdapter; +import org.springframework.ldap.support.LdapNameBuilder; +import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.security.jackson.SecurityJacksonModules; +import org.springframework.security.ldap.userdetails.LdapUserDetailsImpl; +import org.springframework.security.ldap.userdetails.LdapUserDetailsMapper; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * Tests for {@link org.springframework.security.ldap.jackson.LdapUserDetailsImplMixin}. + */ +public class LdapUserDetailsImplMixinTests { + + private static final String USER_PASSWORD = "Password1234"; + + private static final String AUTHORITIES_ARRAYLIST_JSON = "[\"java.util.Collections$UnmodifiableRandomAccessList\", []]"; + + // @formatter:off + private static final String USER_JSON = "{" + + "\"@class\": \"org.springframework.security.ldap.userdetails.LdapUserDetailsImpl\", " + + "\"dn\": \"ignored=ignored\"," + + "\"username\": \"ghengis\"," + + "\"password\": \"" + USER_PASSWORD + "\"," + + "\"accountNonExpired\": true, " + + "\"accountNonLocked\": true, " + + "\"credentialsNonExpired\": true, " + + "\"enabled\": true, " + + "\"authorities\": " + AUTHORITIES_ARRAYLIST_JSON + "," + + "\"graceLoginsRemaining\": " + Integer.MAX_VALUE + "," + + "\"timeBeforeExpiration\": " + Integer.MAX_VALUE + + "}"; + // @formatter:on + + private JsonMapper mapper; + + @BeforeEach + public void setup() { + ClassLoader loader = getClass().getClassLoader(); + this.mapper = JsonMapper.builder().addModules(SecurityJacksonModules.getModules(loader)).build(); + } + + @Test + public void serializeWhenMixinRegisteredThenSerializes() throws Exception { + LdapUserDetailsMapper mapper = new LdapUserDetailsMapper(); + LdapUserDetailsImpl p = (LdapUserDetailsImpl) mapper.mapUserFromContext(createUserContext(), "ghengis", + AuthorityUtils.NO_AUTHORITIES); + + String json = this.mapper.writeValueAsString(p); + JSONAssert.assertEquals(USER_JSON, json, true); + } + + @Test + public void serializeWhenEraseCredentialInvokedThenUserPasswordIsNull() throws JacksonException, JSONException { + LdapUserDetailsMapper mapper = new LdapUserDetailsMapper(); + LdapUserDetailsImpl p = (LdapUserDetailsImpl) mapper.mapUserFromContext(createUserContext(), "ghengis", + AuthorityUtils.NO_AUTHORITIES); + p.eraseCredentials(); + String actualJson = this.mapper.writeValueAsString(p); + JSONAssert.assertEquals(USER_JSON.replaceAll("\"" + USER_PASSWORD + "\"", "null"), actualJson, true); + } + + @Test + public void deserializeWhenMixinNotRegisteredThenThrowJsonProcessingException() { + assertThatExceptionOfType(JacksonException.class) + .isThrownBy(() -> new JsonMapper().readValue(USER_JSON, LdapUserDetailsImpl.class)); + } + + @Test + public void deserializeWhenMixinRegisteredThenDeserializes() throws Exception { + LdapUserDetailsMapper mapper = new LdapUserDetailsMapper(); + LdapUserDetailsImpl expectedAuthentication = (LdapUserDetailsImpl) mapper + .mapUserFromContext(createUserContext(), "ghengis", AuthorityUtils.NO_AUTHORITIES); + + LdapUserDetailsImpl authentication = this.mapper.readValue(USER_JSON, LdapUserDetailsImpl.class); + assertThat(authentication.getAuthorities()).containsExactlyElementsOf(expectedAuthentication.getAuthorities()); + assertThat(authentication.getDn()).isEqualTo(expectedAuthentication.getDn()); + assertThat(authentication.getUsername()).isEqualTo(expectedAuthentication.getUsername()); + assertThat(authentication.getPassword()).isEqualTo(expectedAuthentication.getPassword()); + assertThat(authentication.getGraceLoginsRemaining()) + .isEqualTo(expectedAuthentication.getGraceLoginsRemaining()); + assertThat(authentication.getTimeBeforeExpiration()) + .isEqualTo(expectedAuthentication.getTimeBeforeExpiration()); + assertThat(authentication.isAccountNonExpired()).isEqualTo(expectedAuthentication.isAccountNonExpired()); + assertThat(authentication.isAccountNonLocked()).isEqualTo(expectedAuthentication.isAccountNonLocked()); + assertThat(authentication.isEnabled()).isEqualTo(expectedAuthentication.isEnabled()); + assertThat(authentication.isCredentialsNonExpired()) + .isEqualTo(expectedAuthentication.isCredentialsNonExpired()); + } + + private DirContextAdapter createUserContext() { + DirContextAdapter ctx = new DirContextAdapter(); + ctx.setDn(LdapNameBuilder.newInstance("ignored=ignored").build()); + ctx.setAttributeValue("userPassword", USER_PASSWORD); + return ctx; + } + +} diff --git a/ldap/src/test/java/org/springframework/security/ldap/jackson/PersonMixinTests.java b/ldap/src/test/java/org/springframework/security/ldap/jackson/PersonMixinTests.java new file mode 100644 index 0000000000..48ccf940a7 --- /dev/null +++ b/ldap/src/test/java/org/springframework/security/ldap/jackson/PersonMixinTests.java @@ -0,0 +1,137 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.ldap.jackson; + +import org.json.JSONException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; +import tools.jackson.core.JacksonException; +import tools.jackson.databind.json.JsonMapper; + +import org.springframework.ldap.core.DirContextAdapter; +import org.springframework.ldap.support.LdapNameBuilder; +import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.security.jackson.SecurityJacksonModules; +import org.springframework.security.ldap.userdetails.Person; +import org.springframework.security.ldap.userdetails.PersonContextMapper; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * Tests for {@link org.springframework.security.ldap.jackson.PersonMixin}. + */ +@SuppressWarnings("removal") +public class PersonMixinTests { + + private static final String USER_PASSWORD = "Password1234"; + + private static final String AUTHORITIES_ARRAYLIST_JSON = "[\"java.util.Collections$UnmodifiableRandomAccessList\", []]"; + + // @formatter:off + private static final String PERSON_JSON = "{" + + "\"@class\": \"org.springframework.security.ldap.userdetails.Person\", " + + "\"dn\": \"ignored=ignored\"," + + "\"username\": \"ghengis\"," + + "\"password\": \"" + USER_PASSWORD + "\"," + + "\"givenName\": \"Ghengis\"," + + "\"sn\": \"Khan\"," + + "\"cn\": [\"java.util.Arrays$ArrayList\",[\"Ghengis Khan\"]]," + + "\"description\": \"Scary\"," + + "\"telephoneNumber\": \"+442075436521\"," + + "\"accountNonExpired\": true, " + + "\"accountNonLocked\": true, " + + "\"credentialsNonExpired\": true, " + + "\"enabled\": true, " + + "\"authorities\": " + AUTHORITIES_ARRAYLIST_JSON + "," + + "\"graceLoginsRemaining\": " + Integer.MAX_VALUE + "," + + "\"timeBeforeExpiration\": " + Integer.MAX_VALUE + + "}"; + // @formatter:on + + private JsonMapper mapper; + + @BeforeEach + public void setup() { + ClassLoader loader = getClass().getClassLoader(); + this.mapper = JsonMapper.builder().addModules(SecurityJacksonModules.getModules(loader)).build(); + } + + @Test + public void serializeWhenMixinRegisteredThenSerializes() throws Exception { + PersonContextMapper mapper = new PersonContextMapper(); + Person p = (Person) mapper.mapUserFromContext(createUserContext(), "ghengis", AuthorityUtils.NO_AUTHORITIES); + + String json = this.mapper.writeValueAsString(p); + JSONAssert.assertEquals(PERSON_JSON, json, true); + } + + @Test + public void serializeWhenEraseCredentialInvokedThenUserPasswordIsNull() throws JacksonException, JSONException { + PersonContextMapper mapper = new PersonContextMapper(); + Person p = (Person) mapper.mapUserFromContext(createUserContext(), "ghengis", AuthorityUtils.NO_AUTHORITIES); + p.eraseCredentials(); + String actualJson = this.mapper.writeValueAsString(p); + JSONAssert.assertEquals(PERSON_JSON.replaceAll("\"" + USER_PASSWORD + "\"", "null"), actualJson, true); + } + + @Test + public void deserializeWhenMixinNotRegisteredThenThrowJsonProcessingException() { + assertThatExceptionOfType(JacksonException.class) + .isThrownBy(() -> new JsonMapper().readValue(PERSON_JSON, Person.class)); + } + + @Test + public void deserializeWhenMixinRegisteredThenDeserializes() throws Exception { + PersonContextMapper mapper = new PersonContextMapper(); + Person expectedAuthentication = (Person) mapper.mapUserFromContext(createUserContext(), "ghengis", + AuthorityUtils.NO_AUTHORITIES); + + Person authentication = this.mapper.readValue(PERSON_JSON, Person.class); + assertThat(authentication.getAuthorities()).containsExactlyElementsOf(expectedAuthentication.getAuthorities()); + assertThat(authentication.getDn()).isEqualTo(expectedAuthentication.getDn()); + assertThat(authentication.getDescription()).isEqualTo(expectedAuthentication.getDescription()); + assertThat(authentication.getUsername()).isEqualTo(expectedAuthentication.getUsername()); + assertThat(authentication.getPassword()).isEqualTo(expectedAuthentication.getPassword()); + assertThat(authentication.getSn()).isEqualTo(expectedAuthentication.getSn()); + assertThat(authentication.getGivenName()).isEqualTo(expectedAuthentication.getGivenName()); + assertThat(authentication.getTelephoneNumber()).isEqualTo(expectedAuthentication.getTelephoneNumber()); + assertThat(authentication.getGraceLoginsRemaining()) + .isEqualTo(expectedAuthentication.getGraceLoginsRemaining()); + assertThat(authentication.getTimeBeforeExpiration()) + .isEqualTo(expectedAuthentication.getTimeBeforeExpiration()); + assertThat(authentication.isAccountNonExpired()).isEqualTo(expectedAuthentication.isAccountNonExpired()); + assertThat(authentication.isAccountNonLocked()).isEqualTo(expectedAuthentication.isAccountNonLocked()); + assertThat(authentication.isEnabled()).isEqualTo(expectedAuthentication.isEnabled()); + assertThat(authentication.isCredentialsNonExpired()) + .isEqualTo(expectedAuthentication.isCredentialsNonExpired()); + } + + private DirContextAdapter createUserContext() { + DirContextAdapter ctx = new DirContextAdapter(); + ctx.setDn(LdapNameBuilder.newInstance("ignored=ignored").build()); + ctx.setAttributeValue("userPassword", USER_PASSWORD); + ctx.setAttributeValue("cn", "Ghengis Khan"); + ctx.setAttributeValue("description", "Scary"); + ctx.setAttributeValue("givenName", "Ghengis"); + ctx.setAttributeValue("sn", "Khan"); + ctx.setAttributeValue("telephoneNumber", "+442075436521"); + return ctx; + } + +} diff --git a/oauth2/oauth2-authorization-server/spring-security-oauth2-authorization-server.gradle b/oauth2/oauth2-authorization-server/spring-security-oauth2-authorization-server.gradle index ba2da37a3c..beb19bd536 100644 --- a/oauth2/oauth2-authorization-server/spring-security-oauth2-authorization-server.gradle +++ b/oauth2/oauth2-authorization-server/spring-security-oauth2-authorization-server.gradle @@ -11,10 +11,11 @@ dependencies { exclude group: "commons-logging", module: "commons-logging" } api "com.nimbusds:nimbus-jose-jwt" - api "com.fasterxml.jackson.core:jackson-databind" + api 'tools.jackson.core:jackson-databind' optional "com.fasterxml.jackson.datatype:jackson-datatype-jsr310" optional "org.springframework:spring-jdbc" + optional "com.fasterxml.jackson.core:jackson-databind" testImplementation project(":spring-security-test") testImplementation project(path : ':spring-security-oauth2-jose', configuration : 'tests') diff --git a/oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/JdbcOAuth2AuthorizationService.java b/oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/JdbcOAuth2AuthorizationService.java index f23b1ff147..eb0aa4f531 100644 --- a/oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/JdbcOAuth2AuthorizationService.java +++ b/oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/JdbcOAuth2AuthorizationService.java @@ -33,13 +33,15 @@ import java.util.Map; import java.util.Set; import java.util.function.Function; -import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.Module; import com.fasterxml.jackson.databind.ObjectMapper; +import tools.jackson.databind.json.JsonMapper; import org.springframework.aot.hint.RuntimeHints; import org.springframework.aot.hint.RuntimeHintsRegistrar; import org.springframework.context.annotation.ImportRuntimeHints; +import org.springframework.core.ParameterizedTypeReference; import org.springframework.core.io.ClassPathResource; import org.springframework.dao.DataRetrievalFailureException; import org.springframework.jdbc.core.ArgumentPreparedStatementSetter; @@ -64,8 +66,10 @@ import org.springframework.security.oauth2.core.oidc.OidcIdToken; import org.springframework.security.oauth2.core.oidc.endpoint.OidcParameterNames; import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository; +import org.springframework.security.oauth2.server.authorization.jackson.OAuth2AuthorizationServerJacksonModule; import org.springframework.security.oauth2.server.authorization.jackson2.OAuth2AuthorizationServerJackson2Module; import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; @@ -469,16 +473,12 @@ public class JdbcOAuth2AuthorizationService implements OAuth2AuthorizationServic private LobHandler lobHandler = new DefaultLobHandler(); - private ObjectMapper objectMapper = new ObjectMapper(); + private Mapper mapper = (ClassUtils.isPresent("tools.jackson.databind.json.JsonMapper", + OAuth2AuthorizationRowMapper.class.getClassLoader())) ? new JacksonDelegate() : new Jackson2Delegate(); public OAuth2AuthorizationRowMapper(RegisteredClientRepository registeredClientRepository) { Assert.notNull(registeredClientRepository, "registeredClientRepository cannot be null"); this.registeredClientRepository = registeredClientRepository; - - ClassLoader classLoader = JdbcOAuth2AuthorizationService.class.getClassLoader(); - List securityModules = SecurityJackson2Modules.getModules(classLoader); - this.objectMapper.registerModules(securityModules); - this.objectMapper.registerModule(new OAuth2AuthorizationServerJackson2Module()); } @Override @@ -623,9 +623,9 @@ public class JdbcOAuth2AuthorizationService implements OAuth2AuthorizationServic this.lobHandler = lobHandler; } - public final void setObjectMapper(ObjectMapper objectMapper) { - Assert.notNull(objectMapper, "objectMapper cannot be null"); - this.objectMapper = objectMapper; + public final void setMapper(Mapper mapper) { + Assert.notNull(mapper, "objectMapper cannot be null"); + this.mapper = mapper; } protected final RegisteredClientRepository getRegisteredClientRepository() { @@ -636,13 +636,13 @@ public class JdbcOAuth2AuthorizationService implements OAuth2AuthorizationServic return this.lobHandler; } - protected final ObjectMapper getObjectMapper() { - return this.objectMapper; + protected final Mapper getMapper() { + return this.mapper; } private Map parseMap(String data) { try { - return this.objectMapper.readValue(data, new TypeReference<>() { + return this.mapper.readValue(data, new ParameterizedTypeReference<>() { }); } catch (Exception ex) { @@ -659,13 +659,10 @@ public class JdbcOAuth2AuthorizationService implements OAuth2AuthorizationServic public static class OAuth2AuthorizationParametersMapper implements Function> { - private ObjectMapper objectMapper = new ObjectMapper(); + private Mapper mapper = (ClassUtils.isPresent("tools.jackson.databind.json.JsonMapper", + OAuth2AuthorizationRowMapper.class.getClassLoader())) ? new JacksonDelegate() : new Jackson2Delegate(); public OAuth2AuthorizationParametersMapper() { - ClassLoader classLoader = JdbcOAuth2AuthorizationService.class.getClassLoader(); - List securityModules = SecurityJackson2Modules.getModules(classLoader); - this.objectMapper.registerModules(securityModules); - this.objectMapper.registerModule(new OAuth2AuthorizationServerJackson2Module()); } @Override @@ -737,13 +734,13 @@ public class JdbcOAuth2AuthorizationService implements OAuth2AuthorizationServic return parameters; } - public final void setObjectMapper(ObjectMapper objectMapper) { - Assert.notNull(objectMapper, "objectMapper cannot be null"); - this.objectMapper = objectMapper; + public final void setMapper(Mapper mapper) { + Assert.notNull(mapper, "mapper cannot be null"); + this.mapper = mapper; } - protected final ObjectMapper getObjectMapper() { - return this.objectMapper; + protected final Mapper getMapper() { + return this.mapper; } private List toSqlParameterList(String tokenColumnName, @@ -774,7 +771,7 @@ public class JdbcOAuth2AuthorizationService implements OAuth2AuthorizationServic private String writeMap(Map data) { try { - return this.objectMapper.writeValueAsString(data); + return this.mapper.writeValueAsString(data); } catch (Exception ex) { throw new IllegalArgumentException(ex.getMessage(), ex); @@ -851,4 +848,74 @@ public class JdbcOAuth2AuthorizationService implements OAuth2AuthorizationServic } + public interface Mapper { + + String writeValueAsString(Object data); + + T readValue(String value, ParameterizedTypeReference typeReference); + + } + + @SuppressWarnings("removal") + public static class Jackson2Delegate implements Mapper { + + private final ObjectMapper objectMapper = new ObjectMapper(); + + public Jackson2Delegate() { + ClassLoader classLoader = Jackson2Delegate.class.getClassLoader(); + List securityModules = SecurityJackson2Modules.getModules(classLoader); + this.objectMapper.registerModules(securityModules); + this.objectMapper.registerModule(new OAuth2AuthorizationServerJackson2Module()); + } + + @Override + public String writeValueAsString(Object data) { + try { + return this.objectMapper.writeValueAsString(data); + } + catch (JsonProcessingException ex) { + throw new IllegalArgumentException(ex.getMessage(), ex); + } + } + + @Override + public T readValue(String value, ParameterizedTypeReference typeReference) { + try { + com.fasterxml.jackson.databind.JavaType javaType = this.objectMapper.getTypeFactory() + .constructType(typeReference.getType()); + return this.objectMapper.readValue(value, javaType); + } + catch (JsonProcessingException ex) { + throw new IllegalArgumentException(ex.getMessage(), ex); + } + } + + } + + public static class JacksonDelegate implements Mapper { + + private final JsonMapper jsonMapper; + + public JacksonDelegate() { + this.jsonMapper = JsonMapper.builder().addModules(new OAuth2AuthorizationServerJacksonModule()).build(); + } + + public JacksonDelegate(JsonMapper.Builder builder) { + this.jsonMapper = builder.addModules(new OAuth2AuthorizationServerJacksonModule()).build(); + } + + @Override + public String writeValueAsString(Object data) { + return this.jsonMapper.writeValueAsString(data); + } + + @Override + public T readValue(String value, ParameterizedTypeReference typeReference) { + tools.jackson.databind.JavaType javaType = this.jsonMapper.getTypeFactory() + .constructType(typeReference.getType()); + return this.jsonMapper.readValue(value, javaType); + } + + } + } diff --git a/oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/jackson/JsonNodeUtils.java b/oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/jackson/JsonNodeUtils.java new file mode 100644 index 0000000000..84301d1e01 --- /dev/null +++ b/oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/jackson/JsonNodeUtils.java @@ -0,0 +1,66 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.oauth2.server.authorization.jackson; + +import java.util.Map; +import java.util.Set; + +import tools.jackson.core.type.TypeReference; +import tools.jackson.databind.DeserializationContext; +import tools.jackson.databind.JsonNode; + +/** + * Utility class for {@code JsonNode}. + * + * @author Joe Grandja + * @since 7.0 + */ +abstract class JsonNodeUtils { + + static final TypeReference> STRING_SET = new TypeReference<>() { + }; + + static final TypeReference> STRING_OBJECT_MAP = new TypeReference<>() { + }; + + static String findStringValue(JsonNode jsonNode, String fieldName) { + if (jsonNode == null) { + return null; + } + JsonNode value = jsonNode.findValue(fieldName); + return (value != null && value.isString()) ? value.stringValue() : null; + } + + static T findValue(JsonNode jsonNode, String fieldName, TypeReference valueTypeReference, + DeserializationContext context) { + if (jsonNode == null) { + return null; + } + JsonNode value = jsonNode.findValue(fieldName); + return (value != null && value.isContainer()) + ? context.readTreeAsValue(value, context.getTypeFactory().constructType(valueTypeReference)) : null; + } + + static JsonNode findObjectNode(JsonNode jsonNode, String fieldName) { + if (jsonNode == null) { + return null; + } + JsonNode value = jsonNode.findValue(fieldName); + return (value != null && value.isObject()) ? value : null; + } + +} diff --git a/oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/jackson/JwsAlgorithmMixin.java b/oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/jackson/JwsAlgorithmMixin.java new file mode 100644 index 0000000000..610ff73a77 --- /dev/null +++ b/oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/jackson/JwsAlgorithmMixin.java @@ -0,0 +1,36 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.oauth2.server.authorization.jackson; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm; + +/** + * This mixin class is used to serialize/deserialize {@link SignatureAlgorithm}. + * + * @author Joe Grandja + * @since 7.0 + * @see SignatureAlgorithm + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) +@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE, + isGetterVisibility = JsonAutoDetect.Visibility.NONE) +abstract class JwsAlgorithmMixin { + +} diff --git a/oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/jackson/OAuth2AuthorizationRequestDeserializer.java b/oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/jackson/OAuth2AuthorizationRequestDeserializer.java new file mode 100644 index 0000000000..ce1e55df80 --- /dev/null +++ b/oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/jackson/OAuth2AuthorizationRequestDeserializer.java @@ -0,0 +1,77 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.oauth2.server.authorization.jackson; + +import tools.jackson.core.JsonParser; +import tools.jackson.databind.DeserializationContext; +import tools.jackson.databind.JsonNode; +import tools.jackson.databind.ValueDeserializer; +import tools.jackson.databind.exc.InvalidFormatException; + +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; +import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest.Builder; + +/** + * A {@code JsonDeserializer} for {@link OAuth2AuthorizationRequest}. + * + * @author Joe Grandja + * @since 7.0 + * @see OAuth2AuthorizationRequest + * @see OAuth2AuthorizationRequestMixin + */ +final class OAuth2AuthorizationRequestDeserializer extends ValueDeserializer { + + @Override + public OAuth2AuthorizationRequest deserialize(JsonParser parser, DeserializationContext context) { + JsonNode root = context.readTree(parser); + return deserialize(parser, context, root); + } + + private OAuth2AuthorizationRequest deserialize(JsonParser parser, DeserializationContext context, JsonNode root) { + AuthorizationGrantType authorizationGrantType = convertAuthorizationGrantType( + JsonNodeUtils.findObjectNode(root, "authorizationGrantType")); + Builder builder = getBuilder(parser, authorizationGrantType); + builder.authorizationUri(JsonNodeUtils.findStringValue(root, "authorizationUri")); + builder.clientId(JsonNodeUtils.findStringValue(root, "clientId")); + builder.redirectUri(JsonNodeUtils.findStringValue(root, "redirectUri")); + builder.scopes(JsonNodeUtils.findValue(root, "scopes", JsonNodeUtils.STRING_SET, context)); + builder.state(JsonNodeUtils.findStringValue(root, "state")); + builder.additionalParameters( + JsonNodeUtils.findValue(root, "additionalParameters", JsonNodeUtils.STRING_OBJECT_MAP, context)); + builder.authorizationRequestUri(JsonNodeUtils.findStringValue(root, "authorizationRequestUri")); + builder.attributes(JsonNodeUtils.findValue(root, "attributes", JsonNodeUtils.STRING_OBJECT_MAP, context)); + return builder.build(); + } + + private Builder getBuilder(JsonParser parser, AuthorizationGrantType authorizationGrantType) { + if (AuthorizationGrantType.AUTHORIZATION_CODE.equals(authorizationGrantType)) { + return OAuth2AuthorizationRequest.authorizationCode(); + } + throw new InvalidFormatException(parser, "Invalid authorizationGrantType", authorizationGrantType, + AuthorizationGrantType.class); + } + + private static AuthorizationGrantType convertAuthorizationGrantType(JsonNode jsonNode) { + String value = JsonNodeUtils.findStringValue(jsonNode, "value"); + if (AuthorizationGrantType.AUTHORIZATION_CODE.getValue().equalsIgnoreCase(value)) { + return AuthorizationGrantType.AUTHORIZATION_CODE; + } + return null; + } + +} diff --git a/oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/jackson/OAuth2AuthorizationRequestMixin.java b/oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/jackson/OAuth2AuthorizationRequestMixin.java new file mode 100644 index 0000000000..c451f8cf2d --- /dev/null +++ b/oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/jackson/OAuth2AuthorizationRequestMixin.java @@ -0,0 +1,40 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.oauth2.server.authorization.jackson; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import tools.jackson.databind.annotation.JsonDeserialize; + +import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; + +/** + * This mixin class is used to serialize/deserialize {@link OAuth2AuthorizationRequest}. + * It also registers a custom deserializer {@link OAuth2AuthorizationRequestDeserializer}. + * + * @author Joe Grandja + * @since 7.0 + * @see OAuth2AuthorizationRequest + * @see OAuth2AuthorizationRequestDeserializer + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) +@JsonDeserialize(using = OAuth2AuthorizationRequestDeserializer.class) +@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE, + isGetterVisibility = JsonAutoDetect.Visibility.NONE) +abstract class OAuth2AuthorizationRequestMixin { + +} diff --git a/oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/jackson/OAuth2AuthorizationServerJacksonModule.java b/oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/jackson/OAuth2AuthorizationServerJacksonModule.java new file mode 100644 index 0000000000..fd8b1b8d6a --- /dev/null +++ b/oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/jackson/OAuth2AuthorizationServerJacksonModule.java @@ -0,0 +1,95 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.oauth2.server.authorization.jackson; + +import java.net.URL; + +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import tools.jackson.core.Version; +import tools.jackson.databind.DefaultTyping; +import tools.jackson.databind.cfg.MapperBuilder; +import tools.jackson.databind.jsontype.BasicPolymorphicTypeValidator; + +import org.springframework.security.jackson.CoreJacksonModule; +import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; +import org.springframework.security.oauth2.jose.jws.MacAlgorithm; +import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm; +import org.springframework.security.oauth2.server.authorization.authentication.OAuth2TokenExchangeActor; +import org.springframework.security.oauth2.server.authorization.authentication.OAuth2TokenExchangeCompositeAuthenticationToken; +import org.springframework.security.oauth2.server.authorization.settings.OAuth2TokenFormat; + +/** + * Jackson {@code Module} for {@code spring-security-oauth2-authorization-server}, that + * registers the following mix-in annotations: + * + *
    + *
  • {@link OAuth2TokenExchangeActor}
  • + *
  • {@link OAuth2AuthorizationRequestMixin}
  • + *
  • {@link OAuth2TokenExchangeCompositeAuthenticationTokenMixin}
  • + *
  • {@link JwsAlgorithmMixin}
  • + *
  • {@link OAuth2TokenFormatMixin}
  • + *
+ * + * If not already enabled, default typing will be automatically enabled as type info is + * required to properly serialize/deserialize objects. In order to use this module just + * add it to your {@code JsonMapper.Builder} configuration. + * + *
+ *     JsonMapper mapper = JsonMapper.builder()
+ *             .addModules(new OAuth2AuthorizationServerJacksonModule()).build;
+ * 
+ * + * @author Sebastien Deleuze + * @author Steve Riesenberg + * @since 7.0 + */ +@SuppressWarnings("serial") +public class OAuth2AuthorizationServerJacksonModule extends CoreJacksonModule { + + public OAuth2AuthorizationServerJacksonModule() { + super(OAuth2AuthorizationServerJacksonModule.class.getName(), new Version(1, 0, 0, null, null, null)); + } + + @Override + public void configurePolymorphicTypeValidator(BasicPolymorphicTypeValidator.Builder builder) { + super.configurePolymorphicTypeValidator(builder); + builder.allowIfSubType(OAuth2TokenFormat.class) + .allowIfSubType(OAuth2TokenExchangeActor.class) + .allowIfSubType(OAuth2TokenExchangeCompositeAuthenticationToken.class) + .allowIfSubType(SignatureAlgorithm.class) + .allowIfSubType(MacAlgorithm.class) + .allowIfSubType(OAuth2AuthorizationRequest.class) + .allowIfSubType(URL.class); + } + + @Override + public void setupModule(SetupContext context) { + super.setupModule(context); + BasicPolymorphicTypeValidator.Builder builder = BasicPolymorphicTypeValidator.builder(); + this.configurePolymorphicTypeValidator(builder); + ((MapperBuilder) context.getOwner()).activateDefaultTyping(builder.build(), DefaultTyping.NON_FINAL, + JsonTypeInfo.As.PROPERTY); + context.setMixIn(OAuth2TokenExchangeActor.class, OAuth2TokenExchangeActorMixin.class); + context.setMixIn(OAuth2AuthorizationRequest.class, OAuth2AuthorizationRequestMixin.class); + context.setMixIn(OAuth2TokenExchangeCompositeAuthenticationToken.class, + OAuth2TokenExchangeCompositeAuthenticationTokenMixin.class); + context.setMixIn(SignatureAlgorithm.class, JwsAlgorithmMixin.class); + context.setMixIn(MacAlgorithm.class, JwsAlgorithmMixin.class); + context.setMixIn(OAuth2TokenFormat.class, OAuth2TokenFormatMixin.class); + } + +} diff --git a/oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/jackson/OAuth2TokenExchangeActorMixin.java b/oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/jackson/OAuth2TokenExchangeActorMixin.java new file mode 100644 index 0000000000..0e81cd1060 --- /dev/null +++ b/oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/jackson/OAuth2TokenExchangeActorMixin.java @@ -0,0 +1,44 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.oauth2.server.authorization.jackson; + +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +import org.springframework.security.oauth2.server.authorization.authentication.OAuth2TokenExchangeActor; + +/** + * This mixin class is used to serialize/deserialize {@link OAuth2TokenExchangeActor}. + * + * @author Steve Riesenberg + * @since 7.0 + * @see OAuth2TokenExchangeActor + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) +@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE, + isGetterVisibility = JsonAutoDetect.Visibility.NONE, creatorVisibility = JsonAutoDetect.Visibility.NONE) +abstract class OAuth2TokenExchangeActorMixin { + + @JsonCreator + OAuth2TokenExchangeActorMixin(@JsonProperty("claims") Map claims) { + } + +} diff --git a/oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/jackson/OAuth2TokenExchangeCompositeAuthenticationTokenMixin.java b/oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/jackson/OAuth2TokenExchangeCompositeAuthenticationTokenMixin.java new file mode 100644 index 0000000000..a9d802ce77 --- /dev/null +++ b/oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/jackson/OAuth2TokenExchangeCompositeAuthenticationTokenMixin.java @@ -0,0 +1,47 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.oauth2.server.authorization.jackson; + +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.server.authorization.authentication.OAuth2TokenExchangeCompositeAuthenticationToken; + +/** + * This mixin class is used to serialize/deserialize + * {@link OAuth2TokenExchangeCompositeAuthenticationToken}. + * + * @author Steve Riesenberg + * @since 7.0 + * @see OAuth2TokenExchangeCompositeAuthenticationToken + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) +@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE, + isGetterVisibility = JsonAutoDetect.Visibility.NONE, creatorVisibility = JsonAutoDetect.Visibility.NONE) +abstract class OAuth2TokenExchangeCompositeAuthenticationTokenMixin { + + @JsonCreator + OAuth2TokenExchangeCompositeAuthenticationTokenMixin(@JsonProperty("subject") Authentication subject, + @JsonProperty("actors") List actors) { + } + +} diff --git a/oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/jackson/OAuth2TokenFormatMixin.java b/oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/jackson/OAuth2TokenFormatMixin.java new file mode 100644 index 0000000000..6a21ce580d --- /dev/null +++ b/oauth2/oauth2-authorization-server/src/main/java/org/springframework/security/oauth2/server/authorization/jackson/OAuth2TokenFormatMixin.java @@ -0,0 +1,42 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.oauth2.server.authorization.jackson; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +import org.springframework.security.oauth2.server.authorization.settings.OAuth2TokenFormat; + +/** + * This mixin class is used to serialize/deserialize {@link OAuth2TokenFormat}. + * + * @author Joe Grandja + * @since 7.0 + * @see OAuth2TokenFormat + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) +@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE, + isGetterVisibility = JsonAutoDetect.Visibility.NONE) +abstract class OAuth2TokenFormatMixin { + + @JsonCreator + OAuth2TokenFormatMixin(@JsonProperty("value") String value) { + } + +} diff --git a/oauth2/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/JdbcOAuth2AuthorizationServiceTests.java b/oauth2/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/JdbcOAuth2AuthorizationServiceTests.java index d56518e0dd..1f83dfc0f0 100644 --- a/oauth2/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/JdbcOAuth2AuthorizationServiceTests.java +++ b/oauth2/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/JdbcOAuth2AuthorizationServiceTests.java @@ -29,11 +29,11 @@ import java.util.Map; import java.util.Set; import java.util.function.Function; -import com.fasterxml.jackson.core.type.TypeReference; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.springframework.core.ParameterizedTypeReference; import org.springframework.dao.DataRetrievalFailureException; import org.springframework.jdbc.core.ArgumentPreparedStatementSetter; import org.springframework.jdbc.core.JdbcOperations; @@ -747,7 +747,7 @@ public class JdbcOAuth2AuthorizationServiceTests { private Map parseMap(String data) { try { - return getObjectMapper().readValue(data, new TypeReference<>() { + return getMapper().readValue(data, new ParameterizedTypeReference<>() { }); } catch (Exception ex) { @@ -852,7 +852,7 @@ public class JdbcOAuth2AuthorizationServiceTests { private String writeMap(Map data) { try { - return getObjectMapper().writeValueAsString(data); + return getMapper().writeValueAsString(data); } catch (Exception ex) { throw new IllegalArgumentException(ex.getMessage(), ex); diff --git a/oauth2/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/client/JdbcRegisteredClientRepositoryTests.java b/oauth2/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/client/JdbcRegisteredClientRepositoryTests.java index cbd30b6079..922c0c4e0a 100644 --- a/oauth2/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/client/JdbcRegisteredClientRepositoryTests.java +++ b/oauth2/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/client/JdbcRegisteredClientRepositoryTests.java @@ -365,6 +365,7 @@ public class JdbcRegisteredClientRepositoryTests { return !result.isEmpty() ? result.get(0) : null; } + @SuppressWarnings("removal") private static final class CustomRegisteredClientRowMapper implements RowMapper { private final ObjectMapper objectMapper = new ObjectMapper(); diff --git a/oauth2/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/jackson/OAuth2AuthorizationServerJacksonModuleTests.java b/oauth2/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/jackson/OAuth2AuthorizationServerJacksonModuleTests.java new file mode 100644 index 0000000000..00b3cb8662 --- /dev/null +++ b/oauth2/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/jackson/OAuth2AuthorizationServerJacksonModuleTests.java @@ -0,0 +1,112 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.oauth2.server.authorization.jackson; + +import java.security.Principal; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import tools.jackson.core.type.TypeReference; +import tools.jackson.databind.json.JsonMapper; + +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.jose.jws.MacAlgorithm; +import org.springframework.security.oauth2.jwt.JwtClaimNames; +import org.springframework.security.oauth2.server.authorization.OAuth2Authorization; +import org.springframework.security.oauth2.server.authorization.TestOAuth2Authorizations; +import org.springframework.security.oauth2.server.authorization.authentication.OAuth2TokenExchangeActor; +import org.springframework.security.oauth2.server.authorization.authentication.OAuth2TokenExchangeCompositeAuthenticationToken; +import org.springframework.security.oauth2.server.authorization.jackson2.OAuth2AuthorizationServerJackson2Module; +import org.springframework.security.oauth2.server.authorization.settings.ClientSettings; +import org.springframework.security.oauth2.server.authorization.settings.TokenSettings; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link OAuth2AuthorizationServerJackson2Module}. + * + * @author Steve Riesenberg + * @author Joe Grandja + */ +@SuppressWarnings("removal") +public class OAuth2AuthorizationServerJacksonModuleTests { + + private static final TypeReference> STRING_OBJECT_MAP = new TypeReference<>() { + }; + + private JsonMapper mapper; + + @BeforeEach + public void setup() { + this.mapper = JsonMapper.builder().addModules(new OAuth2AuthorizationServerJacksonModule()).build(); + } + + @Test + public void readValueWhenOAuth2AuthorizationAttributesThenSuccess() { + Authentication principal = new UsernamePasswordAuthenticationToken("principal", "credentials"); + OAuth2Authorization authorization = TestOAuth2Authorizations.authorization() + .attributes((attrs) -> attrs.put(Principal.class.getName(), principal)) + .build(); + Map attributes = authorization.getAttributes(); + String json = this.mapper.writeValueAsString(attributes); + assertThat(this.mapper.readValue(json, STRING_OBJECT_MAP)).isEqualTo(attributes); + } + + @Test + public void readValueWhenOAuth2AccessTokenMetadataThenSuccess() { + OAuth2Authorization authorization = TestOAuth2Authorizations.authorization().build(); + Map metadata = authorization.getAccessToken().getMetadata(); + String json = this.mapper.writeValueAsString(metadata); + assertThat(this.mapper.readValue(json, STRING_OBJECT_MAP)).isEqualTo(metadata); + } + + @Test + public void readValueWhenClientSettingsThenSuccess() { + ClientSettings clientSettings = ClientSettings.builder() + .tokenEndpointAuthenticationSigningAlgorithm(MacAlgorithm.HS256) + .build(); + Map clientSettingsMap = clientSettings.getSettings(); + String json = this.mapper.writeValueAsString(clientSettingsMap); + assertThat(this.mapper.readValue(json, STRING_OBJECT_MAP)).isEqualTo(clientSettingsMap); + } + + @Test + public void readValueWhenTokenSettingsThenSuccess() { + TokenSettings tokenSettings = TokenSettings.builder().build(); + Map tokenSettingsMap = tokenSettings.getSettings(); + String json = this.mapper.writeValueAsString(tokenSettingsMap); + assertThat(this.mapper.readValue(json, STRING_OBJECT_MAP)).isEqualTo(tokenSettingsMap); + } + + @Test + public void readValueWhenOAuth2TokenExchangeCompositeAuthenticationTokenThenSuccess() { + Authentication subject = new UsernamePasswordAuthenticationToken("principal", "credentials"); + OAuth2TokenExchangeActor actor1 = new OAuth2TokenExchangeActor( + Map.of(JwtClaimNames.ISS, "issuer-1", JwtClaimNames.SUB, "actor1")); + OAuth2TokenExchangeActor actor2 = new OAuth2TokenExchangeActor( + Map.of(JwtClaimNames.ISS, "issuer-2", JwtClaimNames.SUB, "actor2")); + OAuth2TokenExchangeCompositeAuthenticationToken authentication = new OAuth2TokenExchangeCompositeAuthenticationToken( + subject, List.of(actor1, actor2)); + String json = this.mapper.writeValueAsString(authentication); + assertThat(this.mapper.readValue(json, OAuth2TokenExchangeCompositeAuthenticationToken.class)) + .isEqualTo(authentication); + } + +} diff --git a/oauth2/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/jackson/TestingAuthenticationTokenMixin.java b/oauth2/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/jackson/TestingAuthenticationTokenMixin.java new file mode 100644 index 0000000000..bc0a70212a --- /dev/null +++ b/oauth2/oauth2-authorization-server/src/test/java/org/springframework/security/oauth2/server/authorization/jackson/TestingAuthenticationTokenMixin.java @@ -0,0 +1,49 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.oauth2.server.authorization.jackson; + +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +import org.springframework.security.authentication.TestingAuthenticationToken; +import org.springframework.security.core.GrantedAuthority; + +/** + * This mixin class is used to serialize/deserialize {@link TestingAuthenticationToken}. + * + * @author Steve Riesenberg + * @since 7.0 + * @see TestingAuthenticationToken + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) +@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE, + isGetterVisibility = JsonAutoDetect.Visibility.NONE) +@JsonIgnoreProperties(value = { "authenticated" }, ignoreUnknown = true) +public class TestingAuthenticationTokenMixin { + + @JsonCreator + TestingAuthenticationTokenMixin(@JsonProperty("principal") Object principal, + @JsonProperty("credentials") Object credentials, + @JsonProperty("authorities") List authorities) { + } + +} diff --git a/oauth2/oauth2-client/spring-security-oauth2-client.gradle b/oauth2/oauth2-client/spring-security-oauth2-client.gradle index 11b6c91f0a..1d0dcc6f79 100644 --- a/oauth2/oauth2-client/spring-security-oauth2-client.gradle +++ b/oauth2/oauth2-client/spring-security-oauth2-client.gradle @@ -15,6 +15,7 @@ dependencies { optional 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310' optional 'org.springframework:spring-jdbc' optional 'org.springframework:spring-r2dbc' + optional 'tools.jackson.core:jackson-databind' testImplementation project(path: ':spring-security-oauth2-core', configuration: 'tests') testImplementation project(path: ':spring-security-oauth2-jose', configuration: 'tests') diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson/ClientRegistrationDeserializer.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson/ClientRegistrationDeserializer.java new file mode 100644 index 0000000000..cabe8ac870 --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson/ClientRegistrationDeserializer.java @@ -0,0 +1,76 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.oauth2.client.jackson; + +import tools.jackson.core.JsonParser; +import tools.jackson.databind.DeserializationContext; +import tools.jackson.databind.JsonNode; +import tools.jackson.databind.ValueDeserializer; +import tools.jackson.databind.util.StdConverter; + +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.core.AuthenticationMethod; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.ClientAuthenticationMethod; + +/** + * A {@code JsonDeserializer} for {@link ClientRegistration}. + * + * @author Sebastien Deleuze + * @author Joe Grandja + * @since 7.0 + * @see ClientRegistration + * @see ClientRegistrationMixin + */ +final class ClientRegistrationDeserializer extends ValueDeserializer { + + private static final StdConverter CLIENT_AUTHENTICATION_METHOD_CONVERTER = new StdConverters.ClientAuthenticationMethodConverter(); + + private static final StdConverter AUTHORIZATION_GRANT_TYPE_CONVERTER = new StdConverters.AuthorizationGrantTypeConverter(); + + private static final StdConverter AUTHENTICATION_METHOD_CONVERTER = new StdConverters.AuthenticationMethodConverter(); + + @Override + public ClientRegistration deserialize(JsonParser parser, DeserializationContext context) { + JsonNode clientRegistrationNode = context.readTree(parser); + JsonNode providerDetailsNode = JsonNodeUtils.findObjectNode(clientRegistrationNode, "providerDetails"); + JsonNode userInfoEndpointNode = JsonNodeUtils.findObjectNode(providerDetailsNode, "userInfoEndpoint"); + return ClientRegistration + .withRegistrationId(JsonNodeUtils.findStringValue(clientRegistrationNode, "registrationId")) + .clientId(JsonNodeUtils.findStringValue(clientRegistrationNode, "clientId")) + .clientSecret(JsonNodeUtils.findStringValue(clientRegistrationNode, "clientSecret")) + .clientAuthenticationMethod(CLIENT_AUTHENTICATION_METHOD_CONVERTER + .convert(JsonNodeUtils.findObjectNode(clientRegistrationNode, "clientAuthenticationMethod"))) + .authorizationGrantType(AUTHORIZATION_GRANT_TYPE_CONVERTER + .convert(JsonNodeUtils.findObjectNode(clientRegistrationNode, "authorizationGrantType"))) + .redirectUri(JsonNodeUtils.findStringValue(clientRegistrationNode, "redirectUri")) + .scope(JsonNodeUtils.findValue(clientRegistrationNode, "scopes", JsonNodeUtils.STRING_SET, context)) + .clientName(JsonNodeUtils.findStringValue(clientRegistrationNode, "clientName")) + .authorizationUri(JsonNodeUtils.findStringValue(providerDetailsNode, "authorizationUri")) + .tokenUri(JsonNodeUtils.findStringValue(providerDetailsNode, "tokenUri")) + .userInfoUri(JsonNodeUtils.findStringValue(userInfoEndpointNode, "uri")) + .userInfoAuthenticationMethod(AUTHENTICATION_METHOD_CONVERTER + .convert(JsonNodeUtils.findObjectNode(userInfoEndpointNode, "authenticationMethod"))) + .userNameAttributeName(JsonNodeUtils.findStringValue(userInfoEndpointNode, "userNameAttributeName")) + .jwkSetUri(JsonNodeUtils.findStringValue(providerDetailsNode, "jwkSetUri")) + .issuerUri(JsonNodeUtils.findStringValue(providerDetailsNode, "issuerUri")) + .providerConfigurationMetadata(JsonNodeUtils.findValue(providerDetailsNode, "configurationMetadata", + JsonNodeUtils.STRING_OBJECT_MAP, context)) + .build(); + } + +} diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson/ClientRegistrationMixin.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson/ClientRegistrationMixin.java new file mode 100644 index 0000000000..01a92682d2 --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson/ClientRegistrationMixin.java @@ -0,0 +1,42 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.oauth2.client.jackson; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import tools.jackson.databind.annotation.JsonDeserialize; + +import org.springframework.security.oauth2.client.registration.ClientRegistration; + +/** + * This mixin class is used to serialize/deserialize {@link ClientRegistration}. It also + * registers a custom deserializer {@link ClientRegistrationDeserializer}. + * + * @author Sebastien Deleuze + * @author Joe Grandja + * @since 7.0 + * @see ClientRegistration + * @see ClientRegistrationDeserializer + * @see OAuth2ClientJacksonModule + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) +@JsonDeserialize(using = ClientRegistrationDeserializer.class) +@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE, + isGetterVisibility = JsonAutoDetect.Visibility.NONE) +abstract class ClientRegistrationMixin { + +} diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson/DefaultOAuth2UserMixin.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson/DefaultOAuth2UserMixin.java new file mode 100644 index 0000000000..e02a6514d7 --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson/DefaultOAuth2UserMixin.java @@ -0,0 +1,50 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.oauth2.client.jackson; + +import java.util.Collection; +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.oauth2.core.user.DefaultOAuth2User; + +/** + * This mixin class is used to serialize/deserialize {@link DefaultOAuth2User}. + * + * @author Sebastien Deleuze + * @author Joe Grandja + * @since 7.0 + * @see DefaultOAuth2User + * @see OAuth2ClientJacksonModule + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) +@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE, + isGetterVisibility = JsonAutoDetect.Visibility.NONE) +abstract class DefaultOAuth2UserMixin { + + @JsonCreator + DefaultOAuth2UserMixin(@JsonProperty("authorities") Collection authorities, + @JsonProperty("attributes") Map attributes, + @JsonProperty("nameAttributeKey") String nameAttributeKey) { + } + +} diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson/DefaultOidcUserMixin.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson/DefaultOidcUserMixin.java new file mode 100644 index 0000000000..173f3d20d8 --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson/DefaultOidcUserMixin.java @@ -0,0 +1,53 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.oauth2.client.jackson; + +import java.util.Collection; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.oauth2.core.oidc.OidcIdToken; +import org.springframework.security.oauth2.core.oidc.OidcUserInfo; +import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser; + +/** + * This mixin class is used to serialize/deserialize {@link DefaultOidcUser}. + * + * @author Sebastien Deleuze + * @author Joe Grandja + * @since 7.0 + * @see DefaultOidcUser + * @see OAuth2ClientJacksonModule + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) +@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE, + isGetterVisibility = JsonAutoDetect.Visibility.NONE) +@JsonIgnoreProperties({ "attributes" }) +abstract class DefaultOidcUserMixin { + + @JsonCreator + DefaultOidcUserMixin(@JsonProperty("authorities") Collection authorities, + @JsonProperty("idToken") OidcIdToken idToken, @JsonProperty("userInfo") OidcUserInfo userInfo, + @JsonProperty("nameAttributeKey") String nameAttributeKey) { + } + +} diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson/JsonNodeUtils.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson/JsonNodeUtils.java new file mode 100644 index 0000000000..4f181aabb1 --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson/JsonNodeUtils.java @@ -0,0 +1,67 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.oauth2.client.jackson; + +import java.util.Map; +import java.util.Set; + +import tools.jackson.core.type.TypeReference; +import tools.jackson.databind.DeserializationContext; +import tools.jackson.databind.JsonNode; + +/** + * Utility class for {@code JsonNode}. + * + * @author Sebastien Deleuze + * @author Joe Grandja + * @since 7.0 + */ +abstract class JsonNodeUtils { + + static final TypeReference> STRING_SET = new TypeReference<>() { + }; + + static final TypeReference> STRING_OBJECT_MAP = new TypeReference<>() { + }; + + static String findStringValue(JsonNode jsonNode, String fieldName) { + if (jsonNode == null) { + return null; + } + JsonNode value = jsonNode.findValue(fieldName); + return (value != null && value.isString()) ? value.stringValue() : null; + } + + static T findValue(JsonNode jsonNode, String fieldName, TypeReference valueTypeReference, + DeserializationContext context) { + if (jsonNode == null) { + return null; + } + JsonNode value = jsonNode.findValue(fieldName); + return (value != null && value.isContainer()) + ? context.readTreeAsValue(value, context.getTypeFactory().constructType(valueTypeReference)) : null; + } + + static JsonNode findObjectNode(JsonNode jsonNode, String fieldName) { + if (jsonNode == null) { + return null; + } + JsonNode value = jsonNode.findValue(fieldName); + return (value != null && value.isObject()) ? value : null; + } + +} diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson/OAuth2AccessTokenMixin.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson/OAuth2AccessTokenMixin.java new file mode 100644 index 0000000000..1f91943f5b --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson/OAuth2AccessTokenMixin.java @@ -0,0 +1,52 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.oauth2.client.jackson; + +import java.time.Instant; +import java.util.Set; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import tools.jackson.databind.annotation.JsonDeserialize; + +import org.springframework.security.oauth2.core.OAuth2AccessToken; + +/** + * This mixin class is used to serialize/deserialize {@link OAuth2AccessToken}. + * + * @author Sebastien Deleuze + * @author Joe Grandja + * @since 7.0 + * @see OAuth2AccessToken + * @see OAuth2ClientJacksonModule + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) +@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE, + isGetterVisibility = JsonAutoDetect.Visibility.NONE) +abstract class OAuth2AccessTokenMixin { + + @JsonCreator + OAuth2AccessTokenMixin( + @JsonProperty("tokenType") @JsonDeserialize( + converter = StdConverters.AccessTokenTypeConverter.class) OAuth2AccessToken.TokenType tokenType, + @JsonProperty("tokenValue") String tokenValue, @JsonProperty("issuedAt") Instant issuedAt, + @JsonProperty("expiresAt") Instant expiresAt, @JsonProperty("scopes") Set scopes) { + } + +} diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson/OAuth2AuthenticationExceptionMixin.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson/OAuth2AuthenticationExceptionMixin.java new file mode 100644 index 0000000000..1da1259f62 --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson/OAuth2AuthenticationExceptionMixin.java @@ -0,0 +1,56 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.oauth2.client.jackson; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.OAuth2Error; + +/** + * This mixin class is used to serialize/deserialize + * {@link OAuth2AuthenticationException}. + * + * @author Sebastien Deleuze + * @author Dennis Neufeld + * @author Steve Riesenberg + * @since 7.0 + * @see OAuth2AuthenticationException + * @see OAuth2ClientJacksonModule + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) +@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.NONE, getterVisibility = JsonAutoDetect.Visibility.NONE, + isGetterVisibility = JsonAutoDetect.Visibility.NONE) +@JsonIgnoreProperties({ "cause", "stackTrace", "suppressedExceptions" }) +abstract class OAuth2AuthenticationExceptionMixin { + + @JsonProperty("error") + abstract OAuth2Error getError(); + + @JsonProperty("detailMessage") + abstract String getMessage(); + + @JsonCreator + OAuth2AuthenticationExceptionMixin(@JsonProperty("error") OAuth2Error error, + @JsonProperty("detailMessage") String message) { + } + +} diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson/OAuth2AuthenticationTokenMixin.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson/OAuth2AuthenticationTokenMixin.java new file mode 100644 index 0000000000..51f3333263 --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson/OAuth2AuthenticationTokenMixin.java @@ -0,0 +1,52 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.oauth2.client.jackson; + +import java.util.Collection; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; +import org.springframework.security.oauth2.core.user.OAuth2User; + +/** + * This mixin class is used to serialize/deserialize {@link OAuth2AuthenticationToken}. + * + * @author Sebastien Deleuze + * @author Joe Grandja + * @since 7.0O + * @see OAuth2AuthenticationToken + * @see OAuth2ClientJacksonModule + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) +@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE, + isGetterVisibility = JsonAutoDetect.Visibility.NONE) +@JsonIgnoreProperties({ "authenticated" }) +abstract class OAuth2AuthenticationTokenMixin { + + @JsonCreator + OAuth2AuthenticationTokenMixin(@JsonProperty("principal") OAuth2User principal, + @JsonProperty("authorities") Collection authorities, + @JsonProperty("authorizedClientRegistrationId") String authorizedClientRegistrationId) { + } + +} diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson/OAuth2AuthorizationRequestDeserializer.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson/OAuth2AuthorizationRequestDeserializer.java new file mode 100644 index 0000000000..786207e68e --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson/OAuth2AuthorizationRequestDeserializer.java @@ -0,0 +1,72 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.oauth2.client.jackson; + +import tools.jackson.core.JsonParser; +import tools.jackson.core.exc.StreamReadException; +import tools.jackson.databind.DeserializationContext; +import tools.jackson.databind.JsonNode; +import tools.jackson.databind.ValueDeserializer; +import tools.jackson.databind.util.StdConverter; + +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; +import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest.Builder; + +/** + * A {@code JsonDeserializer} for {@link OAuth2AuthorizationRequest}. + * + * @author Sebastien Deleuze + * @author Joe Grandja + * @since 7.0 + * @see OAuth2AuthorizationRequest + * @see OAuth2AuthorizationRequestMixin + */ +final class OAuth2AuthorizationRequestDeserializer extends ValueDeserializer { + + private static final StdConverter AUTHORIZATION_GRANT_TYPE_CONVERTER = new StdConverters.AuthorizationGrantTypeConverter(); + + @Override + public OAuth2AuthorizationRequest deserialize(JsonParser parser, DeserializationContext context) { + JsonNode root = context.readTree(parser); + return deserialize(parser, context, root); + } + + private OAuth2AuthorizationRequest deserialize(JsonParser parser, DeserializationContext context, JsonNode root) { + AuthorizationGrantType authorizationGrantType = AUTHORIZATION_GRANT_TYPE_CONVERTER + .convert(JsonNodeUtils.findObjectNode(root, "authorizationGrantType")); + Builder builder = getBuilder(parser, authorizationGrantType); + builder.authorizationUri(JsonNodeUtils.findStringValue(root, "authorizationUri")); + builder.clientId(JsonNodeUtils.findStringValue(root, "clientId")); + builder.redirectUri(JsonNodeUtils.findStringValue(root, "redirectUri")); + builder.scopes(JsonNodeUtils.findValue(root, "scopes", JsonNodeUtils.STRING_SET, context)); + builder.state(JsonNodeUtils.findStringValue(root, "state")); + builder.additionalParameters( + JsonNodeUtils.findValue(root, "additionalParameters", JsonNodeUtils.STRING_OBJECT_MAP, context)); + builder.authorizationRequestUri(JsonNodeUtils.findStringValue(root, "authorizationRequestUri")); + builder.attributes(JsonNodeUtils.findValue(root, "attributes", JsonNodeUtils.STRING_OBJECT_MAP, context)); + return builder.build(); + } + + private Builder getBuilder(JsonParser parser, AuthorizationGrantType authorizationGrantType) { + if (AuthorizationGrantType.AUTHORIZATION_CODE.equals(authorizationGrantType)) { + return OAuth2AuthorizationRequest.authorizationCode(); + } + throw new StreamReadException(parser, "Invalid authorizationGrantType"); + } + +} diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson/OAuth2AuthorizationRequestMixin.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson/OAuth2AuthorizationRequestMixin.java new file mode 100644 index 0000000000..cfb52f3fdc --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson/OAuth2AuthorizationRequestMixin.java @@ -0,0 +1,42 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.oauth2.client.jackson; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import tools.jackson.databind.annotation.JsonDeserialize; + +import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; + +/** + * This mixin class is used to serialize/deserialize {@link OAuth2AuthorizationRequest}. + * It also registers a custom deserializer {@link OAuth2AuthorizationRequestDeserializer}. + * + * @author Sebastien Deleuze + * @author Joe Grandja + * @since 7.0 + * @see OAuth2AuthorizationRequest + * @see OAuth2AuthorizationRequestDeserializer + * @see OAuth2ClientJacksonModule + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) +@JsonDeserialize(using = OAuth2AuthorizationRequestDeserializer.class) +@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE, + isGetterVisibility = JsonAutoDetect.Visibility.NONE) +abstract class OAuth2AuthorizationRequestMixin { + +} diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson/OAuth2AuthorizedClientMixin.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson/OAuth2AuthorizedClientMixin.java new file mode 100644 index 0000000000..d1dba7a6d7 --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson/OAuth2AuthorizedClientMixin.java @@ -0,0 +1,50 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.oauth2.client.jackson; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.core.OAuth2AccessToken; +import org.springframework.security.oauth2.core.OAuth2RefreshToken; + +/** + * This mixin class is used to serialize/deserialize {@link OAuth2AuthorizedClient}. + * + * @author Sebastien Deleuze + * @author Joe Grandja + * @since 7.0 + * @see OAuth2AuthorizedClient + * @see OAuth2ClientJacksonModule + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) +@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE, + isGetterVisibility = JsonAutoDetect.Visibility.NONE) +abstract class OAuth2AuthorizedClientMixin { + + @JsonCreator + OAuth2AuthorizedClientMixin(@JsonProperty("clientRegistration") ClientRegistration clientRegistration, + @JsonProperty("principalName") String principalName, + @JsonProperty("accessToken") OAuth2AccessToken accessToken, + @JsonProperty("refreshToken") OAuth2RefreshToken refreshToken) { + } + +} diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson/OAuth2ClientJacksonModule.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson/OAuth2ClientJacksonModule.java new file mode 100644 index 0000000000..0d27b4eec1 --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson/OAuth2ClientJacksonModule.java @@ -0,0 +1,118 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.oauth2.client.jackson; + +import tools.jackson.core.Version; +import tools.jackson.databind.jsontype.BasicPolymorphicTypeValidator; + +import org.springframework.security.jackson.SecurityJacksonModule; +import org.springframework.security.jackson.SecurityJacksonModules; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; +import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.core.OAuth2AccessToken; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.OAuth2Error; +import org.springframework.security.oauth2.core.OAuth2RefreshToken; +import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; +import org.springframework.security.oauth2.core.oidc.OidcIdToken; +import org.springframework.security.oauth2.core.oidc.OidcUserInfo; +import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser; +import org.springframework.security.oauth2.core.oidc.user.OidcUserAuthority; +import org.springframework.security.oauth2.core.user.DefaultOAuth2User; +import org.springframework.security.oauth2.core.user.OAuth2UserAuthority; + +/** + * Jackson {@code Module} for {@code spring-security-oauth2-client}, that registers the + * following mix-in annotations: + * + *
    + *
  • {@link OAuth2AuthorizationRequestMixin}
  • + *
  • {@link ClientRegistrationMixin}
  • + *
  • {@link OAuth2AccessTokenMixin}
  • + *
  • {@link OAuth2RefreshTokenMixin}
  • + *
  • {@link OAuth2AuthorizedClientMixin}
  • + *
  • {@link OAuth2UserAuthorityMixin}
  • + *
  • {@link DefaultOAuth2UserMixin}
  • + *
  • {@link OidcIdTokenMixin}
  • + *
  • {@link OidcUserInfoMixin}
  • + *
  • {@link OidcUserAuthorityMixin}
  • + *
  • {@link DefaultOidcUserMixin}
  • + *
  • {@link OAuth2AuthenticationTokenMixin}
  • + *
  • {@link OAuth2AuthenticationExceptionMixin}
  • + *
  • {@link OAuth2ErrorMixin}
  • + *
+ * + *

+ * The recommended way to configure it is to use {@link SecurityJacksonModules} in order + * to enable properly automatic inclusion of type information with related validation. + * + *

+ *     ClassLoader loader = getClass().getClassLoader();
+ *     JsonMapper mapper = JsonMapper.builder()
+ * 				.addModules(SecurityJacksonModules.getModules(loader))
+ * 				.build();
+ * 
+ * + * @author Sebastien Deleuze + * @author Joe Grandja + * @since 7.0 + */ +@SuppressWarnings("serial") +public class OAuth2ClientJacksonModule extends SecurityJacksonModule { + + public OAuth2ClientJacksonModule() { + super(OAuth2ClientJacksonModule.class.getName(), new Version(1, 0, 0, null, null, null)); + } + + @Override + public void configurePolymorphicTypeValidator(BasicPolymorphicTypeValidator.Builder builder) { + builder.allowIfSubType(OAuth2AuthenticationException.class) + .allowIfSubType(DefaultOidcUser.class) + .allowIfSubType(OAuth2AuthorizationRequest.class) + .allowIfSubType(OAuth2Error.class) + .allowIfSubType(OAuth2AuthorizedClient.class) + .allowIfSubType(OidcIdToken.class) + .allowIfSubType(OidcUserInfo.class) + .allowIfSubType(DefaultOAuth2User.class) + .allowIfSubType(ClientRegistration.class) + .allowIfSubType(OAuth2AccessToken.class) + .allowIfSubType(OAuth2RefreshToken.class) + .allowIfSubType(OAuth2AuthenticationToken.class) + .allowIfSubType(OidcUserAuthority.class) + .allowIfSubType(OAuth2UserAuthority.class); + } + + @Override + public void setupModule(SetupContext context) { + context.setMixIn(OAuth2AuthorizationRequest.class, OAuth2AuthorizationRequestMixin.class); + context.setMixIn(ClientRegistration.class, ClientRegistrationMixin.class); + context.setMixIn(OAuth2AccessToken.class, OAuth2AccessTokenMixin.class); + context.setMixIn(OAuth2RefreshToken.class, OAuth2RefreshTokenMixin.class); + context.setMixIn(OAuth2AuthorizedClient.class, OAuth2AuthorizedClientMixin.class); + context.setMixIn(OAuth2UserAuthority.class, OAuth2UserAuthorityMixin.class); + context.setMixIn(DefaultOAuth2User.class, DefaultOAuth2UserMixin.class); + context.setMixIn(OidcIdToken.class, OidcIdTokenMixin.class); + context.setMixIn(OidcUserInfo.class, OidcUserInfoMixin.class); + context.setMixIn(OidcUserAuthority.class, OidcUserAuthorityMixin.class); + context.setMixIn(DefaultOidcUser.class, DefaultOidcUserMixin.class); + context.setMixIn(OAuth2AuthenticationToken.class, OAuth2AuthenticationTokenMixin.class); + context.setMixIn(OAuth2AuthenticationException.class, OAuth2AuthenticationExceptionMixin.class); + context.setMixIn(OAuth2Error.class, OAuth2ErrorMixin.class); + } + +} diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson/OAuth2ErrorMixin.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson/OAuth2ErrorMixin.java new file mode 100644 index 0000000000..53c07c7f97 --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson/OAuth2ErrorMixin.java @@ -0,0 +1,47 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.oauth2.client.jackson; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +import org.springframework.security.oauth2.core.OAuth2Error; + +/** + * This mixin class is used to serialize/deserialize {@link OAuth2Error} as part of + * {@link org.springframework.security.oauth2.core.OAuth2AuthenticationException}. + * + * @author Sebastien Deleuze + * @author Dennis Neufeld + * @since 7.0 + * @see OAuth2Error + * @see OAuth2AuthenticationExceptionMixin + * @see OAuth2ClientJacksonModule + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) +@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE, + isGetterVisibility = JsonAutoDetect.Visibility.NONE) +abstract class OAuth2ErrorMixin { + + @JsonCreator + OAuth2ErrorMixin(@JsonProperty("errorCode") String errorCode, @JsonProperty("description") String description, + @JsonProperty("uri") String uri) { + } + +} diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson/OAuth2RefreshTokenMixin.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson/OAuth2RefreshTokenMixin.java new file mode 100644 index 0000000000..a4a9085ee4 --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson/OAuth2RefreshTokenMixin.java @@ -0,0 +1,46 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.oauth2.client.jackson; + +import java.time.Instant; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +import org.springframework.security.oauth2.core.OAuth2RefreshToken; + +/** + * This mixin class is used to serialize/deserialize {@link OAuth2RefreshToken}. + * + * @author Sebastien Deleuze + * @author Joe Grandja + * @since 7.0 + * @see OAuth2RefreshToken + * @see OAuth2ClientJacksonModule + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) +@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE, + isGetterVisibility = JsonAutoDetect.Visibility.NONE) +abstract class OAuth2RefreshTokenMixin { + + @JsonCreator + OAuth2RefreshTokenMixin(@JsonProperty("tokenValue") String tokenValue, @JsonProperty("issuedAt") Instant issuedAt) { + } + +} diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson/OAuth2UserAuthorityMixin.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson/OAuth2UserAuthorityMixin.java new file mode 100644 index 0000000000..3a4095b50d --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson/OAuth2UserAuthorityMixin.java @@ -0,0 +1,47 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.oauth2.client.jackson; + +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +import org.springframework.security.oauth2.core.user.OAuth2UserAuthority; + +/** + * This mixin class is used to serialize/deserialize {@link OAuth2UserAuthority}. + * + * @author Sebastien Deleuze + * @author Joe Grandja + * @since 7.0 + * @see OAuth2UserAuthority + * @see OAuth2ClientJacksonModule + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) +@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE, + isGetterVisibility = JsonAutoDetect.Visibility.NONE) +abstract class OAuth2UserAuthorityMixin { + + @JsonCreator + OAuth2UserAuthorityMixin(@JsonProperty("authority") String authority, + @JsonProperty("attributes") Map attributes) { + } + +} diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson/OidcIdTokenMixin.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson/OidcIdTokenMixin.java new file mode 100644 index 0000000000..14ed972340 --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson/OidcIdTokenMixin.java @@ -0,0 +1,48 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.oauth2.client.jackson; + +import java.time.Instant; +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +import org.springframework.security.oauth2.core.oidc.OidcIdToken; + +/** + * This mixin class is used to serialize/deserialize {@link OidcIdToken}. + * + * @author Sebastien Deleuze + * @author Joe Grandja + * @since 7.0 + * @see OidcIdToken + * @see OAuth2ClientJacksonModule + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) +@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE, + isGetterVisibility = JsonAutoDetect.Visibility.NONE) +abstract class OidcIdTokenMixin { + + @JsonCreator + OidcIdTokenMixin(@JsonProperty("tokenValue") String tokenValue, @JsonProperty("issuedAt") Instant issuedAt, + @JsonProperty("expiresAt") Instant expiresAt, @JsonProperty("claims") Map claims) { + } + +} diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson/OidcUserAuthorityMixin.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson/OidcUserAuthorityMixin.java new file mode 100644 index 0000000000..fa31b76ae3 --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson/OidcUserAuthorityMixin.java @@ -0,0 +1,49 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.oauth2.client.jackson; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +import org.springframework.security.oauth2.core.oidc.OidcIdToken; +import org.springframework.security.oauth2.core.oidc.OidcUserInfo; +import org.springframework.security.oauth2.core.oidc.user.OidcUserAuthority; + +/** + * This mixin class is used to serialize/deserialize {@link OidcUserAuthority}. + * + * @author Sebastien Deleuze + * @author Joe Grandja + * @since 7.0 + * @see OidcUserAuthority + * @see OAuth2ClientJacksonModule + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) +@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE, + isGetterVisibility = JsonAutoDetect.Visibility.NONE) +@JsonIgnoreProperties({ "attributes" }) +abstract class OidcUserAuthorityMixin { + + @JsonCreator + OidcUserAuthorityMixin(@JsonProperty("authority") String authority, @JsonProperty("idToken") OidcIdToken idToken, + @JsonProperty("userInfo") OidcUserInfo userInfo) { + } + +} diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson/OidcUserInfoMixin.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson/OidcUserInfoMixin.java new file mode 100644 index 0000000000..96dc41ca1c --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson/OidcUserInfoMixin.java @@ -0,0 +1,46 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.oauth2.client.jackson; + +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +import org.springframework.security.oauth2.core.oidc.OidcUserInfo; + +/** + * This mixin class is used to serialize/deserialize {@link OidcUserInfo}. + * + * @author Sebastien Deleuze + * @author Joe Grandja + * @since 7.0 + * @see OidcUserInfo + * @see OAuth2ClientJacksonModule + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) +@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE, + isGetterVisibility = JsonAutoDetect.Visibility.NONE) +abstract class OidcUserInfoMixin { + + @JsonCreator + OidcUserInfoMixin(@JsonProperty("claims") Map claims) { + } + +} diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson/StdConverters.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson/StdConverters.java new file mode 100644 index 0000000000..c9fafcdd6e --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson/StdConverters.java @@ -0,0 +1,94 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.oauth2.client.jackson; + +import tools.jackson.databind.JsonNode; +import tools.jackson.databind.util.StdConverter; + +import org.springframework.security.oauth2.core.AuthenticationMethod; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.ClientAuthenticationMethod; +import org.springframework.security.oauth2.core.OAuth2AccessToken; + +/** + * {@code StdConverter} implementations. + * + * @author Sebastien Deleuze + * @author Joe Grandja + * @since 7.0 + */ +abstract class StdConverters { + + static final class AccessTokenTypeConverter extends StdConverter { + + @Override + public OAuth2AccessToken.TokenType convert(JsonNode jsonNode) { + String value = JsonNodeUtils.findStringValue(jsonNode, "value"); + if (OAuth2AccessToken.TokenType.BEARER.getValue().equalsIgnoreCase(value)) { + return OAuth2AccessToken.TokenType.BEARER; + } + return null; + } + + } + + static final class ClientAuthenticationMethodConverter extends StdConverter { + + @Override + public ClientAuthenticationMethod convert(JsonNode jsonNode) { + String value = JsonNodeUtils.findStringValue(jsonNode, "value"); + return ClientAuthenticationMethod.valueOf(value); + } + + } + + static final class AuthorizationGrantTypeConverter extends StdConverter { + + @Override + public AuthorizationGrantType convert(JsonNode jsonNode) { + String value = JsonNodeUtils.findStringValue(jsonNode, "value"); + if (AuthorizationGrantType.AUTHORIZATION_CODE.getValue().equalsIgnoreCase(value)) { + return AuthorizationGrantType.AUTHORIZATION_CODE; + } + if (AuthorizationGrantType.CLIENT_CREDENTIALS.getValue().equalsIgnoreCase(value)) { + return AuthorizationGrantType.CLIENT_CREDENTIALS; + } + return new AuthorizationGrantType(value); + } + + } + + static final class AuthenticationMethodConverter extends StdConverter { + + @Override + public AuthenticationMethod convert(JsonNode jsonNode) { + String value = JsonNodeUtils.findStringValue(jsonNode, "value"); + if (AuthenticationMethod.HEADER.getValue().equalsIgnoreCase(value)) { + return AuthenticationMethod.HEADER; + } + if (AuthenticationMethod.FORM.getValue().equalsIgnoreCase(value)) { + return AuthenticationMethod.FORM; + } + if (AuthenticationMethod.QUERY.getValue().equalsIgnoreCase(value)) { + return AuthenticationMethod.QUERY; + } + return null; + } + + } + +} diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson/package-info.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson/package-info.java new file mode 100644 index 0000000000..3ac4da65bb --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Jackson 3+ serialization support for OAuth2 client. + */ +package org.springframework.security.oauth2.client.jackson; diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/package-info.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/package-info.java new file mode 100644 index 0000000000..5477db1b36 --- /dev/null +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/jackson2/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Jackson 2 serialization support for OAuth2 client. + */ +package org.springframework.security.oauth2.client.jackson2; diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/jackson/OAuth2AuthenticationExceptionMixinTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/jackson/OAuth2AuthenticationExceptionMixinTests.java new file mode 100644 index 0000000000..e7d25bc710 --- /dev/null +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/jackson/OAuth2AuthenticationExceptionMixinTests.java @@ -0,0 +1,129 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.oauth2.client.jackson; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; +import tools.jackson.databind.exc.ValueInstantiationException; +import tools.jackson.databind.json.JsonMapper; + +import org.springframework.security.jackson.SecurityJacksonModules; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.OAuth2Error; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * Tests for + * {@link org.springframework.security.oauth2.client.jackson.OAuth2AuthenticationExceptionMixin}. + * + * @author Dennis Neufeld + * @since 5.3.4 + */ +public class OAuth2AuthenticationExceptionMixinTests { + + private JsonMapper mapper; + + @BeforeEach + public void setup() { + ClassLoader loader = getClass().getClassLoader(); + this.mapper = JsonMapper.builder().addModules(SecurityJacksonModules.getModules(loader)).build(); + } + + @Test + public void serializeWhenMixinRegisteredThenSerializes() throws Exception { + OAuth2AuthenticationException exception = new OAuth2AuthenticationException( + new OAuth2Error("[authorization_request_not_found]", "Authorization Request Not Found", "/foo/bar"), + "Authorization Request Not Found"); + String serializedJson = this.mapper.writeValueAsString(exception); + String expected = asJson(exception); + JSONAssert.assertEquals(expected, serializedJson, true); + } + + @Test + public void serializeWhenRequiredAttributesOnlyThenSerializes() throws Exception { + OAuth2AuthenticationException exception = new OAuth2AuthenticationException( + new OAuth2Error("[authorization_request_not_found]")); + String serializedJson = this.mapper.writeValueAsString(exception); + String expected = asJson(exception); + JSONAssert.assertEquals(expected, serializedJson, true); + } + + @Test + public void deserializeWhenMixinNotRegisteredThenThrowJsonProcessingException() { + String json = asJson(new OAuth2AuthenticationException(new OAuth2Error("[authorization_request_not_found]"))); + assertThatExceptionOfType(ValueInstantiationException.class) + .isThrownBy(() -> new JsonMapper().readValue(json, OAuth2AuthenticationException.class)); + } + + @Test + public void deserializeWhenMixinRegisteredThenDeserializes() throws Exception { + OAuth2AuthenticationException expected = new OAuth2AuthenticationException( + new OAuth2Error("[authorization_request_not_found]", "Authorization Request Not Found", "/foo/bar"), + "Authorization Request Not Found"); + OAuth2AuthenticationException exception = this.mapper.readValue(asJson(expected), + OAuth2AuthenticationException.class); + assertThat(exception).isNotNull(); + assertThat(exception.getCause()).isNull(); + assertThat(exception.getMessage()).isEqualTo(expected.getMessage()); + OAuth2Error oauth2Error = exception.getError(); + assertThat(oauth2Error).isNotNull(); + assertThat(oauth2Error.getErrorCode()).isEqualTo(expected.getError().getErrorCode()); + assertThat(oauth2Error.getDescription()).isEqualTo(expected.getError().getDescription()); + assertThat(oauth2Error.getUri()).isEqualTo(expected.getError().getUri()); + } + + @Test + public void deserializeWhenRequiredAttributesOnlyThenDeserializes() throws Exception { + OAuth2AuthenticationException expected = new OAuth2AuthenticationException( + new OAuth2Error("[authorization_request_not_found]")); + OAuth2AuthenticationException exception = this.mapper.readValue(asJson(expected), + OAuth2AuthenticationException.class); + assertThat(exception).isNotNull(); + assertThat(exception.getCause()).isNull(); + assertThat(exception.getMessage()).isNull(); + OAuth2Error oauth2Error = exception.getError(); + assertThat(oauth2Error).isNotNull(); + assertThat(oauth2Error.getErrorCode()).isEqualTo(expected.getError().getErrorCode()); + assertThat(oauth2Error.getDescription()).isNull(); + assertThat(oauth2Error.getUri()).isNull(); + } + + private String asJson(OAuth2AuthenticationException exception) { + OAuth2Error error = exception.getError(); + // @formatter:off + return "\n{" + + "\n \"@class\": \"org.springframework.security.oauth2.core.OAuth2AuthenticationException\"," + + "\n \"error\":" + + "\n {" + + "\n \"@class\":\"org.springframework.security.oauth2.core.OAuth2Error\"," + + "\n \"errorCode\":\"" + error.getErrorCode() + "\"," + + "\n \"description\":" + jsonStringOrNull(error.getDescription()) + "," + + "\n \"uri\":" + jsonStringOrNull(error.getUri()) + + "\n }," + + "\n \"detailMessage\":" + jsonStringOrNull(exception.getMessage()) + + "\n}"; + // @formatter:on + } + + private String jsonStringOrNull(String input) { + return (input != null) ? "\"" + input + "\"" : "null"; + } + +} diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/jackson/OAuth2AuthenticationTokenMixinTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/jackson/OAuth2AuthenticationTokenMixinTests.java new file mode 100644 index 0000000000..d24cf351f8 --- /dev/null +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/jackson/OAuth2AuthenticationTokenMixinTests.java @@ -0,0 +1,339 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.oauth2.client.jackson; + +import java.time.Instant; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +import com.fasterxml.jackson.datatype.jsr310.DecimalUtils; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; +import tools.jackson.core.JacksonException; +import tools.jackson.databind.DeserializationFeature; +import tools.jackson.databind.json.JsonMapper; + +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.jackson.SecurityJacksonModules; +import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; +import org.springframework.security.oauth2.client.authentication.TestOAuth2AuthenticationTokens; +import org.springframework.security.oauth2.core.oidc.IdTokenClaimNames; +import org.springframework.security.oauth2.core.oidc.OidcIdToken; +import org.springframework.security.oauth2.core.oidc.OidcUserInfo; +import org.springframework.security.oauth2.core.oidc.StandardClaimNames; +import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser; +import org.springframework.security.oauth2.core.oidc.user.OidcUserAuthority; +import org.springframework.security.oauth2.core.oidc.user.TestOidcUsers; +import org.springframework.security.oauth2.core.user.DefaultOAuth2User; +import org.springframework.security.oauth2.core.user.OAuth2UserAuthority; +import org.springframework.util.CollectionUtils; +import org.springframework.util.StringUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * Tests for + * {@link org.springframework.security.oauth2.client.jackson.OAuth2AuthenticationTokenMixin}. + * + * @author Joe Grandja + */ +public class OAuth2AuthenticationTokenMixinTests { + + private JsonMapper mapper; + + @BeforeEach + public void setup() { + ClassLoader loader = getClass().getClassLoader(); + this.mapper = JsonMapper.builder() + .addModules(SecurityJacksonModules.getModules(loader)) + // see https://github.com/FasterXML/jackson-databind/issues/3052 for details + .enable(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS) + .build(); + } + + @Test + public void serializeWhenMixinRegisteredThenSerializes() throws Exception { + // OidcUser + OAuth2AuthenticationToken authentication = TestOAuth2AuthenticationTokens.oidcAuthenticated(); + String expectedJson = asJson(authentication); + String json = this.mapper.writeValueAsString(authentication); + JSONAssert.assertEquals(expectedJson, json, true); + // OAuth2User + authentication = TestOAuth2AuthenticationTokens.authenticated(); + expectedJson = asJson(authentication); + json = this.mapper.writeValueAsString(authentication); + JSONAssert.assertEquals(expectedJson, json, true); + } + + @Test + public void serializeWhenRequiredAttributesOnlyThenSerializes() throws Exception { + DefaultOidcUser principal = TestOidcUsers.create(); + principal = new DefaultOidcUser(principal.getAuthorities(), principal.getIdToken()); + OAuth2AuthenticationToken authentication = new OAuth2AuthenticationToken(principal, Collections.emptyList(), + "registration-id"); + String expectedJson = asJson(authentication); + String json = this.mapper.writeValueAsString(authentication); + JSONAssert.assertEquals(expectedJson, json, true); + } + + @Test + public void deserializeWhenMixinNotRegisteredThenThrowJsonProcessingException() { + OAuth2AuthenticationToken authentication = TestOAuth2AuthenticationTokens.oidcAuthenticated(); + String json = asJson(authentication); + assertThatExceptionOfType(JacksonException.class) + .isThrownBy(() -> new JsonMapper().readValue(json, OAuth2AuthenticationToken.class)); + } + + @Test + public void deserializeWhenMixinRegisteredThenDeserializes() throws Exception { + // OidcUser + OAuth2AuthenticationToken expectedAuthentication = TestOAuth2AuthenticationTokens.oidcAuthenticated(); + String json = asJson(expectedAuthentication); + OAuth2AuthenticationToken authentication = this.mapper.readValue(json, OAuth2AuthenticationToken.class); + assertThat(authentication.getAuthorities()).containsExactlyElementsOf(expectedAuthentication.getAuthorities()); + assertThat(authentication.getDetails()).isEqualTo(expectedAuthentication.getDetails()); + assertThat(authentication.isAuthenticated()).isEqualTo(expectedAuthentication.isAuthenticated()); + assertThat(authentication.getAuthorizedClientRegistrationId()) + .isEqualTo(expectedAuthentication.getAuthorizedClientRegistrationId()); + DefaultOidcUser expectedOidcUser = (DefaultOidcUser) expectedAuthentication.getPrincipal(); + DefaultOidcUser oidcUser = (DefaultOidcUser) authentication.getPrincipal(); + assertThat(oidcUser.getAuthorities().containsAll(expectedOidcUser.getAuthorities())).isTrue(); + assertThat(oidcUser.getAttributes()).containsExactlyEntriesOf(expectedOidcUser.getAttributes()); + assertThat(oidcUser.getName()).isEqualTo(expectedOidcUser.getName()); + OidcIdToken expectedIdToken = expectedOidcUser.getIdToken(); + OidcIdToken idToken = oidcUser.getIdToken(); + assertThat(idToken.getTokenValue()).isEqualTo(expectedIdToken.getTokenValue()); + assertThat(idToken.getIssuedAt()).isEqualTo(expectedIdToken.getIssuedAt()); + assertThat(idToken.getExpiresAt()).isEqualTo(expectedIdToken.getExpiresAt()); + assertThat(idToken.getClaims()).containsExactlyEntriesOf(expectedIdToken.getClaims()); + OidcUserInfo expectedUserInfo = expectedOidcUser.getUserInfo(); + OidcUserInfo userInfo = oidcUser.getUserInfo(); + assertThat(userInfo.getClaims()).containsExactlyEntriesOf(expectedUserInfo.getClaims()); + // OAuth2User + expectedAuthentication = TestOAuth2AuthenticationTokens.authenticated(); + json = asJson(expectedAuthentication); + authentication = this.mapper.readValue(json, OAuth2AuthenticationToken.class); + assertThat(authentication.getAuthorities()).containsExactlyElementsOf(expectedAuthentication.getAuthorities()); + assertThat(authentication.getDetails()).isEqualTo(expectedAuthentication.getDetails()); + assertThat(authentication.isAuthenticated()).isEqualTo(expectedAuthentication.isAuthenticated()); + assertThat(authentication.getAuthorizedClientRegistrationId()) + .isEqualTo(expectedAuthentication.getAuthorizedClientRegistrationId()); + DefaultOAuth2User expectedOauth2User = (DefaultOAuth2User) expectedAuthentication.getPrincipal(); + DefaultOAuth2User oauth2User = (DefaultOAuth2User) authentication.getPrincipal(); + assertThat(oauth2User.getAuthorities().containsAll(expectedOauth2User.getAuthorities())).isTrue(); + assertThat(oauth2User.getAttributes()).containsExactlyEntriesOf(expectedOauth2User.getAttributes()); + assertThat(oauth2User.getName()).isEqualTo(expectedOauth2User.getName()); + } + + @Test + public void deserializeWhenRequiredAttributesOnlyThenDeserializes() throws Exception { + DefaultOidcUser expectedPrincipal = TestOidcUsers.create(); + expectedPrincipal = new DefaultOidcUser(expectedPrincipal.getAuthorities(), expectedPrincipal.getIdToken()); + OAuth2AuthenticationToken expectedAuthentication = new OAuth2AuthenticationToken(expectedPrincipal, + Collections.emptyList(), "registration-id"); + String json = asJson(expectedAuthentication); + OAuth2AuthenticationToken authentication = this.mapper.readValue(json, OAuth2AuthenticationToken.class); + assertThat(authentication.getAuthorities()).isEmpty(); + assertThat(authentication.getDetails()).isEqualTo(expectedAuthentication.getDetails()); + assertThat(authentication.isAuthenticated()).isEqualTo(expectedAuthentication.isAuthenticated()); + assertThat(authentication.getAuthorizedClientRegistrationId()) + .isEqualTo(expectedAuthentication.getAuthorizedClientRegistrationId()); + DefaultOidcUser principal = (DefaultOidcUser) authentication.getPrincipal(); + assertThat(principal.getAuthorities().containsAll(expectedPrincipal.getAuthorities())).isTrue(); + assertThat(principal.getAttributes()).containsExactlyEntriesOf(expectedPrincipal.getAttributes()); + assertThat(principal.getName()).isEqualTo(expectedPrincipal.getName()); + OidcIdToken expectedIdToken = expectedPrincipal.getIdToken(); + OidcIdToken idToken = principal.getIdToken(); + assertThat(idToken.getTokenValue()).isEqualTo(expectedIdToken.getTokenValue()); + assertThat(idToken.getIssuedAt()).isEqualTo(expectedIdToken.getIssuedAt()); + assertThat(idToken.getExpiresAt()).isEqualTo(expectedIdToken.getExpiresAt()); + assertThat(idToken.getClaims()).containsExactlyEntriesOf(expectedIdToken.getClaims()); + assertThat(principal.getUserInfo()).isNull(); + } + + private static String asJson(OAuth2AuthenticationToken authentication) { + String principalJson = (authentication.getPrincipal() instanceof DefaultOidcUser) + ? asJson((DefaultOidcUser) authentication.getPrincipal()) + : asJson((DefaultOAuth2User) authentication.getPrincipal()); + // @formatter:off + return "{\n" + + " \"@class\": \"org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken\",\n" + + " \"principal\": " + principalJson + ",\n" + + " \"authorities\": " + asJson(authentication.getAuthorities(), "java.util.Collections$UnmodifiableRandomAccessList") + ",\n" + + " \"authorizedClientRegistrationId\": \"" + authentication.getAuthorizedClientRegistrationId() + "\",\n" + + " \"details\": null\n" + + "}"; + // @formatter:on + } + + private static String asJson(DefaultOAuth2User oauth2User) { + // @formatter:off + return "{\n" + + " \"@class\": \"org.springframework.security.oauth2.core.user.DefaultOAuth2User\",\n" + + " \"authorities\": " + asJson(oauth2User.getAuthorities(), "java.util.Collections$UnmodifiableSet") + ",\n" + + " \"attributes\": {\n" + + " \"@class\": \"java.util.Collections$UnmodifiableMap\",\n" + + " \"username\": \"user\"\n" + + " },\n" + + " \"nameAttributeKey\": \"username\"\n" + + " }"; + // @formatter:on + } + + private static String asJson(DefaultOidcUser oidcUser) { + // @formatter:off + return "{\n" + + " \"@class\": \"org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser\",\n" + + " \"authorities\": " + asJson(oidcUser.getAuthorities(), "java.util.Collections$UnmodifiableSet") + ",\n" + + " \"idToken\": " + asJson(oidcUser.getIdToken()) + ",\n" + + " \"userInfo\": " + asJson(oidcUser.getUserInfo()) + ",\n" + + " \"nameAttributeKey\": \"" + IdTokenClaimNames.SUB + "\"\n" + + " }"; + // @formatter:on + } + + private static String asJson(Collection authorities, String classTypeInfo) { + OAuth2UserAuthority oauth2UserAuthority = null; + OidcUserAuthority oidcUserAuthority = null; + List simpleAuthorities = new ArrayList<>(); + for (GrantedAuthority authority : authorities) { + if (authority instanceof OidcUserAuthority) { + oidcUserAuthority = (OidcUserAuthority) authority; + } + else if (authority instanceof OAuth2UserAuthority) { + oauth2UserAuthority = (OAuth2UserAuthority) authority; + } + else if (authority instanceof SimpleGrantedAuthority) { + simpleAuthorities.add((SimpleGrantedAuthority) authority); + } + } + String authoritiesJson = (oidcUserAuthority != null) ? asJson(oidcUserAuthority) + : (oauth2UserAuthority != null) ? asJson(oauth2UserAuthority) : ""; + if (!simpleAuthorities.isEmpty()) { + if (StringUtils.hasLength(authoritiesJson)) { + authoritiesJson += ","; + } + authoritiesJson += asJson(simpleAuthorities); + } + // @formatter:off + return "[\n" + + " \"" + classTypeInfo + "\",\n" + + " [" + authoritiesJson + "]\n" + + " ]"; + // @formatter:on + } + + private static String asJson(OAuth2UserAuthority oauth2UserAuthority) { + // @formatter:off + return "{\n" + + " \"@class\": \"org.springframework.security.oauth2.core.user.OAuth2UserAuthority\",\n" + + " \"authority\": \"" + oauth2UserAuthority.getAuthority() + "\",\n" + + " \"userNameAttributeName\": \"username\",\n" + + " \"attributes\": {\n" + + " \"@class\": \"java.util.Collections$UnmodifiableMap\",\n" + + " \"username\": \"user\"\n" + + " }\n" + + " }"; + // @formatter:on + } + + private static String asJson(OidcUserAuthority oidcUserAuthority) { + // @formatter:off + return "{\n" + + " \"@class\": \"org.springframework.security.oauth2.core.oidc.user.OidcUserAuthority\",\n" + + " \"authority\": \"" + oidcUserAuthority.getAuthority() + "\",\n" + + " \"userNameAttributeName\": \"" + oidcUserAuthority.getUserNameAttributeName() + "\",\n" + + " \"idToken\": " + asJson(oidcUserAuthority.getIdToken()) + ",\n" + + " \"userInfo\": " + asJson(oidcUserAuthority.getUserInfo()) + "\n" + + " }"; + // @formatter:on + } + + private static String asJson(List simpleAuthorities) { + // @formatter:off + return simpleAuthorities.stream() + .map((authority) -> "{\n" + + " \"@class\": \"org.springframework.security.core.authority.SimpleGrantedAuthority\",\n" + + " \"authority\": \"" + authority.getAuthority() + "\"\n" + + " }") + .collect(Collectors.joining(",")); + // @formatter:on + } + + private static String asJson(OidcIdToken idToken) { + String aud = ""; + if (!CollectionUtils.isEmpty(idToken.getAudience())) { + aud = StringUtils.collectionToDelimitedString(idToken.getAudience(), ",", "\"", "\""); + } + // @formatter:off + return "{\n" + + " \"@class\": \"org.springframework.security.oauth2.core.oidc.OidcIdToken\",\n" + + " \"tokenValue\": \"" + idToken.getTokenValue() + "\",\n" + + " \"issuedAt\": " + toString(idToken.getIssuedAt()) + ",\n" + + " \"expiresAt\": " + toString(idToken.getExpiresAt()) + ",\n" + + " \"claims\": {\n" + + " \"@class\": \"java.util.Collections$UnmodifiableMap\",\n" + + " \"iat\": [\n" + + " \"java.time.Instant\",\n" + + " " + toString(idToken.getIssuedAt()) + "\n" + + " ],\n" + + " \"exp\": [\n" + + " \"java.time.Instant\",\n" + + " " + toString(idToken.getExpiresAt()) + "\n" + + " ],\n" + + " \"sub\": \"" + idToken.getSubject() + "\",\n" + + " \"iss\": \"" + idToken.getIssuer() + "\",\n" + + " \"aud\": [\n" + + " \"java.util.Collections$UnmodifiableSet\",\n" + + " [" + aud + "]\n" + + " ],\n" + + " \"azp\": \"" + idToken.getAuthorizedParty() + "\"\n" + + " }\n" + + " }"; + // @formatter:on + } + + private static String asJson(OidcUserInfo userInfo) { + if (userInfo == null) { + return null; + } + // @formatter:off + return "{\n" + + " \"@class\": \"org.springframework.security.oauth2.core.oidc.OidcUserInfo\",\n" + + " \"claims\": {\n" + + " \"@class\": \"java.util.Collections$UnmodifiableMap\",\n" + + " \"sub\": \"" + userInfo.getSubject() + "\",\n" + + " \"name\": \"" + userInfo.getClaim(StandardClaimNames.NAME) + "\"\n" + + " }\n" + + " }"; + // @formatter:on + } + + private static String toString(Instant instant) { + if (instant == null) { + return null; + } + return DecimalUtils.toBigDecimal(instant.getEpochSecond(), instant.getNano()).toString(); + } + +} diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/jackson/OAuth2AuthorizationRequestMixinTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/jackson/OAuth2AuthorizationRequestMixinTests.java new file mode 100644 index 0000000000..597f78947a --- /dev/null +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/jackson/OAuth2AuthorizationRequestMixinTests.java @@ -0,0 +1,199 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.oauth2.client.jackson; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; +import tools.jackson.core.JacksonException; +import tools.jackson.core.exc.StreamReadException; +import tools.jackson.databind.json.JsonMapper; + +import org.springframework.security.jackson.SecurityJacksonModules; +import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; +import org.springframework.security.oauth2.core.endpoint.TestOAuth2AuthorizationRequests; +import org.springframework.util.CollectionUtils; +import org.springframework.util.StringUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * Tests for + * {@link org.springframework.security.oauth2.client.jackson.OAuth2AuthorizationRequestMixin}. + * + * @author Joe Grandja + */ +public class OAuth2AuthorizationRequestMixinTests { + + private JsonMapper mapper; + + private OAuth2AuthorizationRequest.Builder authorizationRequestBuilder; + + @BeforeEach + public void setup() { + ClassLoader loader = getClass().getClassLoader(); + this.mapper = JsonMapper.builder().addModules(SecurityJacksonModules.getModules(loader)).build(); + Map additionalParameters = new LinkedHashMap<>(); + additionalParameters.put("param1", "value1"); + additionalParameters.put("param2", "value2"); + // @formatter:off + this.authorizationRequestBuilder = TestOAuth2AuthorizationRequests.request() + .scope("read", "write") + .additionalParameters(additionalParameters); + // @formatter:on + } + + @Test + public void serializeWhenMixinRegisteredThenSerializes() throws Exception { + OAuth2AuthorizationRequest authorizationRequest = this.authorizationRequestBuilder.build(); + String expectedJson = asJson(authorizationRequest); + String json = this.mapper.writeValueAsString(authorizationRequest); + JSONAssert.assertEquals(expectedJson, json, true); + } + + @Test + public void serializeWhenRequiredAttributesOnlyThenSerializes() throws Exception { + // @formatter:off + OAuth2AuthorizationRequest authorizationRequest = this.authorizationRequestBuilder + .scopes(null) + .state(null) + .additionalParameters(Map::clear) + .attributes(Map::clear) + .build(); + // @formatter:on + String expectedJson = asJson(authorizationRequest); + String json = this.mapper.writeValueAsString(authorizationRequest); + JSONAssert.assertEquals(expectedJson, json, true); + } + + @Test + public void deserializeWhenMixinNotRegisteredThenThrowJsonProcessingException() { + String json = asJson(this.authorizationRequestBuilder.build()); + assertThatExceptionOfType(JacksonException.class) + .isThrownBy(() -> new JsonMapper().readValue(json, OAuth2AuthorizationRequest.class)); + } + + @Test + public void deserializeWhenMixinRegisteredThenDeserializes() throws Exception { + OAuth2AuthorizationRequest expectedAuthorizationRequest = this.authorizationRequestBuilder.build(); + String json = asJson(expectedAuthorizationRequest); + OAuth2AuthorizationRequest authorizationRequest = this.mapper.readValue(json, OAuth2AuthorizationRequest.class); + assertThat(authorizationRequest.getAuthorizationUri()) + .isEqualTo(expectedAuthorizationRequest.getAuthorizationUri()); + assertThat(authorizationRequest.getGrantType()).isEqualTo(expectedAuthorizationRequest.getGrantType()); + assertThat(authorizationRequest.getResponseType()).isEqualTo(expectedAuthorizationRequest.getResponseType()); + assertThat(authorizationRequest.getClientId()).isEqualTo(expectedAuthorizationRequest.getClientId()); + assertThat(authorizationRequest.getRedirectUri()).isEqualTo(expectedAuthorizationRequest.getRedirectUri()); + assertThat(authorizationRequest.getScopes()).isEqualTo(expectedAuthorizationRequest.getScopes()); + assertThat(authorizationRequest.getState()).isEqualTo(expectedAuthorizationRequest.getState()); + assertThat(authorizationRequest.getAdditionalParameters()) + .containsExactlyEntriesOf(expectedAuthorizationRequest.getAdditionalParameters()); + assertThat(authorizationRequest.getAuthorizationRequestUri()) + .isEqualTo(expectedAuthorizationRequest.getAuthorizationRequestUri()); + assertThat(authorizationRequest.getAttributes()) + .containsExactlyEntriesOf(expectedAuthorizationRequest.getAttributes()); + } + + @Test + public void deserializeWhenRequiredAttributesOnlyThenDeserializes() throws Exception { + // @formatter:off + OAuth2AuthorizationRequest expectedAuthorizationRequest = this.authorizationRequestBuilder.scopes(null) + .state(null) + .additionalParameters(Map::clear) + .attributes(Map::clear) + .build(); + // @formatter:on + String json = asJson(expectedAuthorizationRequest); + OAuth2AuthorizationRequest authorizationRequest = this.mapper.readValue(json, OAuth2AuthorizationRequest.class); + assertThat(authorizationRequest.getAuthorizationUri()) + .isEqualTo(expectedAuthorizationRequest.getAuthorizationUri()); + assertThat(authorizationRequest.getGrantType()).isEqualTo(expectedAuthorizationRequest.getGrantType()); + assertThat(authorizationRequest.getResponseType()).isEqualTo(expectedAuthorizationRequest.getResponseType()); + assertThat(authorizationRequest.getClientId()).isEqualTo(expectedAuthorizationRequest.getClientId()); + assertThat(authorizationRequest.getRedirectUri()).isEqualTo(expectedAuthorizationRequest.getRedirectUri()); + assertThat(authorizationRequest.getScopes()).isEmpty(); + assertThat(authorizationRequest.getState()).isNull(); + assertThat(authorizationRequest.getAdditionalParameters()).isEmpty(); + assertThat(authorizationRequest.getAuthorizationRequestUri()) + .isEqualTo(expectedAuthorizationRequest.getAuthorizationRequestUri()); + assertThat(authorizationRequest.getAttributes()).isEmpty(); + } + + @Test + public void deserializeWhenInvalidAuthorizationGrantTypeThenThrowJsonParseException() { + OAuth2AuthorizationRequest authorizationRequest = this.authorizationRequestBuilder.build(); + String json = asJson(authorizationRequest).replace("authorization_code", "client_credentials"); + assertThatExceptionOfType(StreamReadException.class) + .isThrownBy(() -> this.mapper.readValue(json, OAuth2AuthorizationRequest.class)) + .withMessageContaining("Invalid authorizationGrantType"); + } + + private static String asJson(OAuth2AuthorizationRequest authorizationRequest) { + String scopes = ""; + if (!CollectionUtils.isEmpty(authorizationRequest.getScopes())) { + scopes = StringUtils.collectionToDelimitedString(authorizationRequest.getScopes(), ",", "\"", "\""); + } + String additionalParameters = "\"@class\": \"java.util.Collections$UnmodifiableMap\""; + if (!CollectionUtils.isEmpty(authorizationRequest.getAdditionalParameters())) { + additionalParameters += "," + authorizationRequest.getAdditionalParameters() + .keySet() + .stream() + .map((key) -> "\"" + key + "\": \"" + authorizationRequest.getAdditionalParameters().get(key) + "\"") + .collect(Collectors.joining(",")); + } + String attributes = "\"@class\": \"java.util.Collections$UnmodifiableMap\""; + if (!CollectionUtils.isEmpty(authorizationRequest.getAttributes())) { + attributes += "," + authorizationRequest.getAttributes() + .keySet() + .stream() + .map((key) -> "\"" + key + "\": \"" + authorizationRequest.getAttributes().get(key) + "\"") + .collect(Collectors.joining(",")); + } + // @formatter:off + return "{\n" + + " \"@class\": \"org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest\",\n" + + " \"authorizationUri\": \"" + authorizationRequest.getAuthorizationUri() + "\",\n" + + " \"authorizationGrantType\": {\n" + + " \"value\": \"" + authorizationRequest.getGrantType().getValue() + "\"\n" + + " },\n" + + " \"responseType\": {\n" + + " \"value\": \"" + authorizationRequest.getResponseType().getValue() + "\"\n" + + " },\n" + + " \"clientId\": \"" + authorizationRequest.getClientId() + "\",\n" + + " \"redirectUri\": \"" + authorizationRequest.getRedirectUri() + "\",\n" + + " \"scopes\": [\n" + + " \"java.util.Collections$UnmodifiableSet\",\n" + + " [" + scopes + "]\n" + + " ],\n" + + " \"state\": " + ((authorizationRequest.getState() != null) ? "\"" + authorizationRequest.getState() + "\"" : "null") + ",\n" + + " \"additionalParameters\": {\n" + + " " + additionalParameters + "\n" + + " },\n" + + " \"authorizationRequestUri\": \"" + authorizationRequest.getAuthorizationRequestUri() + "\",\n" + + " \"attributes\": {\n" + + " " + attributes + "\n" + + " }\n" + + "}"; + // @formatter:on + } + +} diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/jackson/OAuth2AuthorizedClientMixinTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/jackson/OAuth2AuthorizedClientMixinTests.java new file mode 100644 index 0000000000..7d1ff8cf4e --- /dev/null +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/jackson/OAuth2AuthorizedClientMixinTests.java @@ -0,0 +1,395 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.oauth2.client.jackson; + +import java.time.Instant; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.stream.Collectors; + +import com.fasterxml.jackson.datatype.jsr310.DecimalUtils; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; +import tools.jackson.core.JacksonException; +import tools.jackson.databind.json.JsonMapper; + +import org.springframework.security.jackson.SecurityJacksonModules; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.client.registration.TestClientRegistrations; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.OAuth2AccessToken; +import org.springframework.security.oauth2.core.OAuth2RefreshToken; +import org.springframework.security.oauth2.core.TestOAuth2AccessTokens; +import org.springframework.security.oauth2.core.TestOAuth2RefreshTokens; +import org.springframework.util.CollectionUtils; +import org.springframework.util.StringUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * Tests for + * {@link org.springframework.security.oauth2.client.jackson.OAuth2AuthorizedClientMixin}. + * + * @author Joe Grandja + */ +public class OAuth2AuthorizedClientMixinTests { + + private JsonMapper mapper; + + private ClientRegistration.Builder clientRegistrationBuilder; + + private OAuth2AccessToken accessToken; + + private OAuth2RefreshToken refreshToken; + + private String principalName; + + @BeforeEach + public void setup() { + ClassLoader loader = getClass().getClassLoader(); + this.mapper = JsonMapper.builder().addModules(SecurityJacksonModules.getModules(loader)).build(); + Map providerConfigurationMetadata = new LinkedHashMap<>(); + providerConfigurationMetadata.put("config1", "value1"); + providerConfigurationMetadata.put("config2", "value2"); + // @formatter:off + this.clientRegistrationBuilder = TestClientRegistrations.clientRegistration() + .authorizationGrantType(new AuthorizationGrantType("custom-grant")) + .scope("read", "write") + .providerConfigurationMetadata(providerConfigurationMetadata); + // @formatter:on + this.accessToken = TestOAuth2AccessTokens.scopes("read", "write"); + this.refreshToken = TestOAuth2RefreshTokens.refreshToken(); + this.principalName = "principal-name"; + } + + @Test + public void serializeWhenMixinRegisteredThenSerializes() throws Exception { + OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(this.clientRegistrationBuilder.build(), + this.principalName, this.accessToken, this.refreshToken); + String expectedJson = asJson(authorizedClient); + String json = this.mapper.writeValueAsString(authorizedClient); + JSONAssert.assertEquals(expectedJson, json, true); + } + + @Test + public void serializeWhenRequiredAttributesOnlyThenSerializes() throws Exception { + // @formatter:off + ClientRegistration clientRegistration = TestClientRegistrations.clientRegistration() + .clientSecret(null) + .clientName(null) + .userInfoUri(null) + .userNameAttributeName(null) + .jwkSetUri(null) + .issuerUri(null) + .build(); + // @formatter:on + OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(clientRegistration, this.principalName, + TestOAuth2AccessTokens.noScopes()); + String expectedJson = asJson(authorizedClient); + String json = this.mapper.writeValueAsString(authorizedClient); + JSONAssert.assertEquals(expectedJson, json, true); + } + + @Test + public void deserializeWhenMixinNotRegisteredThenThrowJsonProcessingException() { + OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(this.clientRegistrationBuilder.build(), + this.principalName, this.accessToken); + String json = asJson(authorizedClient); + assertThatExceptionOfType(JacksonException.class) + .isThrownBy(() -> new JsonMapper().readValue(json, OAuth2AuthorizedClient.class)); + } + + @Test + public void deserializeWhenMixinRegisteredThenDeserializes() throws Exception { + ClientRegistration expectedClientRegistration = this.clientRegistrationBuilder.build(); + OAuth2AccessToken expectedAccessToken = this.accessToken; + OAuth2RefreshToken expectedRefreshToken = this.refreshToken; + OAuth2AuthorizedClient expectedAuthorizedClient = new OAuth2AuthorizedClient(expectedClientRegistration, + this.principalName, expectedAccessToken, expectedRefreshToken); + String json = asJson(expectedAuthorizedClient); + OAuth2AuthorizedClient authorizedClient = this.mapper.readValue(json, OAuth2AuthorizedClient.class); + ClientRegistration clientRegistration = authorizedClient.getClientRegistration(); + assertThat(clientRegistration.getRegistrationId()).isEqualTo(expectedClientRegistration.getRegistrationId()); + assertThat(clientRegistration.getClientId()).isEqualTo(expectedClientRegistration.getClientId()); + assertThat(clientRegistration.getClientSecret()).isEqualTo(expectedClientRegistration.getClientSecret()); + assertThat(clientRegistration.getClientAuthenticationMethod()) + .isEqualTo(expectedClientRegistration.getClientAuthenticationMethod()); + assertThat(clientRegistration.getAuthorizationGrantType()) + .isEqualTo(expectedClientRegistration.getAuthorizationGrantType()); + assertThat(clientRegistration.getRedirectUri()).isEqualTo(expectedClientRegistration.getRedirectUri()); + assertThat(clientRegistration.getScopes()).isEqualTo(expectedClientRegistration.getScopes()); + assertThat(clientRegistration.getProviderDetails().getAuthorizationUri()) + .isEqualTo(expectedClientRegistration.getProviderDetails().getAuthorizationUri()); + assertThat(clientRegistration.getProviderDetails().getTokenUri()) + .isEqualTo(expectedClientRegistration.getProviderDetails().getTokenUri()); + assertThat(clientRegistration.getProviderDetails().getUserInfoEndpoint().getUri()) + .isEqualTo(expectedClientRegistration.getProviderDetails().getUserInfoEndpoint().getUri()); + assertThat(clientRegistration.getProviderDetails().getUserInfoEndpoint().getAuthenticationMethod()) + .isEqualTo(expectedClientRegistration.getProviderDetails().getUserInfoEndpoint().getAuthenticationMethod()); + assertThat(clientRegistration.getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName()).isEqualTo( + expectedClientRegistration.getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName()); + assertThat(clientRegistration.getProviderDetails().getJwkSetUri()) + .isEqualTo(expectedClientRegistration.getProviderDetails().getJwkSetUri()); + assertThat(clientRegistration.getProviderDetails().getIssuerUri()) + .isEqualTo(expectedClientRegistration.getProviderDetails().getIssuerUri()); + assertThat(clientRegistration.getProviderDetails().getConfigurationMetadata()) + .containsExactlyEntriesOf(clientRegistration.getProviderDetails().getConfigurationMetadata()); + assertThat(clientRegistration.getClientName()).isEqualTo(expectedClientRegistration.getClientName()); + assertThat(authorizedClient.getPrincipalName()).isEqualTo(expectedAuthorizedClient.getPrincipalName()); + OAuth2AccessToken accessToken = authorizedClient.getAccessToken(); + assertThat(accessToken.getTokenType()).isEqualTo(expectedAccessToken.getTokenType()); + assertThat(accessToken.getScopes()).isEqualTo(expectedAccessToken.getScopes()); + assertThat(accessToken.getTokenValue()).isEqualTo(expectedAccessToken.getTokenValue()); + assertThat(accessToken.getIssuedAt()).isEqualTo(expectedAccessToken.getIssuedAt()); + assertThat(accessToken.getExpiresAt()).isEqualTo(expectedAccessToken.getExpiresAt()); + OAuth2RefreshToken refreshToken = authorizedClient.getRefreshToken(); + assertThat(refreshToken.getTokenValue()).isEqualTo(expectedRefreshToken.getTokenValue()); + assertThat(refreshToken.getIssuedAt()).isEqualTo(expectedRefreshToken.getIssuedAt()); + assertThat(refreshToken.getExpiresAt()).isEqualTo(expectedRefreshToken.getExpiresAt()); + } + + @Test + public void deserializeWhenRequiredAttributesOnlyThenDeserializes() throws Exception { + // @formatter:off + ClientRegistration expectedClientRegistration = TestClientRegistrations.clientRegistration() + .clientSecret(null) + .clientName(null) + .userInfoUri(null) + .userNameAttributeName(null) + .jwkSetUri(null) + .issuerUri(null) + .build(); + // @formatter:on + OAuth2AccessToken expectedAccessToken = TestOAuth2AccessTokens.noScopes(); + OAuth2AuthorizedClient expectedAuthorizedClient = new OAuth2AuthorizedClient(expectedClientRegistration, + this.principalName, expectedAccessToken); + String json = asJson(expectedAuthorizedClient); + OAuth2AuthorizedClient authorizedClient = this.mapper.readValue(json, OAuth2AuthorizedClient.class); + ClientRegistration clientRegistration = authorizedClient.getClientRegistration(); + assertThat(clientRegistration.getRegistrationId()).isEqualTo(expectedClientRegistration.getRegistrationId()); + assertThat(clientRegistration.getClientId()).isEqualTo(expectedClientRegistration.getClientId()); + assertThat(clientRegistration.getClientSecret()).isEmpty(); + assertThat(clientRegistration.getClientAuthenticationMethod()) + .isEqualTo(expectedClientRegistration.getClientAuthenticationMethod()); + assertThat(clientRegistration.getAuthorizationGrantType()) + .isEqualTo(expectedClientRegistration.getAuthorizationGrantType()); + assertThat(clientRegistration.getRedirectUri()).isEqualTo(expectedClientRegistration.getRedirectUri()); + assertThat(clientRegistration.getScopes()).isEqualTo(expectedClientRegistration.getScopes()); + assertThat(clientRegistration.getProviderDetails().getAuthorizationUri()) + .isEqualTo(expectedClientRegistration.getProviderDetails().getAuthorizationUri()); + assertThat(clientRegistration.getProviderDetails().getTokenUri()) + .isEqualTo(expectedClientRegistration.getProviderDetails().getTokenUri()); + assertThat(clientRegistration.getProviderDetails().getUserInfoEndpoint().getUri()).isNull(); + assertThat(clientRegistration.getProviderDetails().getUserInfoEndpoint().getAuthenticationMethod()) + .isEqualTo(expectedClientRegistration.getProviderDetails().getUserInfoEndpoint().getAuthenticationMethod()); + assertThat(clientRegistration.getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName()).isNull(); + assertThat(clientRegistration.getProviderDetails().getJwkSetUri()).isNull(); + assertThat(clientRegistration.getProviderDetails().getIssuerUri()).isNull(); + assertThat(clientRegistration.getProviderDetails().getConfigurationMetadata()).isEmpty(); + assertThat(clientRegistration.getClientName()).isEqualTo(clientRegistration.getRegistrationId()); + assertThat(authorizedClient.getPrincipalName()).isEqualTo(expectedAuthorizedClient.getPrincipalName()); + OAuth2AccessToken accessToken = authorizedClient.getAccessToken(); + assertThat(accessToken.getTokenType()).isEqualTo(expectedAccessToken.getTokenType()); + assertThat(accessToken.getScopes()).isEmpty(); + assertThat(accessToken.getTokenValue()).isEqualTo(expectedAccessToken.getTokenValue()); + assertThat(accessToken.getIssuedAt()).isEqualTo(expectedAccessToken.getIssuedAt()); + assertThat(accessToken.getExpiresAt()).isEqualTo(expectedAccessToken.getExpiresAt()); + assertThat(authorizedClient.getRefreshToken()).isNull(); + } + + @Test + void deserializeWhenClientSettingsPropertyDoesNotExistThenDefaulted() throws JacksonException { + // ClientRegistration.clientSettings was added later, so old values will be + // serialized without that property + // this test checks for passivity + ClientRegistration clientRegistration = this.clientRegistrationBuilder.build(); + ClientRegistration.ProviderDetails providerDetails = clientRegistration.getProviderDetails(); + ClientRegistration.ProviderDetails.UserInfoEndpoint userInfoEndpoint = providerDetails.getUserInfoEndpoint(); + String scopes = ""; + if (!CollectionUtils.isEmpty(clientRegistration.getScopes())) { + scopes = StringUtils.collectionToDelimitedString(clientRegistration.getScopes(), ",", "\"", "\""); + } + String configurationMetadata = "\"@class\": \"java.util.Collections$UnmodifiableMap\""; + if (!CollectionUtils.isEmpty(providerDetails.getConfigurationMetadata())) { + configurationMetadata += "," + providerDetails.getConfigurationMetadata() + .keySet() + .stream() + .map((key) -> "\"" + key + "\": \"" + providerDetails.getConfigurationMetadata().get(key) + "\"") + .collect(Collectors.joining(",")); + } + // @formatter:off + String json = "{\n" + + " \"@class\": \"org.springframework.security.oauth2.client.registration.ClientRegistration\",\n" + + " \"registrationId\": \"" + clientRegistration.getRegistrationId() + "\",\n" + + " \"clientId\": \"" + clientRegistration.getClientId() + "\",\n" + + " \"clientSecret\": \"" + clientRegistration.getClientSecret() + "\",\n" + + " \"clientAuthenticationMethod\": {\n" + + " \"value\": \"" + clientRegistration.getClientAuthenticationMethod().getValue() + "\"\n" + + " },\n" + + " \"authorizationGrantType\": {\n" + + " \"value\": \"" + clientRegistration.getAuthorizationGrantType().getValue() + "\"\n" + + " },\n" + + " \"redirectUri\": \"" + clientRegistration.getRedirectUri() + "\",\n" + + " \"scopes\": [\n" + + " \"java.util.Collections$UnmodifiableSet\",\n" + + " [" + scopes + "]\n" + + " ],\n" + + " \"providerDetails\": {\n" + + " \"@class\": \"org.springframework.security.oauth2.client.registration.ClientRegistration$ProviderDetails\",\n" + + " \"authorizationUri\": \"" + providerDetails.getAuthorizationUri() + "\",\n" + + " \"tokenUri\": \"" + providerDetails.getTokenUri() + "\",\n" + + " \"userInfoEndpoint\": {\n" + + " \"@class\": \"org.springframework.security.oauth2.client.registration.ClientRegistration$ProviderDetails$UserInfoEndpoint\",\n" + + " \"uri\": " + ((userInfoEndpoint.getUri() != null) ? "\"" + userInfoEndpoint.getUri() + "\"" : null) + ",\n" + + " \"authenticationMethod\": {\n" + + " \"value\": \"" + userInfoEndpoint.getAuthenticationMethod().getValue() + "\"\n" + + " },\n" + + " \"userNameAttributeName\": " + ((userInfoEndpoint.getUserNameAttributeName() != null) ? "\"" + userInfoEndpoint.getUserNameAttributeName() + "\"" : null) + "\n" + + " },\n" + + " \"jwkSetUri\": " + ((providerDetails.getJwkSetUri() != null) ? "\"" + providerDetails.getJwkSetUri() + "\"" : null) + ",\n" + + " \"issuerUri\": " + ((providerDetails.getIssuerUri() != null) ? "\"" + providerDetails.getIssuerUri() + "\"" : null) + ",\n" + + " \"configurationMetadata\": {\n" + + " " + configurationMetadata + "\n" + + " }\n" + + " },\n" + + " \"clientName\": \"" + clientRegistration.getClientName() + "\"\n" + + "}"; + // @formatter:on + // validate the test input + assertThat(json).doesNotContain("clientSettings"); + ClientRegistration registration = this.mapper.readValue(json, ClientRegistration.class); + // the default value of requireProofKey is false + assertThat(registration.getClientSettings().isRequireProofKey()).isFalse(); + } + + private static String asJson(OAuth2AuthorizedClient authorizedClient) { + // @formatter:off + return "{\n" + + " \"@class\": \"org.springframework.security.oauth2.client.OAuth2AuthorizedClient\",\n" + + " \"clientRegistration\": " + asJson(authorizedClient.getClientRegistration()) + ",\n" + + " \"principalName\": \"" + authorizedClient.getPrincipalName() + "\",\n" + + " \"accessToken\": " + asJson(authorizedClient.getAccessToken()) + ",\n" + + " \"refreshToken\": " + asJson(authorizedClient.getRefreshToken()) + "\n" + + "}"; + // @formatter:on + } + + private static String asJson(ClientRegistration clientRegistration) { + ClientRegistration.ProviderDetails providerDetails = clientRegistration.getProviderDetails(); + ClientRegistration.ProviderDetails.UserInfoEndpoint userInfoEndpoint = providerDetails.getUserInfoEndpoint(); + String scopes = ""; + if (!CollectionUtils.isEmpty(clientRegistration.getScopes())) { + scopes = StringUtils.collectionToDelimitedString(clientRegistration.getScopes(), ",", "\"", "\""); + } + String configurationMetadata = "\"@class\": \"java.util.Collections$UnmodifiableMap\""; + if (!CollectionUtils.isEmpty(providerDetails.getConfigurationMetadata())) { + configurationMetadata += "," + providerDetails.getConfigurationMetadata() + .keySet() + .stream() + .map((key) -> "\"" + key + "\": \"" + providerDetails.getConfigurationMetadata().get(key) + "\"") + .collect(Collectors.joining(",")); + } + // @formatter:off + return "{\n" + + " \"@class\": \"org.springframework.security.oauth2.client.registration.ClientRegistration\",\n" + + " \"registrationId\": \"" + clientRegistration.getRegistrationId() + "\",\n" + + " \"clientId\": \"" + clientRegistration.getClientId() + "\",\n" + + " \"clientSecret\": \"" + clientRegistration.getClientSecret() + "\",\n" + + " \"clientAuthenticationMethod\": {\n" + + " \"value\": \"" + clientRegistration.getClientAuthenticationMethod().getValue() + "\"\n" + + " },\n" + + " \"authorizationGrantType\": {\n" + + " \"value\": \"" + clientRegistration.getAuthorizationGrantType().getValue() + "\"\n" + + " },\n" + + " \"redirectUri\": \"" + clientRegistration.getRedirectUri() + "\",\n" + + " \"scopes\": [\n" + + " \"java.util.Collections$UnmodifiableSet\",\n" + + " [" + scopes + "]\n" + + " ],\n" + + " \"providerDetails\": {\n" + + " \"@class\": \"org.springframework.security.oauth2.client.registration.ClientRegistration$ProviderDetails\",\n" + + " \"authorizationUri\": \"" + providerDetails.getAuthorizationUri() + "\",\n" + + " \"tokenUri\": \"" + providerDetails.getTokenUri() + "\",\n" + + " \"userInfoEndpoint\": {\n" + + " \"@class\": \"org.springframework.security.oauth2.client.registration.ClientRegistration$ProviderDetails$UserInfoEndpoint\",\n" + + " \"uri\": " + ((userInfoEndpoint.getUri() != null) ? "\"" + userInfoEndpoint.getUri() + "\"" : null) + ",\n" + + " \"authenticationMethod\": {\n" + + " \"value\": \"" + userInfoEndpoint.getAuthenticationMethod().getValue() + "\"\n" + + " },\n" + + " \"userNameAttributeName\": " + ((userInfoEndpoint.getUserNameAttributeName() != null) ? "\"" + userInfoEndpoint.getUserNameAttributeName() + "\"" : null) + "\n" + + " },\n" + + " \"jwkSetUri\": " + ((providerDetails.getJwkSetUri() != null) ? "\"" + providerDetails.getJwkSetUri() + "\"" : null) + ",\n" + + " \"issuerUri\": " + ((providerDetails.getIssuerUri() != null) ? "\"" + providerDetails.getIssuerUri() + "\"" : null) + ",\n" + + " \"configurationMetadata\": {\n" + + " " + configurationMetadata + "\n" + + " }\n" + + " },\n" + + " \"clientName\": \"" + clientRegistration.getClientName() + "\",\n" + + " \"clientSettings\": {\n" + + " \"requireProofKey\": " + clientRegistration.getClientSettings().isRequireProofKey() + "\n" + + " }\n" + + "}"; + // @formatter:on + } + + private static String asJson(OAuth2AccessToken accessToken) { + String scopes = ""; + if (!CollectionUtils.isEmpty(accessToken.getScopes())) { + scopes = StringUtils.collectionToDelimitedString(accessToken.getScopes(), ",", "\"", "\""); + } + // @formatter:off + return "{\n" + + " \"@class\": \"org.springframework.security.oauth2.core.OAuth2AccessToken\",\n" + + " \"tokenType\": {\n" + + " \"value\": \"" + accessToken.getTokenType().getValue() + "\"\n" + + " },\n" + + " \"tokenValue\": \"" + accessToken.getTokenValue() + "\",\n" + + " \"issuedAt\": " + toString(accessToken.getIssuedAt()) + ",\n" + + " \"expiresAt\": " + toString(accessToken.getExpiresAt()) + ",\n" + + " \"scopes\": [\n" + + " \"java.util.Collections$UnmodifiableSet\",\n" + + " [" + scopes + "]\n" + + " ]\n" + + "}"; + // @formatter:on + } + + private static String asJson(OAuth2RefreshToken refreshToken) { + if (refreshToken == null) { + return null; + } + // @formatter:off + return "{\n" + + " \"@class\": \"org.springframework.security.oauth2.core.OAuth2RefreshToken\",\n" + + " \"tokenValue\": \"" + refreshToken.getTokenValue() + "\",\n" + + " \"issuedAt\": " + toString(refreshToken.getIssuedAt()) + ",\n" + + " \"expiresAt\": " + toString(refreshToken.getExpiresAt()) + "\n" + + "}"; + // @formatter:on + } + + private static String toString(Instant instant) { + if (instant == null) { + return null; + } + return DecimalUtils.toBigDecimal(instant.getEpochSecond(), instant.getNano()).toString(); + } + +} diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/jackson/StdConvertersTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/jackson/StdConvertersTests.java new file mode 100644 index 0000000000..cbd95c63a3 --- /dev/null +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/jackson/StdConvertersTests.java @@ -0,0 +1,53 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.oauth2.client.jackson; + +import java.util.stream.Stream; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import tools.jackson.databind.JsonNode; +import tools.jackson.databind.node.JsonNodeFactory; +import tools.jackson.databind.node.ObjectNode; +import tools.jackson.databind.util.StdConverter; + +import org.springframework.security.oauth2.core.ClientAuthenticationMethod; + +import static org.assertj.core.api.Assertions.assertThat; + +public class StdConvertersTests { + + private final StdConverter clientAuthenticationMethodConverter = new org.springframework.security.oauth2.client.jackson.StdConverters.ClientAuthenticationMethodConverter(); + + @ParameterizedTest + @MethodSource("convertWhenClientAuthenticationMethodConvertedThenDeserializes") + void convertWhenClientAuthenticationMethodConvertedThenDeserializes(String clientAuthenticationMethod) { + ObjectNode jsonNode = JsonNodeFactory.instance.objectNode(); + jsonNode.put("value", clientAuthenticationMethod); + ClientAuthenticationMethod actual = this.clientAuthenticationMethodConverter.convert(jsonNode); + assertThat(actual.getValue()).isEqualTo(clientAuthenticationMethod); + } + + static Stream convertWhenClientAuthenticationMethodConvertedThenDeserializes() { + return Stream.of(Arguments.of(ClientAuthenticationMethod.CLIENT_SECRET_BASIC.getValue()), + Arguments.of(ClientAuthenticationMethod.CLIENT_SECRET_POST.getValue()), + Arguments.of(ClientAuthenticationMethod.PRIVATE_KEY_JWT.getValue()), + Arguments.of(ClientAuthenticationMethod.NONE.getValue()), Arguments.of("custom_method")); + } + +} diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/registration/ClientRegistrationsTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/registration/ClientRegistrationsTests.java index bf7dba4718..0cf1002d56 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/registration/ClientRegistrationsTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/registration/ClientRegistrationsTests.java @@ -20,8 +20,6 @@ import java.net.URI; import java.util.Arrays; import java.util.Map; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.ObjectMapper; import okhttp3.mockwebserver.Dispatcher; import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.MockWebServer; @@ -29,6 +27,8 @@ import okhttp3.mockwebserver.RecordedRequest; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import tools.jackson.core.type.TypeReference; +import tools.jackson.databind.json.JsonMapper; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; @@ -111,7 +111,7 @@ public class ClientRegistrationsTests { private MockWebServer server; - private ObjectMapper mapper = new ObjectMapper(); + private JsonMapper mapper = new JsonMapper(); private Map response; diff --git a/oauth2/oauth2-jose/spring-security-oauth2-jose.gradle b/oauth2/oauth2-jose/spring-security-oauth2-jose.gradle index 0397cfafd9..8af3d4d30b 100644 --- a/oauth2/oauth2-jose/spring-security-oauth2-jose.gradle +++ b/oauth2/oauth2-jose/spring-security-oauth2-jose.gradle @@ -15,7 +15,7 @@ dependencies { testImplementation "jakarta.servlet:jakarta.servlet-api" testImplementation 'com.squareup.okhttp3:mockwebserver' testImplementation 'io.projectreactor.netty:reactor-netty' - testImplementation 'com.fasterxml.jackson.core:jackson-databind' + testImplementation 'tools.jackson.core:jackson-databind' testImplementation "org.assertj:assertj-core" testImplementation "org.junit.jupiter:junit-jupiter-api" testImplementation "org.junit.jupiter:junit-jupiter-params" diff --git a/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/JwtDecodersTests.java b/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/JwtDecodersTests.java index ebbd50f9a0..ac78e0d9e2 100644 --- a/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/JwtDecodersTests.java +++ b/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/JwtDecodersTests.java @@ -21,10 +21,6 @@ import java.util.HashMap; import java.util.Map; import java.util.Optional; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.JsonMappingException; -import com.fasterxml.jackson.databind.ObjectMapper; import okhttp3.HttpUrl; import okhttp3.mockwebserver.Dispatcher; import okhttp3.mockwebserver.MockResponse; @@ -33,6 +29,8 @@ import okhttp3.mockwebserver.RecordedRequest; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import tools.jackson.core.type.TypeReference; +import tools.jackson.databind.json.JsonMapper; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; @@ -191,8 +189,7 @@ public class JwtDecodersTests { // gh-7512 @Test - public void issuerWhenResponseDoesNotContainJwksUriThenThrowsIllegalArgumentException() - throws JsonMappingException, JsonProcessingException { + public void issuerWhenResponseDoesNotContainJwksUriThenThrowsIllegalArgumentException() { prepareConfigurationResponse(this.buildResponseWithMissingJwksUri()); // @formatter:off assertThatIllegalArgumentException() @@ -203,8 +200,7 @@ public class JwtDecodersTests { // gh-7512 @Test - public void issuerWhenOidcFallbackResponseDoesNotContainJwksUriThenThrowsIllegalArgumentException() - throws JsonMappingException, JsonProcessingException { + public void issuerWhenOidcFallbackResponseDoesNotContainJwksUriThenThrowsIllegalArgumentException() { prepareConfigurationResponseOidc(this.buildResponseWithMissingJwksUri()); // @formatter:off assertThatIllegalArgumentException() @@ -216,8 +212,7 @@ public class JwtDecodersTests { // gh-7512 @Test - public void issuerWhenOAuth2ResponseDoesNotContainJwksUriThenThrowsIllegalArgumentException() - throws JsonMappingException, JsonProcessingException { + public void issuerWhenOAuth2ResponseDoesNotContainJwksUriThenThrowsIllegalArgumentException() { prepareConfigurationResponseOAuth2(this.buildResponseWithMissingJwksUri()); // @formatter:off assertThatIllegalArgumentException() @@ -384,8 +379,8 @@ public class JwtDecodersTests { // @formatter:on } - public String buildResponseWithMissingJwksUri() throws JsonMappingException, JsonProcessingException { - ObjectMapper mapper = new ObjectMapper(); + public String buildResponseWithMissingJwksUri() { + JsonMapper mapper = new JsonMapper(); Map response = mapper.readValue(DEFAULT_RESPONSE_TEMPLATE, new TypeReference>() { }); diff --git a/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/ReactiveJwtDecoderProviderConfigurationUtilsTests.java b/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/ReactiveJwtDecoderProviderConfigurationUtilsTests.java index fbaccad175..228518ae51 100644 --- a/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/ReactiveJwtDecoderProviderConfigurationUtilsTests.java +++ b/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/ReactiveJwtDecoderProviderConfigurationUtilsTests.java @@ -21,10 +21,6 @@ import java.util.HashMap; import java.util.Map; import java.util.Optional; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.JsonMappingException; -import com.fasterxml.jackson.databind.ObjectMapper; import okhttp3.HttpUrl; import okhttp3.mockwebserver.Dispatcher; import okhttp3.mockwebserver.MockResponse; @@ -33,6 +29,8 @@ import okhttp3.mockwebserver.RecordedRequest; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import tools.jackson.core.type.TypeReference; +import tools.jackson.databind.json.JsonMapper; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; @@ -153,8 +151,7 @@ public class ReactiveJwtDecoderProviderConfigurationUtilsTests { // gh-7512 @Test - public void issuerWhenOidcFallbackResponseDoesNotContainJwksUriThenThrowsIllegalArgumentException() - throws JsonMappingException, JsonProcessingException { + public void issuerWhenOidcFallbackResponseDoesNotContainJwksUriThenThrowsIllegalArgumentException() { prepareConfigurationResponseOidc(this.buildResponseWithMissingJwksUri()); // @formatter:off assertThatIllegalArgumentException() @@ -165,8 +162,7 @@ public class ReactiveJwtDecoderProviderConfigurationUtilsTests { // gh-7512 @Test - public void issuerWhenOAuth2ResponseDoesNotContainJwksUriThenThrowsIllegalArgumentException() - throws JsonMappingException, JsonProcessingException { + public void issuerWhenOAuth2ResponseDoesNotContainJwksUriThenThrowsIllegalArgumentException() { prepareConfigurationResponseOAuth2(this.buildResponseWithMissingJwksUri()); // @formatter:off assertThatIllegalArgumentException() @@ -323,8 +319,8 @@ public class ReactiveJwtDecoderProviderConfigurationUtilsTests { // @formatter:on } - public String buildResponseWithMissingJwksUri() throws JsonMappingException, JsonProcessingException { - ObjectMapper mapper = new ObjectMapper(); + public String buildResponseWithMissingJwksUri() { + JsonMapper mapper = new JsonMapper(); Map response = mapper.readValue(DEFAULT_RESPONSE_TEMPLATE, new TypeReference>() { }); diff --git a/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/ReactiveJwtDecodersTests.java b/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/ReactiveJwtDecodersTests.java index c3384ef6f1..704014b0a3 100644 --- a/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/ReactiveJwtDecodersTests.java +++ b/oauth2/oauth2-jose/src/test/java/org/springframework/security/oauth2/jwt/ReactiveJwtDecodersTests.java @@ -21,10 +21,6 @@ import java.util.HashMap; import java.util.Map; import java.util.Optional; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.JsonMappingException; -import com.fasterxml.jackson.databind.ObjectMapper; import okhttp3.HttpUrl; import okhttp3.mockwebserver.Dispatcher; import okhttp3.mockwebserver.MockResponse; @@ -33,6 +29,8 @@ import okhttp3.mockwebserver.RecordedRequest; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import tools.jackson.core.type.TypeReference; +import tools.jackson.databind.json.JsonMapper; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; @@ -166,8 +164,7 @@ public class ReactiveJwtDecodersTests { // gh-7512 @Test - public void issuerWhenResponseDoesNotContainJwksUriThenThrowsIllegalArgumentException() - throws JsonMappingException, JsonProcessingException { + public void issuerWhenResponseDoesNotContainJwksUriThenThrowsIllegalArgumentException() { prepareConfigurationResponse(this.buildResponseWithMissingJwksUri()); // @formatter:off assertThatIllegalArgumentException() @@ -178,8 +175,7 @@ public class ReactiveJwtDecodersTests { // gh-7512 @Test - public void issuerWhenOidcFallbackResponseDoesNotContainJwksUriThenThrowsIllegalArgumentException() - throws JsonMappingException, JsonProcessingException { + public void issuerWhenOidcFallbackResponseDoesNotContainJwksUriThenThrowsIllegalArgumentException() { prepareConfigurationResponseOidc(this.buildResponseWithMissingJwksUri()); // @formatter:off assertThatIllegalArgumentException() @@ -190,8 +186,7 @@ public class ReactiveJwtDecodersTests { // gh-7512 @Test - public void issuerWhenOAuth2ResponseDoesNotContainJwksUriThenThrowsIllegalArgumentException() - throws JsonMappingException, JsonProcessingException { + public void issuerWhenOAuth2ResponseDoesNotContainJwksUriThenThrowsIllegalArgumentException() { prepareConfigurationResponseOAuth2(this.buildResponseWithMissingJwksUri()); // @formatter:off assertThatIllegalArgumentException() @@ -357,8 +352,8 @@ public class ReactiveJwtDecodersTests { // @formatter:on } - public String buildResponseWithMissingJwksUri() throws JsonMappingException, JsonProcessingException { - ObjectMapper mapper = new ObjectMapper(); + public String buildResponseWithMissingJwksUri() { + JsonMapper mapper = new JsonMapper(); Map response = mapper.readValue(DEFAULT_RESPONSE_TEMPLATE, new TypeReference>() { }); diff --git a/oauth2/oauth2-resource-server/spring-security-oauth2-resource-server.gradle b/oauth2/oauth2-resource-server/spring-security-oauth2-resource-server.gradle index fbed03dcb3..5a0fb73311 100644 --- a/oauth2/oauth2-resource-server/spring-security-oauth2-resource-server.gradle +++ b/oauth2/oauth2-resource-server/spring-security-oauth2-resource-server.gradle @@ -17,7 +17,7 @@ dependencies { testImplementation project(path : ':spring-security-core', configuration : 'tests') testImplementation project(path: ':spring-security-oauth2-jose', configuration: 'tests') testImplementation 'com.squareup.okhttp3:mockwebserver' - testImplementation 'com.fasterxml.jackson.core:jackson-databind' + testImplementation 'tools.jackson.core:jackson-databind' testImplementation 'io.projectreactor.netty:reactor-netty' testImplementation 'io.projectreactor:reactor-test' testImplementation "org.assertj:assertj-core" diff --git a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/OAuth2ProtectedResourceMetadataFilter.java b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/OAuth2ProtectedResourceMetadataFilter.java index 66948bccf6..df74af6a7b 100644 --- a/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/OAuth2ProtectedResourceMetadataFilter.java +++ b/oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/OAuth2ProtectedResourceMetadataFilter.java @@ -156,6 +156,7 @@ public final class OAuth2ProtectedResourceMetadataFilter extends OncePerRequestF private HttpMessageConverters() { } + @SuppressWarnings("removal") private static GenericHttpMessageConverter getJsonMessageConverter() { if (jackson2Present) { return new MappingJackson2HttpMessageConverter(); diff --git a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/introspection/SpringReactiveOpaqueTokenIntrospectorTests.java b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/introspection/SpringReactiveOpaqueTokenIntrospectorTests.java index a5a2d2c050..5d9b2f7223 100644 --- a/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/introspection/SpringReactiveOpaqueTokenIntrospectorTests.java +++ b/oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/introspection/SpringReactiveOpaqueTokenIntrospectorTests.java @@ -26,13 +26,13 @@ import java.util.Map; import java.util.Optional; import java.util.function.Function; -import com.fasterxml.jackson.databind.ObjectMapper; import okhttp3.mockwebserver.Dispatcher; import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.MockWebServer; import okhttp3.mockwebserver.RecordedRequest; import org.junit.jupiter.api.Test; import reactor.core.publisher.Mono; +import tools.jackson.databind.json.JsonMapper; import org.springframework.core.ParameterizedTypeReference; import org.springframework.core.convert.converter.Converter; @@ -104,7 +104,7 @@ public class SpringReactiveOpaqueTokenIntrospectorTests { + " }"; // @formatter:on - private final ObjectMapper mapper = new ObjectMapper(); + private final JsonMapper mapper = new JsonMapper(); @Test public void authenticateWhenActiveTokenThenOk() throws Exception { diff --git a/saml2/saml2-service-provider/spring-security-saml2-service-provider.gradle b/saml2/saml2-service-provider/spring-security-saml2-service-provider.gradle index f1b68b259b..140f8a91e7 100644 --- a/saml2/saml2-service-provider/spring-security-saml2-service-provider.gradle +++ b/saml2/saml2-service-provider/spring-security-saml2-service-provider.gradle @@ -94,6 +94,7 @@ dependencies { optional 'com.fasterxml.jackson.core:jackson-databind' optional 'org.springframework:spring-jdbc' + optional 'tools.jackson.core:jackson-databind' testImplementation project(path: ':spring-security-web', configuration: 'tests') testImplementation 'com.squareup.okhttp3:mockwebserver' diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/jackson/DefaultSaml2AuthenticatedPrincipalMixin.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/jackson/DefaultSaml2AuthenticatedPrincipalMixin.java new file mode 100644 index 0000000000..cde3d898e0 --- /dev/null +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/jackson/DefaultSaml2AuthenticatedPrincipalMixin.java @@ -0,0 +1,52 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.saml2.jackson; + +import java.util.List; +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +import org.springframework.security.jackson.SecurityJacksonModules; +import org.springframework.security.saml2.provider.service.authentication.DefaultSaml2AuthenticatedPrincipal; + +/** + * Jackson Mixin class helps in serialize/deserialize + * {@link DefaultSaml2AuthenticatedPrincipal}. + * + * @author Sebastien Deleuze + * @author Ulrich Grave + * @since 7.0 + * @see Saml2JacksonModule + * @see SecurityJacksonModules + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) +@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE) +class DefaultSaml2AuthenticatedPrincipalMixin { + + @JsonProperty("registrationId") + String registrationId; + + DefaultSaml2AuthenticatedPrincipalMixin(@JsonProperty("name") String name, + @JsonProperty("attributes") Map> attributes, + @JsonProperty("sessionIndexes") List sessionIndexes) { + + } + +} diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/jackson/Saml2AssertionAuthenticationMixin.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/jackson/Saml2AssertionAuthenticationMixin.java new file mode 100644 index 0000000000..8d1a71ad2d --- /dev/null +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/jackson/Saml2AssertionAuthenticationMixin.java @@ -0,0 +1,53 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.saml2.jackson; + +import java.util.Collection; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.jackson.SecurityJacksonModules; +import org.springframework.security.saml2.provider.service.authentication.Saml2AssertionAuthentication; +import org.springframework.security.saml2.provider.service.authentication.Saml2ResponseAssertionAccessor; + +/** + * Jackson Mixin class helps in serialize/deserialize + * {@link Saml2AssertionAuthentication}. + * + * @author Sebastien Deleuze + * @author Josh Cummings + * @since 7.0 + * @see Saml2JacksonModule + * @see SecurityJacksonModules + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) +@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE, + isGetterVisibility = JsonAutoDetect.Visibility.NONE) +class Saml2AssertionAuthenticationMixin { + + @JsonCreator + Saml2AssertionAuthenticationMixin(@JsonProperty("principal") Object principal, + @JsonProperty("assertion") Saml2ResponseAssertionAccessor assertion, + @JsonProperty("authorities") Collection authorities, + @JsonProperty("relyingPartyRegistrationId") String relyingPartyRegistrationId) { + } + +} diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/jackson/Saml2AuthenticationExceptionMixin.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/jackson/Saml2AuthenticationExceptionMixin.java new file mode 100644 index 0000000000..1293a2d30d --- /dev/null +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/jackson/Saml2AuthenticationExceptionMixin.java @@ -0,0 +1,54 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.saml2.jackson; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +import org.springframework.security.saml2.core.Saml2Error; +import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationException; + +/** + * This mixin class is used to serialize/deserialize {@link Saml2AuthenticationException}. + * + * @author Sebastien Deleuze + * @author Ulrich Grave + * @since 7.0 + * @see Saml2AuthenticationException + * @see Saml2JacksonModule + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) +@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.NONE, getterVisibility = JsonAutoDetect.Visibility.NONE, + isGetterVisibility = JsonAutoDetect.Visibility.NONE) +@JsonIgnoreProperties({ "cause", "stackTrace", "suppressedExceptions" }) +abstract class Saml2AuthenticationExceptionMixin { + + @JsonProperty("error") + abstract Saml2Error getSaml2Error(); + + @JsonProperty("detailMessage") + abstract String getMessage(); + + @JsonCreator + Saml2AuthenticationExceptionMixin(@JsonProperty("error") Saml2Error error, + @JsonProperty("detailMessage") String message) { + } + +} diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/jackson/Saml2AuthenticationMixin.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/jackson/Saml2AuthenticationMixin.java new file mode 100644 index 0000000000..39cd0b4638 --- /dev/null +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/jackson/Saml2AuthenticationMixin.java @@ -0,0 +1,52 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.saml2.jackson; + +import java.util.Collection; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +import org.springframework.security.core.AuthenticatedPrincipal; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.jackson.SecurityJacksonModules; +import org.springframework.security.saml2.provider.service.authentication.Saml2Authentication; + +/** + * Jackson Mixin class helps in serialize/deserialize {@link Saml2Authentication}. + * + * @author Sebastien Deleuze + * @since 7.0 + * @see Saml2JacksonModule + * @see SecurityJacksonModules + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) +@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE, + isGetterVisibility = JsonAutoDetect.Visibility.NONE) +@JsonIgnoreProperties({ "authenticated" }) +class Saml2AuthenticationMixin { + + @JsonCreator + Saml2AuthenticationMixin(@JsonProperty("principal") AuthenticatedPrincipal principal, + @JsonProperty("saml2Response") String saml2Response, + @JsonProperty("authorities") Collection authorities) { + } + +} diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/jackson/Saml2ErrorMixin.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/jackson/Saml2ErrorMixin.java new file mode 100644 index 0000000000..c252cc968e --- /dev/null +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/jackson/Saml2ErrorMixin.java @@ -0,0 +1,44 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.saml2.jackson; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +import org.springframework.security.saml2.core.Saml2Error; + +/** + * This mixin class is used to serialize/deserialize {@link Saml2Error}. + * + * @author Sebastien Deleuze + * @author Ulrich Grave + * @since 7.0 + * @see Saml2Error + * @see Saml2JacksonModule + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) +@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE, + isGetterVisibility = JsonAutoDetect.Visibility.NONE) +class Saml2ErrorMixin { + + @JsonCreator + Saml2ErrorMixin(@JsonProperty("errorCode") String errorCode, @JsonProperty("description") String description) { + } + +} diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/jackson/Saml2JacksonModule.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/jackson/Saml2JacksonModule.java new file mode 100644 index 0000000000..c58536b0c5 --- /dev/null +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/jackson/Saml2JacksonModule.java @@ -0,0 +1,91 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.saml2.jackson; + +import tools.jackson.core.Version; +import tools.jackson.databind.jsontype.BasicPolymorphicTypeValidator; + +import org.springframework.security.jackson.SecurityJacksonModule; +import org.springframework.security.jackson.SecurityJacksonModules; +import org.springframework.security.saml2.core.Saml2Error; +import org.springframework.security.saml2.provider.service.authentication.DefaultSaml2AuthenticatedPrincipal; +import org.springframework.security.saml2.provider.service.authentication.Saml2AssertionAuthentication; +import org.springframework.security.saml2.provider.service.authentication.Saml2Authentication; +import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationException; +import org.springframework.security.saml2.provider.service.authentication.Saml2PostAuthenticationRequest; +import org.springframework.security.saml2.provider.service.authentication.Saml2RedirectAuthenticationRequest; +import org.springframework.security.saml2.provider.service.authentication.Saml2ResponseAssertion; +import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutRequest; + +/** + * Jackson module for saml2-service-provider. This module register + * {@link Saml2AuthenticationMixin}, {@link Saml2AssertionAuthenticationMixin}, + * {@link SimpleSaml2ResponseAssertionAccessorMixin}, + * {@link DefaultSaml2AuthenticatedPrincipalMixin}, {@link Saml2LogoutRequestMixin}, + * {@link Saml2RedirectAuthenticationRequestMixin}, + * {@link Saml2PostAuthenticationRequestMixin}, {@link Saml2ErrorMixin} and + * {@link Saml2AuthenticationExceptionMixin}. + * + *

+ * The recommended way to configure it is to use {@link SecurityJacksonModules} in order + * to enable properly automatic inclusion of type information with related validation. + * + *

+ *     ClassLoader loader = getClass().getClassLoader();
+ *     JsonMapper mapper = JsonMapper.builder()
+ * 				.addModules(SecurityJacksonModules.getModules(loader))
+ * 				.build();
+ * 
+ * + * @author Sebastien Deleuze + * @since 7.0 + * @see SecurityJacksonModules + */ +@SuppressWarnings("serial") +public class Saml2JacksonModule extends SecurityJacksonModule { + + public Saml2JacksonModule() { + super(Saml2JacksonModule.class.getName(), new Version(1, 0, 0, null, null, null)); + } + + @Override + public void configurePolymorphicTypeValidator(BasicPolymorphicTypeValidator.Builder builder) { + builder.allowIfSubType(Saml2ResponseAssertion.class) + .allowIfSubType(DefaultSaml2AuthenticatedPrincipal.class) + .allowIfSubType(Saml2PostAuthenticationRequest.class) + .allowIfSubType(Saml2LogoutRequest.class) + .allowIfSubType(Saml2RedirectAuthenticationRequest.class) + .allowIfSubType(Saml2AuthenticationException.class) + .allowIfSubType(Saml2Error.class) + .allowIfSubType(Saml2AssertionAuthentication.class) + .allowIfSubType(Saml2Authentication.class); + } + + @Override + public void setupModule(SetupContext context) { + context.setMixIn(Saml2Authentication.class, Saml2AuthenticationMixin.class); + context.setMixIn(Saml2AssertionAuthentication.class, Saml2AssertionAuthenticationMixin.class); + context.setMixIn(Saml2ResponseAssertion.class, SimpleSaml2ResponseAssertionAccessorMixin.class); + context.setMixIn(DefaultSaml2AuthenticatedPrincipal.class, DefaultSaml2AuthenticatedPrincipalMixin.class); + context.setMixIn(Saml2LogoutRequest.class, Saml2LogoutRequestMixin.class); + context.setMixIn(Saml2RedirectAuthenticationRequest.class, Saml2RedirectAuthenticationRequestMixin.class); + context.setMixIn(Saml2PostAuthenticationRequest.class, Saml2PostAuthenticationRequestMixin.class); + context.setMixIn(Saml2Error.class, Saml2ErrorMixin.class); + context.setMixIn(Saml2AuthenticationException.class, Saml2AuthenticationExceptionMixin.class); + } + +} diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/jackson/Saml2LogoutRequestMixin.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/jackson/Saml2LogoutRequestMixin.java new file mode 100644 index 0000000000..48af334db2 --- /dev/null +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/jackson/Saml2LogoutRequestMixin.java @@ -0,0 +1,55 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.saml2.jackson; + +import java.util.Map; +import java.util.function.Function; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +import org.springframework.security.jackson.SecurityJacksonModules; +import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutRequest; +import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding; + +/** + * Jackson Mixin class helps in serialize/deserialize {@link Saml2LogoutRequest}. + * + * @author Sebastien Deleuze + * @author Ulrich Grave + * @since 7.0 + * @see Saml2JacksonModule + * @see SecurityJacksonModules + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) +@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE) +class Saml2LogoutRequestMixin { + + @JsonIgnore + Function, String> encoder; + + @JsonCreator + Saml2LogoutRequestMixin(@JsonProperty("location") String location, + @JsonProperty("binding") Saml2MessageBinding binding, + @JsonProperty("parameters") Map parameters, @JsonProperty("id") String id, + @JsonProperty("relyingPartyRegistrationId") String relyingPartyRegistrationId) { + } + +} diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/jackson/Saml2PostAuthenticationRequestMixin.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/jackson/Saml2PostAuthenticationRequestMixin.java new file mode 100644 index 0000000000..ab3afe6e67 --- /dev/null +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/jackson/Saml2PostAuthenticationRequestMixin.java @@ -0,0 +1,49 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.saml2.jackson; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +import org.springframework.security.jackson.SecurityJacksonModules; +import org.springframework.security.saml2.provider.service.authentication.Saml2PostAuthenticationRequest; + +/** + * Jackson Mixin class helps in serialize/deserialize + * {@link Saml2PostAuthenticationRequest}. + * + * @author Sebastien Deleuze + * @author Ulrich Grave + * @since 7.0 + * @see Saml2JacksonModule + * @see SecurityJacksonModules + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) +@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE) +class Saml2PostAuthenticationRequestMixin { + + @JsonCreator + Saml2PostAuthenticationRequestMixin(@JsonProperty("samlRequest") String samlRequest, + @JsonProperty("relayState") String relayState, + @JsonProperty("authenticationRequestUri") String authenticationRequestUri, + @JsonProperty("relyingPartyRegistrationId") String relyingPartyRegistrationId, + @JsonProperty("id") String id) { + } + +} diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/jackson/Saml2RedirectAuthenticationRequestMixin.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/jackson/Saml2RedirectAuthenticationRequestMixin.java new file mode 100644 index 0000000000..fc17707741 --- /dev/null +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/jackson/Saml2RedirectAuthenticationRequestMixin.java @@ -0,0 +1,50 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.saml2.jackson; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +import org.springframework.security.jackson.SecurityJacksonModules; +import org.springframework.security.saml2.provider.service.authentication.Saml2RedirectAuthenticationRequest; + +/** + * Jackson Mixin class helps in serialize/deserialize + * {@link Saml2RedirectAuthenticationRequest}. + * + * @author Sebastien Deleuze + * @author Ulrich Grave + * @since 7.0 + * @see Saml2JacksonModule + * @see SecurityJacksonModules + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) +@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE) +class Saml2RedirectAuthenticationRequestMixin { + + @JsonCreator + Saml2RedirectAuthenticationRequestMixin(@JsonProperty("samlRequest") String samlRequest, + @JsonProperty("sigAlg") String sigAlg, @JsonProperty("signature") String signature, + @JsonProperty("relayState") String relayState, + @JsonProperty("authenticationRequestUri") String authenticationRequestUri, + @JsonProperty("relyingPartyRegistrationId") String relyingPartyRegistrationId, + @JsonProperty("id") String id) { + } + +} diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/jackson/SimpleSaml2ResponseAssertionAccessorMixin.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/jackson/SimpleSaml2ResponseAssertionAccessorMixin.java new file mode 100644 index 0000000000..949b06a2ec --- /dev/null +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/jackson/SimpleSaml2ResponseAssertionAccessorMixin.java @@ -0,0 +1,52 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.saml2.jackson; + +import java.util.List; +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +import org.springframework.security.jackson.SecurityJacksonModules; +import org.springframework.security.saml2.provider.service.authentication.Saml2ResponseAssertion; + +/** + * Jackson Mixin class helps in serialize/deserialize {@link Saml2ResponseAssertion}. + * + * @author Sebastien Deleuze + * @author Josh Cummings + * @since 7.0 + * @see Saml2JacksonModule + * @see SecurityJacksonModules + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) +@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE, + isGetterVisibility = JsonAutoDetect.Visibility.NONE) +@JsonIgnoreProperties({ "authenticated" }) +class SimpleSaml2ResponseAssertionAccessorMixin { + + @JsonCreator + SimpleSaml2ResponseAssertionAccessorMixin(@JsonProperty("responseValue") String responseValue, + @JsonProperty("nameId") String nameId, @JsonProperty("sessionIndexes") List sessionIndexes, + @JsonProperty("attributes") Map> attributes) { + } + +} diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/jackson/package-info.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/jackson/package-info.java new file mode 100644 index 0000000000..061f69b005 --- /dev/null +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/jackson/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Jackson 3+ serialization support for SAML2. + */ +package org.springframework.security.saml2.jackson; diff --git a/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/jackson2/package-info.java b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/jackson2/package-info.java new file mode 100644 index 0000000000..eef3717f17 --- /dev/null +++ b/saml2/saml2-service-provider/src/main/java/org/springframework/security/saml2/jackson2/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Jackson 2 serialization support for SAML2. + */ +package org.springframework.security.saml2.jackson2; diff --git a/saml2/saml2-service-provider/src/opensaml5Test/java/org/springframework/security/saml2/provider/service/authentication/OpenSaml5AuthenticationProviderTests.java b/saml2/saml2-service-provider/src/opensaml5Test/java/org/springframework/security/saml2/provider/service/authentication/OpenSaml5AuthenticationProviderTests.java index 35510564ca..01eeafbe16 100644 --- a/saml2/saml2-service-provider/src/opensaml5Test/java/org/springframework/security/saml2/provider/service/authentication/OpenSaml5AuthenticationProviderTests.java +++ b/saml2/saml2-service-provider/src/opensaml5Test/java/org/springframework/security/saml2/provider/service/authentication/OpenSaml5AuthenticationProviderTests.java @@ -32,7 +32,6 @@ import java.util.function.Consumer; import javax.xml.namespace.QName; -import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.Test; import org.opensaml.core.xml.XMLObject; import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport; @@ -69,6 +68,7 @@ import org.opensaml.saml.saml2.core.impl.StatusBuilder; import org.opensaml.saml.saml2.core.impl.StatusCodeBuilder; import org.opensaml.xmlsec.encryption.impl.EncryptedDataBuilder; import org.opensaml.xmlsec.signature.support.SignatureConstants; +import tools.jackson.databind.json.JsonMapper; import org.springframework.core.convert.converter.Converter; import org.springframework.security.authentication.SecurityAssertions; @@ -76,7 +76,7 @@ import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.core.authority.FactorGrantedAuthority; -import org.springframework.security.jackson2.SecurityJackson2Modules; +import org.springframework.security.jackson.SecurityJacksonModules; import org.springframework.security.saml2.core.Saml2Error; import org.springframework.security.saml2.core.Saml2ErrorCodes; import org.springframework.security.saml2.core.Saml2ResponseValidatorResult; @@ -342,9 +342,8 @@ public class OpenSaml5AuthenticationProviderTests { // gh-11785 @Test public void deserializeWhenAssertionContainsAttributesThenWorks() throws Exception { - ObjectMapper mapper = new ObjectMapper(); ClassLoader loader = getClass().getClassLoader(); - mapper.registerModules(SecurityJackson2Modules.getModules(loader)); + JsonMapper mapper = JsonMapper.builder().addModules(SecurityJacksonModules.getModules(loader)).build(); Response response = response(); Assertion assertion = assertion(); List attributes = TestOpenSamlObjects.attributeStatements(); diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/jackson/DefaultSaml2AuthenticatedPrincipalMixinTests.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/jackson/DefaultSaml2AuthenticatedPrincipalMixinTests.java new file mode 100644 index 0000000000..39c64c0962 --- /dev/null +++ b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/jackson/DefaultSaml2AuthenticatedPrincipalMixinTests.java @@ -0,0 +1,105 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.saml2.jackson; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; +import tools.jackson.databind.json.JsonMapper; + +import org.springframework.security.jackson.SecurityJacksonModules; +import org.springframework.security.saml2.provider.service.authentication.DefaultSaml2AuthenticatedPrincipal; + +import static org.assertj.core.api.Assertions.assertThat; + +@SuppressWarnings("removal") +class DefaultSaml2AuthenticatedPrincipalMixinTests { + + private JsonMapper mapper; + + @BeforeEach + void setUp() { + ClassLoader loader = getClass().getClassLoader(); + this.mapper = JsonMapper.builder().addModules(SecurityJacksonModules.getModules(loader)).build(); + } + + @Test + void shouldSerialize() throws Exception { + DefaultSaml2AuthenticatedPrincipal principal = TestSaml2JsonPayloads.createDefaultPrincipal(); + + String principalJson = this.mapper.writeValueAsString(principal); + + JSONAssert.assertEquals(TestSaml2JsonPayloads.DEFAULT_AUTHENTICATED_PRINCIPAL_JSON, principalJson, true); + } + + @Test + void shouldSerializeWithoutRegistrationId() throws Exception { + DefaultSaml2AuthenticatedPrincipal principal = new DefaultSaml2AuthenticatedPrincipal( + TestSaml2JsonPayloads.PRINCIPAL_NAME, TestSaml2JsonPayloads.ATTRIBUTES, + TestSaml2JsonPayloads.SESSION_INDEXES); + + String principalJson = this.mapper.writeValueAsString(principal); + + JSONAssert.assertEquals(principalWithoutRegId(), principalJson, true); + } + + @Test + void shouldSerializeWithoutIndices() throws Exception { + DefaultSaml2AuthenticatedPrincipal principal = new DefaultSaml2AuthenticatedPrincipal( + TestSaml2JsonPayloads.PRINCIPAL_NAME, TestSaml2JsonPayloads.ATTRIBUTES); + principal.setRelyingPartyRegistrationId(TestSaml2JsonPayloads.REG_ID); + + String principalJson = this.mapper.writeValueAsString(principal); + + JSONAssert.assertEquals(principalWithoutIndices(), principalJson, true); + } + + @Test + void shouldDeserialize() throws Exception { + DefaultSaml2AuthenticatedPrincipal principal = this.mapper.readValue( + TestSaml2JsonPayloads.DEFAULT_AUTHENTICATED_PRINCIPAL_JSON, DefaultSaml2AuthenticatedPrincipal.class); + + assertThat(principal).isNotNull(); + assertThat(principal.getName()).isEqualTo(TestSaml2JsonPayloads.PRINCIPAL_NAME); + assertThat(principal.getRelyingPartyRegistrationId()).isEqualTo(TestSaml2JsonPayloads.REG_ID); + assertThat(principal.getAttributes()).isEqualTo(TestSaml2JsonPayloads.ATTRIBUTES); + assertThat(principal.getSessionIndexes()).isEqualTo(TestSaml2JsonPayloads.SESSION_INDEXES); + } + + @Test + void shouldDeserializeWithoutRegistrationId() throws Exception { + DefaultSaml2AuthenticatedPrincipal principal = this.mapper.readValue(principalWithoutRegId(), + DefaultSaml2AuthenticatedPrincipal.class); + + assertThat(principal).isNotNull(); + assertThat(principal.getName()).isEqualTo(TestSaml2JsonPayloads.PRINCIPAL_NAME); + assertThat(principal.getRelyingPartyRegistrationId()).isNull(); + assertThat(principal.getAttributes()).isEqualTo(TestSaml2JsonPayloads.ATTRIBUTES); + assertThat(principal.getSessionIndexes()).isEqualTo(TestSaml2JsonPayloads.SESSION_INDEXES); + } + + private static String principalWithoutRegId() { + return TestSaml2JsonPayloads.DEFAULT_AUTHENTICATED_PRINCIPAL_JSON.replace(TestSaml2JsonPayloads.REG_ID_JSON, + "null"); + } + + private static String principalWithoutIndices() { + return TestSaml2JsonPayloads.DEFAULT_AUTHENTICATED_PRINCIPAL_JSON + .replace(TestSaml2JsonPayloads.SESSION_INDEXES_JSON, "[\"java.util.Collections$EmptyList\", []]"); + } + +} diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/jackson/Saml2AuthenticationExceptionMixinTests.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/jackson/Saml2AuthenticationExceptionMixinTests.java new file mode 100644 index 0000000000..9f64913e49 --- /dev/null +++ b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/jackson/Saml2AuthenticationExceptionMixinTests.java @@ -0,0 +1,61 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.saml2.jackson; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; +import tools.jackson.databind.json.JsonMapper; + +import org.springframework.security.jackson.SecurityJacksonModules; +import org.springframework.security.saml2.core.Saml2Error; +import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationException; + +import static org.assertj.core.api.Assertions.assertThat; + +@SuppressWarnings("removal") +class Saml2AuthenticationExceptionMixinTests { + + private JsonMapper mapper; + + @BeforeEach + void setUp() { + ClassLoader loader = getClass().getClassLoader(); + this.mapper = JsonMapper.builder().addModules(SecurityJacksonModules.getModules(loader)).build(); + } + + @Test + void shouldSerialize() throws Exception { + Saml2AuthenticationException exception = TestSaml2JsonPayloads.createDefaultSaml2AuthenticationException(); + + String exceptionJson = this.mapper.writeValueAsString(exception); + + JSONAssert.assertEquals(TestSaml2JsonPayloads.DEFAULT_SAML_AUTH_EXCEPTION_JSON, exceptionJson, true); + } + + @Test + void shouldDeserialize() throws Exception { + Saml2AuthenticationException exception = this.mapper + .readValue(TestSaml2JsonPayloads.DEFAULT_SAML_AUTH_EXCEPTION_JSON, Saml2AuthenticationException.class); + + assertThat(exception).isNotNull(); + assertThat(exception.getMessage()).isEqualTo("exceptionMessage"); + assertThat(exception.getSaml2Error()).extracting(Saml2Error::getErrorCode, Saml2Error::getDescription) + .contains("errorCode", "errorDescription"); + } + +} diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/jackson/Saml2AuthenticationMixinTests.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/jackson/Saml2AuthenticationMixinTests.java new file mode 100644 index 0000000000..eee63b4d0a --- /dev/null +++ b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/jackson/Saml2AuthenticationMixinTests.java @@ -0,0 +1,64 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.saml2.jackson; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; +import tools.jackson.databind.json.JsonMapper; + +import org.springframework.security.jackson.SecurityJacksonModules; +import org.springframework.security.saml2.provider.service.authentication.Saml2Authentication; + +import static org.assertj.core.api.Assertions.assertThat; + +@SuppressWarnings("removal") +class Saml2AuthenticationMixinTests { + + private JsonMapper mapper; + + @BeforeEach + void setUp() { + ClassLoader loader = getClass().getClassLoader(); + this.mapper = JsonMapper.builder().addModules(SecurityJacksonModules.getModules(loader)).build(); + } + + @Test + void shouldSerialize() throws Exception { + Saml2Authentication authentication = TestSaml2JsonPayloads.createDefaultAuthentication(); + + String authenticationJson = this.mapper.writeValueAsString(authentication); + + JSONAssert.assertEquals(TestSaml2JsonPayloads.DEFAULT_SAML2AUTHENTICATION_JSON, authenticationJson, true); + } + + @Test + void shouldDeserialize() throws Exception { + Saml2Authentication authentication = this.mapper + .readValue(TestSaml2JsonPayloads.DEFAULT_SAML2AUTHENTICATION_JSON, Saml2Authentication.class); + + assertThat(authentication).isNotNull(); + assertThat(authentication.getDetails()).isEqualTo(TestSaml2JsonPayloads.DETAILS); + assertThat(authentication.getCredentials()).isEqualTo(TestSaml2JsonPayloads.SAML_RESPONSE); + assertThat(authentication.getSaml2Response()).isEqualTo(TestSaml2JsonPayloads.SAML_RESPONSE); + assertThat(authentication.getAuthorities()).isEqualTo(TestSaml2JsonPayloads.AUTHORITIES); + assertThat(authentication.getPrincipal()).usingRecursiveComparison() + .isEqualTo(TestSaml2JsonPayloads.createDefaultPrincipal()); + assertThat(authentication.getDetails()).usingRecursiveComparison().isEqualTo(TestSaml2JsonPayloads.DETAILS); + } + +} diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/jackson/Saml2LogoutRequestMixinTests.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/jackson/Saml2LogoutRequestMixinTests.java new file mode 100644 index 0000000000..d71e1269b7 --- /dev/null +++ b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/jackson/Saml2LogoutRequestMixinTests.java @@ -0,0 +1,90 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.saml2.jackson; + +import java.util.HashMap; +import java.util.Map; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; +import tools.jackson.core.JacksonException; +import tools.jackson.databind.DeserializationFeature; +import tools.jackson.databind.json.JsonMapper; + +import org.springframework.security.jackson.SecurityJacksonModules; +import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutRequest; +import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding; + +import static org.assertj.core.api.Assertions.assertThat; + +@SuppressWarnings("removal") +class Saml2LogoutRequestMixinTests { + + private JsonMapper mapper; + + @BeforeEach + void setUp() { + ClassLoader loader = getClass().getClassLoader(); + this.mapper = JsonMapper.builder().addModules(SecurityJacksonModules.getModules(loader)).build(); + } + + @Test + void shouldSerialize() throws Exception { + Saml2LogoutRequest request = TestSaml2JsonPayloads.createDefaultSaml2LogoutRequest(); + + String requestJson = this.mapper.writeValueAsString(request); + + JSONAssert.assertEquals(TestSaml2JsonPayloads.DEFAULT_LOGOUT_REQUEST_JSON, requestJson, true); + } + + @Test + void shouldDeserialize() { + deserializeAndAssertRequest(); + } + + // gh-12539 + @Test + void shouldDeserializeWhenFailOnMissingCreatorPropertiesEnabled() { + // Jackson will use reflection to initialize the binding property if this is not + // enabled + JsonMapper customizedMapper = this.mapper.rebuild() + .enable(DeserializationFeature.FAIL_ON_MISSING_CREATOR_PROPERTIES) + .build(); + deserializeAndAssertRequest(); + } + + private void deserializeAndAssertRequest() throws JacksonException { + Saml2LogoutRequest logoutRequest = this.mapper.readValue(TestSaml2JsonPayloads.DEFAULT_LOGOUT_REQUEST_JSON, + Saml2LogoutRequest.class); + + assertThat(logoutRequest).isNotNull(); + assertThat(logoutRequest.getId()).isEqualTo(TestSaml2JsonPayloads.ID); + assertThat(logoutRequest.getRelyingPartyRegistrationId()) + .isEqualTo(TestSaml2JsonPayloads.RELYINGPARTY_REGISTRATION_ID); + assertThat(logoutRequest.getSamlRequest()).isEqualTo(TestSaml2JsonPayloads.SAML_REQUEST); + assertThat(logoutRequest.getRelayState()).isEqualTo(TestSaml2JsonPayloads.RELAY_STATE); + assertThat(logoutRequest.getLocation()).isEqualTo(TestSaml2JsonPayloads.LOCATION); + assertThat(logoutRequest.getBinding()).isEqualTo(Saml2MessageBinding.REDIRECT); + Map expectedParams = new HashMap<>(); + expectedParams.put("SAMLRequest", TestSaml2JsonPayloads.SAML_REQUEST); + expectedParams.put("RelayState", TestSaml2JsonPayloads.RELAY_STATE); + expectedParams.put("AdditionalParam", TestSaml2JsonPayloads.ADDITIONAL_PARAM); + assertThat(logoutRequest.getParameters()).containsAllEntriesOf(expectedParams); + } + +} diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/jackson/Saml2PostAuthenticationRequestMixinTests.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/jackson/Saml2PostAuthenticationRequestMixinTests.java new file mode 100644 index 0000000000..673984566a --- /dev/null +++ b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/jackson/Saml2PostAuthenticationRequestMixinTests.java @@ -0,0 +1,97 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.saml2.jackson; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; +import tools.jackson.databind.json.JsonMapper; + +import org.springframework.security.jackson.SecurityJacksonModules; +import org.springframework.security.saml2.provider.service.authentication.Saml2PostAuthenticationRequest; + +import static org.assertj.core.api.Assertions.assertThat; + +@SuppressWarnings("removal") +class Saml2PostAuthenticationRequestMixinTests { + + private JsonMapper mapper; + + @BeforeEach + void setUp() { + ClassLoader loader = getClass().getClassLoader(); + this.mapper = JsonMapper.builder().addModules(SecurityJacksonModules.getModules(loader)).build(); + } + + @Test + void shouldSerialize() throws Exception { + Saml2PostAuthenticationRequest request = TestSaml2JsonPayloads.createDefaultSaml2PostAuthenticationRequest(); + + String requestJson = this.mapper.writeValueAsString(request); + + JSONAssert.assertEquals(TestSaml2JsonPayloads.DEFAULT_POST_AUTH_REQUEST_JSON, requestJson, true); + } + + @Test + void shouldDeserialize() { + Saml2PostAuthenticationRequest authRequest = this.mapper + .readValue(TestSaml2JsonPayloads.DEFAULT_POST_AUTH_REQUEST_JSON, Saml2PostAuthenticationRequest.class); + + assertThat(authRequest).isNotNull(); + assertThat(authRequest.getSamlRequest()).isEqualTo(TestSaml2JsonPayloads.SAML_REQUEST); + assertThat(authRequest.getRelayState()).isEqualTo(TestSaml2JsonPayloads.RELAY_STATE); + assertThat(authRequest.getAuthenticationRequestUri()) + .isEqualTo(TestSaml2JsonPayloads.AUTHENTICATION_REQUEST_URI); + assertThat(authRequest.getRelyingPartyRegistrationId()) + .isEqualTo(TestSaml2JsonPayloads.RELYINGPARTY_REGISTRATION_ID); + assertThat(authRequest.getId()).isEqualTo(TestSaml2JsonPayloads.ID); + } + + @Test + void shouldDeserializeWithNoRegistrationId() { + String json = TestSaml2JsonPayloads.DEFAULT_POST_AUTH_REQUEST_JSON.replace( + "\"relyingPartyRegistrationId\": \"" + TestSaml2JsonPayloads.RELYINGPARTY_REGISTRATION_ID + "\",", ""); + + Saml2PostAuthenticationRequest authRequest = this.mapper.readValue(json, Saml2PostAuthenticationRequest.class); + + assertThat(authRequest).isNotNull(); + assertThat(authRequest.getSamlRequest()).isEqualTo(TestSaml2JsonPayloads.SAML_REQUEST); + assertThat(authRequest.getRelayState()).isEqualTo(TestSaml2JsonPayloads.RELAY_STATE); + assertThat(authRequest.getAuthenticationRequestUri()) + .isEqualTo(TestSaml2JsonPayloads.AUTHENTICATION_REQUEST_URI); + assertThat(authRequest.getRelyingPartyRegistrationId()).isNull(); + assertThat(authRequest.getId()).isEqualTo(TestSaml2JsonPayloads.ID); + } + + @Test + void shouldDeserializeWithNoId() { + String json = TestSaml2JsonPayloads.DEFAULT_POST_AUTH_REQUEST_JSON + .replace(", \"id\": \"" + TestSaml2JsonPayloads.ID + "\"", ""); + + Saml2PostAuthenticationRequest authRequest = this.mapper.readValue(json, Saml2PostAuthenticationRequest.class); + + assertThat(authRequest).isNotNull(); + assertThat(authRequest.getSamlRequest()).isEqualTo(TestSaml2JsonPayloads.SAML_REQUEST); + assertThat(authRequest.getRelayState()).isEqualTo(TestSaml2JsonPayloads.RELAY_STATE); + assertThat(authRequest.getAuthenticationRequestUri()) + .isEqualTo(TestSaml2JsonPayloads.AUTHENTICATION_REQUEST_URI); + assertThat(authRequest.getRelyingPartyRegistrationId()) + .isEqualTo(TestSaml2JsonPayloads.RELYINGPARTY_REGISTRATION_ID); + assertThat(authRequest.getId()).isNull(); + } + +} diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/jackson/Saml2RedirectAuthenticationRequestMixinTests.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/jackson/Saml2RedirectAuthenticationRequestMixinTests.java new file mode 100644 index 0000000000..c61ad0b7c6 --- /dev/null +++ b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/jackson/Saml2RedirectAuthenticationRequestMixinTests.java @@ -0,0 +1,84 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.saml2.jackson; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; +import tools.jackson.databind.json.JsonMapper; + +import org.springframework.security.jackson.SecurityJacksonModules; +import org.springframework.security.saml2.provider.service.authentication.Saml2RedirectAuthenticationRequest; + +import static org.assertj.core.api.Assertions.assertThat; + +@SuppressWarnings("removal") +class Saml2RedirectAuthenticationRequestMixinTests { + + private JsonMapper mapper; + + @BeforeEach + void setUp() { + ClassLoader loader = getClass().getClassLoader(); + this.mapper = JsonMapper.builder().addModules(SecurityJacksonModules.getModules(loader)).build(); + } + + @Test + void shouldSerialize() throws Exception { + Saml2RedirectAuthenticationRequest request = TestSaml2JsonPayloads + .createDefaultSaml2RedirectAuthenticationRequest(); + + String requestJson = this.mapper.writeValueAsString(request); + + JSONAssert.assertEquals(TestSaml2JsonPayloads.DEFAULT_REDIRECT_AUTH_REQUEST_JSON, requestJson, true); + } + + @Test + void shouldDeserialize() throws Exception { + Saml2RedirectAuthenticationRequest authRequest = this.mapper.readValue( + TestSaml2JsonPayloads.DEFAULT_REDIRECT_AUTH_REQUEST_JSON, Saml2RedirectAuthenticationRequest.class); + + assertThat(authRequest).isNotNull(); + assertThat(authRequest.getSamlRequest()).isEqualTo(TestSaml2JsonPayloads.SAML_REQUEST); + assertThat(authRequest.getRelayState()).isEqualTo(TestSaml2JsonPayloads.RELAY_STATE); + assertThat(authRequest.getAuthenticationRequestUri()) + .isEqualTo(TestSaml2JsonPayloads.AUTHENTICATION_REQUEST_URI); + assertThat(authRequest.getSigAlg()).isEqualTo(TestSaml2JsonPayloads.SIG_ALG); + assertThat(authRequest.getSignature()).isEqualTo(TestSaml2JsonPayloads.SIGNATURE); + assertThat(authRequest.getRelyingPartyRegistrationId()) + .isEqualTo(TestSaml2JsonPayloads.RELYINGPARTY_REGISTRATION_ID); + } + + @Test + void shouldDeserializeWithNoRegistrationId() throws Exception { + String json = TestSaml2JsonPayloads.DEFAULT_REDIRECT_AUTH_REQUEST_JSON.replace( + "\"relyingPartyRegistrationId\": \"" + TestSaml2JsonPayloads.RELYINGPARTY_REGISTRATION_ID + "\",", ""); + + Saml2RedirectAuthenticationRequest authRequest = this.mapper.readValue(json, + Saml2RedirectAuthenticationRequest.class); + + assertThat(authRequest).isNotNull(); + assertThat(authRequest.getSamlRequest()).isEqualTo(TestSaml2JsonPayloads.SAML_REQUEST); + assertThat(authRequest.getRelayState()).isEqualTo(TestSaml2JsonPayloads.RELAY_STATE); + assertThat(authRequest.getAuthenticationRequestUri()) + .isEqualTo(TestSaml2JsonPayloads.AUTHENTICATION_REQUEST_URI); + assertThat(authRequest.getSigAlg()).isEqualTo(TestSaml2JsonPayloads.SIG_ALG); + assertThat(authRequest.getSignature()).isEqualTo(TestSaml2JsonPayloads.SIGNATURE); + assertThat(authRequest.getRelyingPartyRegistrationId()).isNull(); + } + +} diff --git a/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/jackson/TestSaml2JsonPayloads.java b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/jackson/TestSaml2JsonPayloads.java new file mode 100644 index 0000000000..40e7d90e6c --- /dev/null +++ b/saml2/saml2-service-provider/src/test/java/org/springframework/security/saml2/jackson/TestSaml2JsonPayloads.java @@ -0,0 +1,257 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.saml2.jackson; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.saml2.core.Saml2Error; +import org.springframework.security.saml2.provider.service.authentication.DefaultSaml2AuthenticatedPrincipal; +import org.springframework.security.saml2.provider.service.authentication.Saml2Authentication; +import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationException; +import org.springframework.security.saml2.provider.service.authentication.Saml2PostAuthenticationRequest; +import org.springframework.security.saml2.provider.service.authentication.Saml2RedirectAuthenticationRequest; +import org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutRequest; +import org.springframework.security.saml2.provider.service.registration.Saml2MessageBinding; +import org.springframework.security.saml2.provider.service.registration.TestRelyingPartyRegistrations; + +@SuppressWarnings("removal") +final class TestSaml2JsonPayloads { + + private TestSaml2JsonPayloads() { + } + + static final Map> ATTRIBUTES; + + static { + Map> tmpAttributes = new HashMap<>(); + tmpAttributes.put("name", Collections.singletonList("attr_name")); + tmpAttributes.put("email", Collections.singletonList("attr_email")); + tmpAttributes.put("listOf", Collections.unmodifiableList(Arrays.asList("Element1", "Element2", 4, true))); + ATTRIBUTES = Collections.unmodifiableMap(tmpAttributes); + } + + static final String REG_ID = "REG_ID_TEST"; + static final String REG_ID_JSON = "\"" + REG_ID + "\""; + + static final String SESSION_INDEXES_JSON = "[" + " \"java.util.Collections$UnmodifiableRandomAccessList\"," + + " [ \"Index 1\", \"Index 2\" ]" + "]"; + static final List SESSION_INDEXES = Collections.unmodifiableList(Arrays.asList("Index 1", "Index 2")); + + static final String PRINCIPAL_NAME = "principalName"; + + // @formatter:off + static final String DEFAULT_AUTHENTICATED_PRINCIPAL_JSON = "{" + + " \"@class\": \"org.springframework.security.saml2.provider.service.authentication.DefaultSaml2AuthenticatedPrincipal\"," + + " \"name\": \"" + PRINCIPAL_NAME + "\"," + + " \"attributes\": {" + + " \"@class\": \"java.util.Collections$UnmodifiableMap\"," + + " \"listOf\": [" + + " \"java.util.Collections$UnmodifiableRandomAccessList\"," + + " [ \"Element1\", \"Element2\", 4, true ]" + + " ]," + + " \"email\": [" + + " \"java.util.Collections$SingletonList\"," + + " [ \"attr_email\" ]" + + " ]," + + " \"name\": [" + + " \"java.util.Collections$SingletonList\"," + + " [ \"attr_name\" ]" + + " ]" + + " }," + + " \"sessionIndexes\": " + SESSION_INDEXES_JSON + "," + + " \"registrationId\": " + REG_ID_JSON + "" + + "}"; + // @formatter:on + + static DefaultSaml2AuthenticatedPrincipal createDefaultPrincipal() { + DefaultSaml2AuthenticatedPrincipal principal = new DefaultSaml2AuthenticatedPrincipal(PRINCIPAL_NAME, + ATTRIBUTES, SESSION_INDEXES); + principal.setRelyingPartyRegistrationId(REG_ID); + return principal; + } + + static final String SAML_REQUEST = "samlRequestValue"; + static final String RELAY_STATE = "relayStateValue"; + static final String AUTHENTICATION_REQUEST_URI = "authenticationRequestUriValue"; + static final String RELYINGPARTY_REGISTRATION_ID = "registrationIdValue"; + static final String SIG_ALG = "sigAlgValue"; + static final String SIGNATURE = "signatureValue"; + static final String ID = "idValue"; + + // @formatter:off + static final String DEFAULT_REDIRECT_AUTH_REQUEST_JSON = "{" + + " \"@class\": \"org.springframework.security.saml2.provider.service.authentication.Saml2RedirectAuthenticationRequest\"," + + " \"samlRequest\": \"" + SAML_REQUEST + "\"," + + " \"relayState\": \"" + RELAY_STATE + "\"," + + " \"authenticationRequestUri\": \"" + AUTHENTICATION_REQUEST_URI + "\"," + + " \"relyingPartyRegistrationId\": \"" + RELYINGPARTY_REGISTRATION_ID + "\"," + + " \"sigAlg\": \"" + SIG_ALG + "\"," + + " \"signature\": \"" + SIGNATURE + "\"," + + " \"id\": \"" + ID + "\"" + + "}"; + // @formatter:on + + // @formatter:off + static final String DEFAULT_POST_AUTH_REQUEST_JSON = "{" + + " \"@class\": \"org.springframework.security.saml2.provider.service.authentication.Saml2PostAuthenticationRequest\"," + + " \"samlRequest\": \"" + SAML_REQUEST + "\"," + + " \"relayState\": \"" + RELAY_STATE + "\"," + + " \"relyingPartyRegistrationId\": \"" + RELYINGPARTY_REGISTRATION_ID + "\"," + + " \"authenticationRequestUri\": \"" + AUTHENTICATION_REQUEST_URI + "\"," + + " \"id\": \"" + ID + "\"" + + "}"; + // @formatter:on + + static final String LOCATION = "locationValue"; + static final String BINDNG = "REDIRECT"; + static final String ADDITIONAL_PARAM = "additionalParamValue"; + + // @formatter:off + static final String DEFAULT_LOGOUT_REQUEST_JSON = "{" + + " \"@class\": \"org.springframework.security.saml2.provider.service.authentication.logout.Saml2LogoutRequest\"," + + " \"id\": \"" + ID + "\"," + + " \"location\": \"" + LOCATION + "\"," + + " \"binding\": \"" + BINDNG + "\"," + + " \"relyingPartyRegistrationId\": \"" + RELYINGPARTY_REGISTRATION_ID + "\"," + + " \"parameters\": { " + + " \"@class\": \"java.util.Collections$UnmodifiableMap\"," + + " \"SAMLRequest\": \"" + SAML_REQUEST + "\"," + + " \"RelayState\": \"" + RELAY_STATE + "\"," + + " \"AdditionalParam\": \"" + ADDITIONAL_PARAM + "\"" + + " }" + + "}"; + // @formatter:on + + static Saml2PostAuthenticationRequest createDefaultSaml2PostAuthenticationRequest() { + return Saml2PostAuthenticationRequest + .withRelyingPartyRegistration(TestRelyingPartyRegistrations.full() + .registrationId(RELYINGPARTY_REGISTRATION_ID) + .assertingPartyMetadata((party) -> party.singleSignOnServiceLocation(AUTHENTICATION_REQUEST_URI)) + .build()) + .samlRequest(SAML_REQUEST) + .relayState(RELAY_STATE) + .id(ID) + .build(); + } + + static Saml2RedirectAuthenticationRequest createDefaultSaml2RedirectAuthenticationRequest() { + return Saml2RedirectAuthenticationRequest + .withRelyingPartyRegistration(TestRelyingPartyRegistrations.full() + .registrationId(RELYINGPARTY_REGISTRATION_ID) + .assertingPartyMetadata((party) -> party.singleSignOnServiceLocation(AUTHENTICATION_REQUEST_URI)) + .build()) + .samlRequest(SAML_REQUEST) + .relayState(RELAY_STATE) + .sigAlg(SIG_ALG) + .signature(SIGNATURE) + .id(ID) + .build(); + } + + static Saml2LogoutRequest createDefaultSaml2LogoutRequest() { + return Saml2LogoutRequest + .withRelyingPartyRegistration(TestRelyingPartyRegistrations.full() + .registrationId(RELYINGPARTY_REGISTRATION_ID) + .assertingPartyMetadata((party) -> party.singleLogoutServiceLocation(LOCATION) + .singleLogoutServiceBinding(Saml2MessageBinding.REDIRECT)) + .build()) + .id(ID) + .samlRequest(SAML_REQUEST) + .relayState(RELAY_STATE) + .parameters((params) -> params.put("AdditionalParam", ADDITIONAL_PARAM)) + .build(); + } + + static final Collection AUTHORITIES = Collections + .unmodifiableList(Arrays.asList(new SimpleGrantedAuthority("Role1"), new SimpleGrantedAuthority("Role2"))); + + static final Object DETAILS = User.withUsername("username").password("empty").authorities("A", "B").build(); + static final String SAML_RESPONSE = "samlResponseValue"; + + // @formatter:off + static final String DEFAULT_SAML2AUTHENTICATION_JSON = "{" + + " \"@class\": \"org.springframework.security.saml2.provider.service.authentication.Saml2Authentication\"," + + " \"authorities\": [" + + " \"java.util.Collections$UnmodifiableRandomAccessList\"," + + " [" + + " {" + + " \"@class\": \"org.springframework.security.core.authority.SimpleGrantedAuthority\"," + + " \"authority\": \"Role1\"" + + " }," + + " {" + + " \"@class\": \"org.springframework.security.core.authority.SimpleGrantedAuthority\"," + + " \"authority\": \"Role2\"" + + " }" + + " ]" + + " ]," + + " \"details\": {" + + " \"@class\": \"org.springframework.security.core.userdetails.User\"," + + " \"password\": \"empty\"," + + " \"username\": \"username\"," + + " \"authorities\": [" + + " \"java.util.Collections$UnmodifiableSet\", [" + + " {" + + " \"@class\":\"org.springframework.security.core.authority.SimpleGrantedAuthority\"," + + " \"authority\":\"A\"" + + " }," + + " {" + + " \"@class\":\"org.springframework.security.core.authority.SimpleGrantedAuthority\"," + + " \"authority\":\"B\"" + + " }" + + " ]]," + + " \"accountNonExpired\": true," + + " \"accountNonLocked\": true," + + " \"credentialsNonExpired\": true," + + " \"enabled\": true" + + " }," + + " \"principal\": " + DEFAULT_AUTHENTICATED_PRINCIPAL_JSON + "," + + " \"saml2Response\": \"" + SAML_RESPONSE + "\"" + + "}"; + // @formatter:on + + static Saml2Authentication createDefaultAuthentication() { + DefaultSaml2AuthenticatedPrincipal principal = createDefaultPrincipal(); + Saml2Authentication authentication = new Saml2Authentication(principal, SAML_RESPONSE, AUTHORITIES); + authentication.setDetails(DETAILS); + return authentication; + } + + // @formatter:off + static final String DEFAULT_SAML_AUTH_EXCEPTION_JSON = "{" + + " \"@class\": \"org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticationException\"," + + " \"detailMessage\": \"exceptionMessage\"," + + " \"error\": {" + + " \"@class\": \"org.springframework.security.saml2.core.Saml2Error\"," + + " \"errorCode\": \"errorCode\"," + + " \"description\": \"errorDescription\"" + + " }" + + "}"; + // @formatter:on + + static Saml2AuthenticationException createDefaultSaml2AuthenticationException() { + return new Saml2AuthenticationException(new Saml2Error("errorCode", "errorDescription"), "exceptionMessage"); + } + +} diff --git a/web/spring-security-web.gradle b/web/spring-security-web.gradle index 87ce691e0c..40604b1a30 100644 --- a/web/spring-security-web.gradle +++ b/web/spring-security-web.gradle @@ -46,6 +46,7 @@ dependencies { optional 'org.springframework:spring-tx' optional 'org.springframework:spring-webflux' optional 'org.springframework:spring-webmvc' + optional 'tools.jackson.core:jackson-databind' optional libs.webauthn4j.core provided 'jakarta.servlet:jakarta.servlet-api' diff --git a/web/src/main/java/org/springframework/security/web/jackson/CookieDeserializer.java b/web/src/main/java/org/springframework/security/web/jackson/CookieDeserializer.java new file mode 100644 index 0000000000..53faf16d72 --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/jackson/CookieDeserializer.java @@ -0,0 +1,65 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.web.jackson; + +import jakarta.servlet.http.Cookie; +import tools.jackson.core.JacksonException; +import tools.jackson.core.JsonParser; +import tools.jackson.databind.DeserializationContext; +import tools.jackson.databind.JsonNode; +import tools.jackson.databind.ValueDeserializer; +import tools.jackson.databind.node.MissingNode; +import tools.jackson.databind.node.NullNode; + +/** + * Jackson deserializer for {@link Cookie}. This is needed because in most cases we don't + * set {@link Cookie#getDomain()} property. So when jackson deserialize that json + * {@link Cookie#setDomain(String)} throws {@link NullPointerException}. This is + * registered with {@link CookieMixin} but you can also use it with your own mixin. + * + * @author Sebastien Deleuze + * @author Jitendra Singh + * @since 7.0 + * @see CookieMixin + */ +class CookieDeserializer extends ValueDeserializer { + + @Override + public Cookie deserialize(JsonParser jp, DeserializationContext ctxt) throws JacksonException { + JsonNode jsonNode = ctxt.readTree(jp); + Cookie cookie = new Cookie(readJsonNode(jsonNode, "name").stringValue(), + readJsonNode(jsonNode, "value").stringValue()); + JsonNode domainNode = readJsonNode(jsonNode, "domain"); + cookie.setDomain((domainNode.isMissingNode()) ? null : domainNode.stringValue()); + cookie.setMaxAge(readJsonNode(jsonNode, "maxAge").asInt(-1)); + cookie.setSecure(readJsonNode(jsonNode, "secure").asBoolean()); + JsonNode pathNode = readJsonNode(jsonNode, "path"); + cookie.setPath((pathNode.isMissingNode()) ? null : pathNode.stringValue()); + JsonNode attributes = readJsonNode(jsonNode, "attributes"); + cookie.setHttpOnly(readJsonNode(attributes, "HttpOnly") != null); + return cookie; + } + + private JsonNode readJsonNode(JsonNode jsonNode, String field) { + return hasNonNullField(jsonNode, field) ? jsonNode.get(field) : MissingNode.getInstance(); + } + + private boolean hasNonNullField(JsonNode jsonNode, String field) { + return jsonNode.has(field) && !(jsonNode.get(field) instanceof NullNode); + } + +} diff --git a/web/src/main/java/org/springframework/security/web/jackson/CookieMixin.java b/web/src/main/java/org/springframework/security/web/jackson/CookieMixin.java new file mode 100644 index 0000000000..1256bec6ff --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/jackson/CookieMixin.java @@ -0,0 +1,37 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.web.jackson; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import tools.jackson.databind.annotation.JsonDeserialize; + +/** + * Mixin class to serialize/deserialize {@link jakarta.servlet.http.Cookie} + * + * @author Sebastien Deleuze + * @author Jitendra Singh + * @since 7.0 + * @see WebServletJacksonModule + * @see org.springframework.security.jackson.SecurityJacksonModules + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) +@JsonDeserialize(using = CookieDeserializer.class) +@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, isGetterVisibility = JsonAutoDetect.Visibility.NONE) +abstract class CookieMixin { + +} diff --git a/web/src/main/java/org/springframework/security/web/jackson/DefaultCsrfTokenMixin.java b/web/src/main/java/org/springframework/security/web/jackson/DefaultCsrfTokenMixin.java new file mode 100644 index 0000000000..c7606a1ede --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/jackson/DefaultCsrfTokenMixin.java @@ -0,0 +1,48 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.web.jackson; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +/** + * Jackson mixin class to serialize/deserialize + * {@link org.springframework.security.web.csrf.DefaultCsrfToken} serialization support. + * + * @author Sebastien Deleuze + * @author Jitendra Singh + * @since 7.0 + * @see WebJacksonModule + * @see org.springframework.security.jackson.SecurityJacksonModules + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) +class DefaultCsrfTokenMixin { + + /** + * JsonCreator constructor needed by Jackson to create + * {@link org.springframework.security.web.csrf.DefaultCsrfToken} object. + * @param headerName the name of the header + * @param parameterName the parameter name + * @param token the CSRF token value + */ + @JsonCreator + DefaultCsrfTokenMixin(@JsonProperty("headerName") String headerName, + @JsonProperty("parameterName") String parameterName, @JsonProperty("token") String token) { + } + +} diff --git a/web/src/main/java/org/springframework/security/web/jackson/DefaultSavedRequestMixin.java b/web/src/main/java/org/springframework/security/web/jackson/DefaultSavedRequestMixin.java new file mode 100644 index 0000000000..f9eee79ee3 --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/jackson/DefaultSavedRequestMixin.java @@ -0,0 +1,45 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.web.jackson; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import tools.jackson.databind.annotation.JsonDeserialize; + +import org.springframework.security.web.savedrequest.DefaultSavedRequest; + +/** + * Jackson mixin class to serialize/deserialize {@link DefaultSavedRequest}. This mixin + * use {@link DefaultSavedRequest.Builder} to deserialized json.In order to use this mixin + * class you also need to register {@link CookieMixin}. + * + * @author Sebastien Deleuze + * @author Jitendra Singh + * @since 7.0 + * @see WebServletJacksonModule + * @see org.springframework.security.jackson.SecurityJacksonModules + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) +@JsonDeserialize(builder = DefaultSavedRequest.Builder.class) +@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE) +abstract class DefaultSavedRequestMixin { + + @JsonInclude(JsonInclude.Include.NON_NULL) + String matchingRequestParameterName; + +} diff --git a/web/src/main/java/org/springframework/security/web/jackson/PreAuthenticatedAuthenticationTokenDeserializer.java b/web/src/main/java/org/springframework/security/web/jackson/PreAuthenticatedAuthenticationTokenDeserializer.java new file mode 100644 index 0000000000..e9913ad220 --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/jackson/PreAuthenticatedAuthenticationTokenDeserializer.java @@ -0,0 +1,79 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.web.jackson; + +import java.util.List; + +import tools.jackson.core.JacksonException; +import tools.jackson.core.JsonParser; +import tools.jackson.core.type.TypeReference; +import tools.jackson.databind.DeserializationContext; +import tools.jackson.databind.JsonNode; +import tools.jackson.databind.ValueDeserializer; +import tools.jackson.databind.node.MissingNode; + +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken; + +/** + * Custom deserializer for {@link PreAuthenticatedAuthenticationToken}. At the time of + * deserialization it will invoke suitable constructor depending on the value of + * authenticated property. It will ensure that the token's state must not change. + * + * @author Sebastien Deleuze + * @author Jitendra Singh + * @since 7.0 + * @see PreAuthenticatedAuthenticationTokenMixin + */ +class PreAuthenticatedAuthenticationTokenDeserializer extends ValueDeserializer { + + private static final TypeReference> GRANTED_AUTHORITY_LIST = new TypeReference<>() { + }; + + /** + * This method construct {@link PreAuthenticatedAuthenticationToken} object from + * serialized json. + * @param jp the JsonParser + * @param ctxt the DeserializationContext + * @return the user + * @throws tools.jackson.core.JacksonException if an error during JSON processing + * occurs + */ + @Override + public PreAuthenticatedAuthenticationToken deserialize(JsonParser jp, DeserializationContext ctxt) + throws JacksonException { + JsonNode jsonNode = ctxt.readTree(jp); + boolean authenticated = readJsonNode(jsonNode, "authenticated").asBoolean(); + JsonNode principalNode = readJsonNode(jsonNode, "principal"); + Object principal = (!principalNode.isObject()) ? principalNode.stringValue() + : ctxt.readTreeAsValue(principalNode, Object.class); + Object credentials = readJsonNode(jsonNode, "credentials").stringValue(); + JsonNode authoritiesNode = readJsonNode(jsonNode, "authorities"); + List authorities = ctxt.readTreeAsValue(authoritiesNode, + ctxt.getTypeFactory().constructType(GRANTED_AUTHORITY_LIST)); + PreAuthenticatedAuthenticationToken token = (!authenticated) + ? new PreAuthenticatedAuthenticationToken(principal, credentials) + : new PreAuthenticatedAuthenticationToken(principal, credentials, authorities); + token.setDetails(readJsonNode(jsonNode, "details")); + return token; + } + + private JsonNode readJsonNode(JsonNode jsonNode, String field) { + return jsonNode.has(field) ? jsonNode.get(field) : MissingNode.getInstance(); + } + +} diff --git a/web/src/main/java/org/springframework/security/web/jackson/PreAuthenticatedAuthenticationTokenMixin.java b/web/src/main/java/org/springframework/security/web/jackson/PreAuthenticatedAuthenticationTokenMixin.java new file mode 100644 index 0000000000..6c12158d50 --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/jackson/PreAuthenticatedAuthenticationTokenMixin.java @@ -0,0 +1,43 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.web.jackson; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import tools.jackson.databind.annotation.JsonDeserialize; + +import org.springframework.security.jackson.SecurityJacksonModules; + +/** + * This mixin class is used to serialize / deserialize + * {@link org.springframework.security.authentication.UsernamePasswordAuthenticationToken}. + * This class register a custom deserializer + * {@link PreAuthenticatedAuthenticationTokenDeserializer}. + * + * @author Sebastien Deleuze + * @author Jitendra Singh + * @since 7.0 + * @see WebJacksonModule + * @see SecurityJacksonModules + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) +@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE, + isGetterVisibility = JsonAutoDetect.Visibility.NONE) +@JsonDeserialize(using = PreAuthenticatedAuthenticationTokenDeserializer.class) +abstract class PreAuthenticatedAuthenticationTokenMixin { + +} diff --git a/web/src/main/java/org/springframework/security/web/jackson/SavedCookieMixin.java b/web/src/main/java/org/springframework/security/web/jackson/SavedCookieMixin.java new file mode 100644 index 0000000000..68b375d96d --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/jackson/SavedCookieMixin.java @@ -0,0 +1,45 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.web.jackson; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +/** + * Jackson mixin class to serialize/deserialize + * {@link org.springframework.security.web.savedrequest.SavedCookie} serialization + * support. + * + * @author Sebastien Deleuze + * @author Jitendra Singh + * @since 7.0 + * @see WebServletJacksonModule + * @see org.springframework.security.jackson.SecurityJacksonModules + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) +@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE) +abstract class SavedCookieMixin { + + @JsonCreator + SavedCookieMixin(@JsonProperty("name") String name, @JsonProperty("value") String value, + @JsonProperty("domain") String domain, @JsonProperty("maxAge") int maxAge, + @JsonProperty("path") String path, @JsonProperty("secure") boolean secure) { + } + +} diff --git a/web/src/main/java/org/springframework/security/web/jackson/SwitchUserGrantedAuthorityMixIn.java b/web/src/main/java/org/springframework/security/web/jackson/SwitchUserGrantedAuthorityMixIn.java new file mode 100644 index 0000000000..122de53b2b --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/jackson/SwitchUserGrantedAuthorityMixIn.java @@ -0,0 +1,46 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.web.jackson; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +import org.springframework.security.core.Authentication; +import org.springframework.security.web.authentication.switchuser.SwitchUserGrantedAuthority; + +/** + * Jackson mixin class to serialize/deserialize {@link SwitchUserGrantedAuthority}. + * + * @author Sebastien Deleuze + * @author Markus Heiden + * @since 7.0 + * @see WebJacksonModule + * @see WebServletJacksonModule + * @see org.springframework.security.jackson.SecurityJacksonModules + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) +@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE, + isGetterVisibility = JsonAutoDetect.Visibility.NONE) +abstract class SwitchUserGrantedAuthorityMixIn { + + @JsonCreator + SwitchUserGrantedAuthorityMixIn(@JsonProperty("role") String role, @JsonProperty("source") Authentication source) { + } + +} diff --git a/web/src/main/java/org/springframework/security/web/jackson/WebAuthenticationDetailsMixin.java b/web/src/main/java/org/springframework/security/web/jackson/WebAuthenticationDetailsMixin.java new file mode 100644 index 0000000000..32b28381a9 --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/jackson/WebAuthenticationDetailsMixin.java @@ -0,0 +1,44 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.web.jackson; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +/** + * Jackson mixin class to serialize/deserialize + * {@link org.springframework.security.web.authentication.WebAuthenticationDetails}. + * + * @author Sebastien Deleuze + * @author Jitendra Singh + * @since 7.0 + * @see WebServletJacksonModule + * @see org.springframework.security.jackson.SecurityJacksonModules + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY) +@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE, + isGetterVisibility = JsonAutoDetect.Visibility.NONE, creatorVisibility = JsonAutoDetect.Visibility.ANY) +class WebAuthenticationDetailsMixin { + + @JsonCreator + WebAuthenticationDetailsMixin(@JsonProperty("remoteAddress") String remoteAddress, + @JsonProperty("sessionId") String sessionId) { + } + +} diff --git a/web/src/main/java/org/springframework/security/web/jackson/WebJacksonModule.java b/web/src/main/java/org/springframework/security/web/jackson/WebJacksonModule.java new file mode 100644 index 0000000000..ecd4d8942e --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/jackson/WebJacksonModule.java @@ -0,0 +1,76 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.web.jackson; + +import tools.jackson.core.Version; +import tools.jackson.databind.jsontype.BasicPolymorphicTypeValidator; + +import org.springframework.security.jackson.SecurityJacksonModule; +import org.springframework.security.jackson.SecurityJacksonModules; +import org.springframework.security.web.authentication.WebAuthenticationDetails; +import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken; +import org.springframework.security.web.authentication.switchuser.SwitchUserGrantedAuthority; +import org.springframework.security.web.csrf.DefaultCsrfToken; +import org.springframework.security.web.savedrequest.DefaultSavedRequest; +import org.springframework.security.web.savedrequest.SavedCookie; + +/** + * Jackson module for spring-security-web. This module register + * {@link DefaultCsrfTokenMixin}, {@link PreAuthenticatedAuthenticationTokenMixin} and + * {@link SwitchUserGrantedAuthorityMixIn}. + * + *

+ * The recommended way to configure it is to use {@link SecurityJacksonModules} in order + * to enable properly automatic inclusion of type information with related validation. + * + *

+ *     ClassLoader loader = getClass().getClassLoader();
+ *     JsonMapper mapper = JsonMapper.builder()
+ * 				.addModules(SecurityJacksonModules.getModules(loader))
+ * 				.build();
+ * 
+ * + * @author Sebastien Deleuze + * @author Jitendra Singh + * @since 7.0 + * @see SecurityJacksonModules + */ +@SuppressWarnings("serial") +public class WebJacksonModule extends SecurityJacksonModule { + + public WebJacksonModule() { + super(WebJacksonModule.class.getName(), new Version(1, 0, 0, null, null, null)); + } + + @Override + public void configurePolymorphicTypeValidator(BasicPolymorphicTypeValidator.Builder builder) { + builder.allowIfSubType(DefaultCsrfToken.class) + .allowIfSubType(SavedCookie.class) + .allowIfSubType(DefaultSavedRequest.class) + .allowIfSubType(WebAuthenticationDetails.class) + .allowIfSubType(PreAuthenticatedAuthenticationToken.class) + .allowIfSubType(SwitchUserGrantedAuthority.class); + } + + @Override + public void setupModule(SetupContext context) { + context.setMixIn(DefaultCsrfToken.class, DefaultCsrfTokenMixin.class); + context.setMixIn(PreAuthenticatedAuthenticationToken.class, PreAuthenticatedAuthenticationTokenMixin.class); + context.setMixIn(SwitchUserGrantedAuthority.class, SwitchUserGrantedAuthorityMixIn.class); + } + +} diff --git a/web/src/main/java/org/springframework/security/web/jackson/WebServletJacksonModule.java b/web/src/main/java/org/springframework/security/web/jackson/WebServletJacksonModule.java new file mode 100644 index 0000000000..4d716a43d8 --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/jackson/WebServletJacksonModule.java @@ -0,0 +1,73 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.web.jackson; + +import jakarta.servlet.http.Cookie; +import tools.jackson.core.Version; +import tools.jackson.databind.jsontype.BasicPolymorphicTypeValidator; + +import org.springframework.security.jackson.SecurityJacksonModule; +import org.springframework.security.jackson.SecurityJacksonModules; +import org.springframework.security.web.authentication.WebAuthenticationDetails; +import org.springframework.security.web.authentication.switchuser.SwitchUserGrantedAuthority; +import org.springframework.security.web.savedrequest.DefaultSavedRequest; +import org.springframework.security.web.savedrequest.SavedCookie; +import org.springframework.security.web.server.csrf.DefaultCsrfToken; + +/** + * Jackson module for spring-security-web related to servlet. This module registers + * {@link CookieMixin}, {@link SavedCookieMixin}, {@link DefaultSavedRequestMixin}, + * {@link WebAuthenticationDetailsMixin}, and {@link SwitchUserGrantedAuthorityMixIn}. + * + *

+ * The recommended way to configure it is to use {@link SecurityJacksonModules} in order + * to enable properly automatic inclusion of type information with related validation. + * + *

+ *     ClassLoader loader = getClass().getClassLoader();
+ *     JsonMapper mapper = JsonMapper.builder()
+ * 				.addModules(SecurityJacksonModules.getModules(loader))
+ * 				.build();
+ * 
+ * + * @author Sebastien Deleuze + * @author Boris Finkelshteyn + * @since 7.0 + * @see SecurityJacksonModules + */ +@SuppressWarnings("serial") +public class WebServletJacksonModule extends SecurityJacksonModule { + + public WebServletJacksonModule() { + super(WebServletJacksonModule.class.getName(), new Version(1, 0, 0, null, null, null)); + } + + @Override + public void configurePolymorphicTypeValidator(BasicPolymorphicTypeValidator.Builder builder) { + builder.allowIfSubType(Cookie.class).allowIfSubType(DefaultCsrfToken.class); + } + + @Override + public void setupModule(SetupContext context) { + context.setMixIn(Cookie.class, CookieMixin.class); + context.setMixIn(SavedCookie.class, SavedCookieMixin.class); + context.setMixIn(DefaultSavedRequest.class, DefaultSavedRequestMixin.class); + context.setMixIn(WebAuthenticationDetails.class, WebAuthenticationDetailsMixin.class); + context.setMixIn(SwitchUserGrantedAuthority.class, SwitchUserGrantedAuthorityMixIn.class); + } + +} diff --git a/web/src/main/java/org/springframework/security/web/jackson/package-info.java b/web/src/main/java/org/springframework/security/web/jackson/package-info.java new file mode 100644 index 0000000000..1e5c3c1d49 --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/jackson/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Jackson 3+ serialization support for web. + */ +package org.springframework.security.web.jackson; diff --git a/web/src/main/java/org/springframework/security/web/jackson2/package-info.java b/web/src/main/java/org/springframework/security/web/jackson2/package-info.java index 92dbfadf4f..5137100387 100644 --- a/web/src/main/java/org/springframework/security/web/jackson2/package-info.java +++ b/web/src/main/java/org/springframework/security/web/jackson2/package-info.java @@ -15,10 +15,7 @@ */ /** - * Mix-in classes to provide Jackson serialization support. - * - * @author Jitendra Singh - * @since 4.2 + * Jackson 2 serialization support for web. */ @NullMarked package org.springframework.security.web.jackson2; diff --git a/web/src/main/java/org/springframework/security/web/server/jackson/DefaultCsrfServerTokenMixin.java b/web/src/main/java/org/springframework/security/web/server/jackson/DefaultCsrfServerTokenMixin.java new file mode 100644 index 0000000000..be1a2251af --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/server/jackson/DefaultCsrfServerTokenMixin.java @@ -0,0 +1,48 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.web.server.jackson; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +/** + * Jackson mixin class to serialize/deserialize + * {@link org.springframework.security.web.server.csrf.DefaultCsrfToken} serialization + * support. + * + * @author Sebastien Deleuze + * @author Boris Finkelshteyn + * @since 7.0 + * @see WebServerJacksonModule + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) +class DefaultCsrfServerTokenMixin { + + /** + * JsonCreator constructor needed by Jackson to create + * {@link org.springframework.security.web.server.csrf.DefaultCsrfToken} object. + * @param headerName the name of the header + * @param parameterName the parameter name + * @param token the CSRF token value + */ + @JsonCreator + DefaultCsrfServerTokenMixin(@JsonProperty("headerName") String headerName, + @JsonProperty("parameterName") String parameterName, @JsonProperty("token") String token) { + } + +} diff --git a/web/src/main/java/org/springframework/security/web/server/jackson/WebServerJacksonModule.java b/web/src/main/java/org/springframework/security/web/server/jackson/WebServerJacksonModule.java new file mode 100644 index 0000000000..985b86dee0 --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/server/jackson/WebServerJacksonModule.java @@ -0,0 +1,65 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.web.server.jackson; + +import tools.jackson.core.Version; +import tools.jackson.databind.jsontype.BasicPolymorphicTypeValidator; + +import org.springframework.security.jackson.SecurityJacksonModule; +import org.springframework.security.jackson.SecurityJacksonModules; +import org.springframework.security.web.server.csrf.DefaultCsrfToken; + +/** + * Jackson module for spring-security-web-flux. This module register + * {@link DefaultCsrfServerTokenMixin}. + * + *

+ * The recommended way to configure it is to use {@link SecurityJacksonModules} in order + * to enable properly automatic inclusion of type information with related validation. + * + *

+ *     ClassLoader loader = getClass().getClassLoader();
+ *     JsonMapper mapper = JsonMapper.builder()
+ * 				.addModules(SecurityJacksonModules.getModules(loader))
+ * 				.build();
+ * 
+ * + * @author Boris Finkelshteyn + * @since 5.1 + * @see SecurityJacksonModules + */ +@SuppressWarnings("serial") +public class WebServerJacksonModule extends SecurityJacksonModule { + + private static final String NAME = WebServerJacksonModule.class.getName(); + + private static final Version VERSION = new Version(1, 0, 0, null, null, null); + + public WebServerJacksonModule() { + super(NAME, VERSION); + } + + @Override + public void configurePolymorphicTypeValidator(BasicPolymorphicTypeValidator.Builder builder) { + } + + @Override + public void setupModule(SetupContext context) { + context.setMixIn(DefaultCsrfToken.class, DefaultCsrfServerTokenMixin.class); + } + +} diff --git a/web/src/main/java/org/springframework/security/web/server/jackson/package-info.java b/web/src/main/java/org/springframework/security/web/server/jackson/package-info.java new file mode 100644 index 0000000000..2427f318c6 --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/server/jackson/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Jackson 3+ serialization support for reactive web server. + */ +@NullMarked +package org.springframework.security.web.server.jackson; + +import org.jspecify.annotations.NullMarked; diff --git a/web/src/main/java/org/springframework/security/web/server/jackson2/package-info.java b/web/src/main/java/org/springframework/security/web/server/jackson2/package-info.java index 30c26db3ea..db2a9e0ac5 100644 --- a/web/src/main/java/org/springframework/security/web/server/jackson2/package-info.java +++ b/web/src/main/java/org/springframework/security/web/server/jackson2/package-info.java @@ -15,7 +15,7 @@ */ /** - * Reactive web jackson2 integration. + * Jackson 2 serialization support for reactive web server. */ @NullMarked package org.springframework.security.web.server.jackson2; diff --git a/web/src/test/java/org/springframework/security/web/jackson/AbstractMixinTests.java b/web/src/test/java/org/springframework/security/web/jackson/AbstractMixinTests.java new file mode 100644 index 0000000000..3ee641b98d --- /dev/null +++ b/web/src/test/java/org/springframework/security/web/jackson/AbstractMixinTests.java @@ -0,0 +1,38 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.web.jackson; + +import org.junit.jupiter.api.BeforeEach; +import tools.jackson.databind.json.JsonMapper; + +import org.springframework.security.jackson.SecurityJacksonModules; + +/** + * @author Sebastien Deleuze + * @author Jitenra Singh + */ +public abstract class AbstractMixinTests { + + protected JsonMapper mapper; + + @BeforeEach + public void setup() { + ClassLoader loader = getClass().getClassLoader(); + this.mapper = JsonMapper.builder().addModules(SecurityJacksonModules.getModules(loader)).build(); + } + +} diff --git a/web/src/test/java/org/springframework/security/web/jackson/CookieMixinTests.java b/web/src/test/java/org/springframework/security/web/jackson/CookieMixinTests.java new file mode 100644 index 0000000000..ffc69702a3 --- /dev/null +++ b/web/src/test/java/org/springframework/security/web/jackson/CookieMixinTests.java @@ -0,0 +1,95 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.web.jackson; + +import jakarta.servlet.http.Cookie; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; +import tools.jackson.core.JacksonException; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Jitendra Singh + * @since 4.2 + */ +public class CookieMixinTests extends AbstractMixinTests { + + // @formatter:off + private static final String COOKIE_JSON = "{" + + " \"@class\": \"jakarta.servlet.http.Cookie\"," + + " \"name\": \"demo\"," + + " \"value\": \"cookie1\"," + + " \"attributes\":{\"@class\":\"java.util.Collections$EmptyMap\"}," + + " \"comment\": null," + + " \"maxAge\": -1," + + " \"path\": null," + + " \"secure\": false," + + " \"version\": 0," + + " \"domain\": null" + + "}"; + // @formatter:on + + // @formatter:off + private static final String COOKIE_HTTP_ONLY_JSON = "{" + + " \"@class\": \"jakarta.servlet.http.Cookie\"," + + " \"name\": \"demo\"," + + " \"value\": \"cookie1\"," + + " \"attributes\":{\"@class\":\"java.util.Collections$UnmodifiableMap\", \"HttpOnly\": \"\"}," + + " \"comment\": null," + + " \"maxAge\": -1," + + " \"path\": null," + + " \"secure\": false," + + " \"version\": 0," + + " \"domain\": null" + + "}"; + // @formatter:on + + @Test + public void serializeCookie() throws JacksonException, JSONException { + Cookie cookie = new Cookie("demo", "cookie1"); + String actualString = this.mapper.writeValueAsString(cookie); + JSONAssert.assertEquals(COOKIE_JSON, actualString, true); + } + + @Test + public void deserializeCookie() { + Cookie cookie = this.mapper.readValue(COOKIE_JSON, Cookie.class); + assertThat(cookie).isNotNull(); + assertThat(cookie.getName()).isEqualTo("demo"); + assertThat(cookie.getDomain()).isNull(); + } + + @Test + public void serializeCookieWithHttpOnly() throws JacksonException, JSONException { + Cookie cookie = new Cookie("demo", "cookie1"); + cookie.setHttpOnly(true); + String actualString = this.mapper.writeValueAsString(cookie); + JSONAssert.assertEquals(COOKIE_HTTP_ONLY_JSON, actualString, true); + } + + @Test + public void deserializeCookieWithHttpOnly() { + Cookie cookie = this.mapper.readValue(COOKIE_HTTP_ONLY_JSON, Cookie.class); + assertThat(cookie).isNotNull(); + assertThat(cookie.getName()).isEqualTo("demo"); + assertThat(cookie.getDomain()).isNull(); + assertThat(cookie.isHttpOnly()).isEqualTo(true); + } + +} diff --git a/web/src/test/java/org/springframework/security/web/jackson/DefaultCsrfTokenMixinTests.java b/web/src/test/java/org/springframework/security/web/jackson/DefaultCsrfTokenMixinTests.java new file mode 100644 index 0000000000..035a32107e --- /dev/null +++ b/web/src/test/java/org/springframework/security/web/jackson/DefaultCsrfTokenMixinTests.java @@ -0,0 +1,73 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.web.jackson; + +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; +import tools.jackson.core.JacksonException; + +import org.springframework.security.web.csrf.DefaultCsrfToken; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * @author Jitendra Singh + * @since 4.2 + */ +public class DefaultCsrfTokenMixinTests extends AbstractMixinTests { + + // @formatter:off + public static final String CSRF_JSON = "{" + + "\"@class\": \"org.springframework.security.web.csrf.DefaultCsrfToken\", " + + "\"headerName\": \"csrf-header\", " + + "\"parameterName\": \"_csrf\", " + + "\"token\": \"1\"" + + "}"; + // @formatter:on + @Test + public void defaultCsrfTokenSerializedTest() throws JacksonException, JSONException { + DefaultCsrfToken token = new DefaultCsrfToken("csrf-header", "_csrf", "1"); + String serializedJson = this.mapper.writeValueAsString(token); + JSONAssert.assertEquals(CSRF_JSON, serializedJson, true); + } + + @Test + public void defaultCsrfTokenDeserializeTest() { + DefaultCsrfToken token = this.mapper.readValue(CSRF_JSON, DefaultCsrfToken.class); + assertThat(token).isNotNull(); + assertThat(token.getHeaderName()).isEqualTo("csrf-header"); + assertThat(token.getParameterName()).isEqualTo("_csrf"); + assertThat(token.getToken()).isEqualTo("1"); + } + + @Test + public void defaultCsrfTokenDeserializeWithoutClassTest() { + String tokenJson = "{\"headerName\": \"csrf-header\", \"parameterName\": \"_csrf\", \"token\": \"1\"}"; + assertThatExceptionOfType(JacksonException.class) + .isThrownBy(() -> this.mapper.readValue(tokenJson, DefaultCsrfToken.class)); + } + + @Test + public void defaultCsrfTokenDeserializeNullValuesTest() { + String tokenJson = "{\"@class\": \"org.springframework.security.web.csrf.DefaultCsrfToken\", \"headerName\": \"\", \"parameterName\": null, \"token\": \"1\"}"; + assertThatExceptionOfType(JacksonException.class) + .isThrownBy(() -> this.mapper.readValue(tokenJson, DefaultCsrfToken.class)); + } + +} diff --git a/web/src/test/java/org/springframework/security/web/jackson/DefaultSavedRequestMixinTests.java b/web/src/test/java/org/springframework/security/web/jackson/DefaultSavedRequestMixinTests.java new file mode 100644 index 0000000000..fd1e3df743 --- /dev/null +++ b/web/src/test/java/org/springframework/security/web/jackson/DefaultSavedRequestMixinTests.java @@ -0,0 +1,172 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.web.jackson; + +import java.io.IOException; +import java.util.Collections; +import java.util.Locale; + +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletRequestWrapper; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; + +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.security.web.savedrequest.DefaultSavedRequest; +import org.springframework.security.web.savedrequest.SavedCookie; +import org.springframework.security.web.util.UrlUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Jitendra Singh + * @since 4.2 + */ +public class DefaultSavedRequestMixinTests extends AbstractMixinTests { + + // @formatter:off + private static final String COOKIES_JSON = "[\"java.util.ArrayList\", [{" + + "\"@class\": \"org.springframework.security.web.savedrequest.SavedCookie\", " + + "\"name\": \"SESSION\", " + + "\"value\": \"123456789\", " + + "\"maxAge\": -1, " + + "\"path\": null, " + + "\"secure\":false, " + + "\"domain\": null" + + "}]]"; + // @formatter:on + // @formatter:off + private static final String REQUEST_JSON = "{" + + "\"@class\": \"org.springframework.security.web.savedrequest.DefaultSavedRequest\", " + + "\"cookies\": " + COOKIES_JSON + "," + + "\"locales\": [\"java.util.ArrayList\", [\"en\"]], " + + "\"headers\": {\"@class\": \"java.util.TreeMap\", \"x-auth-token\": [\"java.util.ArrayList\", [\"12\"]]}, " + + "\"parameters\": {\"@class\": \"java.util.TreeMap\"}," + + "\"contextPath\": \"\", " + + "\"method\": \"\", " + + "\"pathInfo\": null, " + + "\"queryString\": null, " + + "\"requestURI\": \"\", " + + "\"requestURL\": \"http://localhost\", " + + "\"scheme\": \"http\", " + + "\"serverName\": \"localhost\", " + + "\"servletPath\": \"\", " + + "\"serverPort\": 80" + + "}"; + // @formatter:on + // @formatter:off + private static final String REQUEST_WITH_MATCHING_REQUEST_PARAM_NAME_JSON = "{" + + "\"@class\": \"org.springframework.security.web.savedrequest.DefaultSavedRequest\", " + + "\"cookies\": " + COOKIES_JSON + "," + + "\"locales\": [\"java.util.ArrayList\", [\"en\"]], " + + "\"headers\": {\"@class\": \"java.util.TreeMap\", \"x-auth-token\": [\"java.util.ArrayList\", [\"12\"]]}, " + + "\"parameters\": {\"@class\": \"java.util.TreeMap\"}," + + "\"contextPath\": \"\", " + + "\"method\": \"\", " + + "\"pathInfo\": null, " + + "\"queryString\": null, " + + "\"requestURI\": \"\", " + + "\"requestURL\": \"http://localhost\", " + + "\"scheme\": \"http\", " + + "\"serverName\": \"localhost\", " + + "\"servletPath\": \"\", " + + "\"serverPort\": 80, " + + "\"matchingRequestParameterName\": \"success\"" + + "}"; + // @formatter:on + @Test + public void matchRequestBuildWithConstructorAndBuilder() { + DefaultSavedRequest request = new DefaultSavedRequest.Builder() + .setCookies(Collections.singletonList(new SavedCookie(new Cookie("SESSION", "123456789")))) + .setHeaders(Collections.singletonMap("x-auth-token", Collections.singletonList("12"))) + .setScheme("http") + .setRequestURL("http://localhost") + .setServerName("localhost") + .setRequestURI("") + .setLocales(Collections.singletonList(new Locale("en"))) + .setContextPath("") + .setMethod("") + .setServletPath("") + .build(); + MockHttpServletRequest mockRequest = new MockHttpServletRequest(); + mockRequest.setCookies(new Cookie("SESSION", "123456789")); + mockRequest.addHeader("x-auth-token", "12"); + String currentUrl = UrlUtils.buildFullRequestUrl(mockRequest); + assertThat(request.getRedirectUrl().equals(currentUrl)).isTrue(); + } + + @Test + public void serializeDefaultRequestBuildWithConstructorTest() throws IOException, JSONException { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addHeader("x-auth-token", "12"); + // Spring 5 MockHttpServletRequest automatically adds a header when the cookies + // are set. To get consistency we override the request. + HttpServletRequest requestToWrite = new HttpServletRequestWrapper(request) { + @Override + public Cookie[] getCookies() { + return new Cookie[] { new Cookie("SESSION", "123456789") }; + } + }; + String actualString = this.mapper.writerWithDefaultPrettyPrinter() + .writeValueAsString(new DefaultSavedRequest(requestToWrite)); + JSONAssert.assertEquals(REQUEST_JSON, actualString, true); + } + + @Test + public void serializeDefaultRequestBuildWithBuilderTest() throws IOException, JSONException { + DefaultSavedRequest request = new DefaultSavedRequest.Builder() + .setCookies(Collections.singletonList(new SavedCookie(new Cookie("SESSION", "123456789")))) + .setHeaders(Collections.singletonMap("x-auth-token", Collections.singletonList("12"))) + .setScheme("http") + .setRequestURL("http://localhost") + .setServerName("localhost") + .setRequestURI("") + .setLocales(Collections.singletonList(new Locale("en"))) + .setContextPath("") + .setMethod("") + .setServletPath("") + .build(); + String actualString = this.mapper.writerWithDefaultPrettyPrinter().writeValueAsString(request); + JSONAssert.assertEquals(REQUEST_JSON, actualString, true); + } + + @Test + public void deserializeDefaultSavedRequest() { + DefaultSavedRequest request = (DefaultSavedRequest) this.mapper.readValue(REQUEST_JSON, Object.class); + assertThat(request).isNotNull(); + assertThat(request.getCookies()).hasSize(1); + assertThat(request.getLocales()).hasSize(1).contains(new Locale("en")); + assertThat(request.getHeaderNames()).hasSize(1).contains("x-auth-token"); + assertThat(request.getHeaderValues("x-auth-token")).hasSize(1).contains("12"); + } + + @Test + public void deserializeWhenMatchingRequestParameterNameThenRedirectUrlContainsParam() { + DefaultSavedRequest request = (DefaultSavedRequest) this.mapper + .readValue(REQUEST_WITH_MATCHING_REQUEST_PARAM_NAME_JSON, Object.class); + assertThat(request.getRedirectUrl()).isEqualTo("http://localhost?success"); + } + + @Test + public void deserializeWhenNullMatchingRequestParameterNameThenRedirectUrlDoesNotContainParam() { + DefaultSavedRequest request = (DefaultSavedRequest) this.mapper.readValue(REQUEST_JSON, Object.class); + assertThat(request.getRedirectUrl()).isEqualTo("http://localhost"); + } + +} diff --git a/web/src/test/java/org/springframework/security/web/jackson/PreAuthenticatedAuthenticationTokenMixinTests.java b/web/src/test/java/org/springframework/security/web/jackson/PreAuthenticatedAuthenticationTokenMixinTests.java new file mode 100644 index 0000000000..b04646db11 --- /dev/null +++ b/web/src/test/java/org/springframework/security/web/jackson/PreAuthenticatedAuthenticationTokenMixinTests.java @@ -0,0 +1,70 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.web.jackson; + +import org.json.JSONException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; +import tools.jackson.core.JacksonException; + +import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.security.jackson2.SimpleGrantedAuthorityMixinTests; +import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Rob Winch + * @since 4.2 + */ +public class PreAuthenticatedAuthenticationTokenMixinTests extends AbstractMixinTests { + + // @formatter:off + private static final String PREAUTH_JSON = "{" + + "\"@class\": \"org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken\"," + + "\"principal\": \"principal\", " + + "\"credentials\": \"credentials\", " + + "\"authenticated\": true, " + + "\"details\": null, " + + "\"authorities\": " + SimpleGrantedAuthorityMixinTests.AUTHORITIES_ARRAYLIST_JSON + + "}"; + // @formatter:on + PreAuthenticatedAuthenticationToken expected; + + @BeforeEach + public void setupExpected() { + this.expected = new PreAuthenticatedAuthenticationToken("principal", "credentials", + AuthorityUtils.createAuthorityList("ROLE_USER")); + } + + @Test + public void serializeWhenPrincipalCredentialsAuthoritiesThenSuccess() throws JacksonException, JSONException { + String serializedJson = this.mapper.writeValueAsString(this.expected); + JSONAssert.assertEquals(PREAUTH_JSON, serializedJson, true); + } + + @Test + public void deserializeAuthenticatedUsernamePasswordAuthenticationTokenMixinTest() { + PreAuthenticatedAuthenticationToken deserialized = this.mapper.readValue(PREAUTH_JSON, + PreAuthenticatedAuthenticationToken.class); + assertThat(deserialized).isNotNull(); + assertThat(deserialized.isAuthenticated()).isTrue(); + assertThat(deserialized.getAuthorities()).isEqualTo(this.expected.getAuthorities()); + } + +} diff --git a/web/src/test/java/org/springframework/security/web/jackson/SavedCookieMixinTests.java b/web/src/test/java/org/springframework/security/web/jackson/SavedCookieMixinTests.java new file mode 100644 index 0000000000..efa7fc8bf1 --- /dev/null +++ b/web/src/test/java/org/springframework/security/web/jackson/SavedCookieMixinTests.java @@ -0,0 +1,98 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.web.jackson; + +import java.util.ArrayList; +import java.util.List; + +import jakarta.servlet.http.Cookie; +import org.json.JSONException; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; +import tools.jackson.core.JacksonException; + +import org.springframework.security.web.savedrequest.SavedCookie; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Jitendra Singh + */ +public class SavedCookieMixinTests extends AbstractMixinTests { + + // @formatter:off + private static final String COOKIE_JSON = "{" + + "\"@class\": \"org.springframework.security.web.savedrequest.SavedCookie\", " + + "\"name\": \"SESSION\", " + + "\"value\": \"123456789\", " + + "\"maxAge\": -1, " + + "\"path\": null, " + + "\"secure\":false, " + + "\"domain\": null" + + "}"; + // @formatter:on + // @formatter:off + private static final String COOKIES_JSON = "[\"java.util.ArrayList\", [" + + COOKIE_JSON + + "]]"; + // @formatter:on + @Test + public void serializeWithDefaultConfigurationTest() throws JacksonException, JSONException { + SavedCookie savedCookie = new SavedCookie(new Cookie("SESSION", "123456789")); + String actualJson = this.mapper.writeValueAsString(savedCookie); + JSONAssert.assertEquals(COOKIE_JSON, actualJson, true); + } + + @Test + @Disabled("No supported by Jackson 3 as ObjectMapper/JsonMapper is immutable") + public void serializeWithOverrideConfigurationTest() throws JacksonException, JSONException { + SavedCookie savedCookie = new SavedCookie(new Cookie("SESSION", "123456789")); + // this.mapper.setVisibility(PropertyAccessor.FIELD, + // JsonAutoDetect.Visibility.PUBLIC_ONLY) + // .setVisibility(PropertyAccessor.GETTER, JsonAutoDetect.Visibility.ANY); + String actualJson = this.mapper.writeValueAsString(savedCookie); + JSONAssert.assertEquals(COOKIE_JSON, actualJson, true); + } + + @Test + public void serializeSavedCookieWithList() throws JacksonException, JSONException { + List savedCookies = new ArrayList<>(); + savedCookies.add(new SavedCookie(new Cookie("SESSION", "123456789"))); + String actualJson = this.mapper.writeValueAsString(savedCookies); + JSONAssert.assertEquals(COOKIES_JSON, actualJson, true); + } + + @Test + @SuppressWarnings("unchecked") + public void deserializeSavedCookieWithList() { + List savedCookies = (List) this.mapper.readValue(COOKIES_JSON, Object.class); + assertThat(savedCookies).isNotNull().hasSize(1); + assertThat(savedCookies.get(0).getName()).isEqualTo("SESSION"); + assertThat(savedCookies.get(0).getValue()).isEqualTo("123456789"); + } + + @Test + public void deserializeSavedCookieJsonTest() { + SavedCookie savedCookie = (SavedCookie) this.mapper.readValue(COOKIE_JSON, Object.class); + assertThat(savedCookie).isNotNull(); + assertThat(savedCookie.getName()).isEqualTo("SESSION"); + assertThat(savedCookie.getValue()).isEqualTo("123456789"); + assertThat(savedCookie.isSecure()).isEqualTo(false); + } + +} diff --git a/web/src/test/java/org/springframework/security/web/jackson/SwitchUserGrantedAuthorityMixInTests.java b/web/src/test/java/org/springframework/security/web/jackson/SwitchUserGrantedAuthorityMixInTests.java new file mode 100644 index 0000000000..445e39e5f5 --- /dev/null +++ b/web/src/test/java/org/springframework/security/web/jackson/SwitchUserGrantedAuthorityMixInTests.java @@ -0,0 +1,82 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.web.jackson; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; +import tools.jackson.databind.json.JsonMapper; + +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.security.jackson.SecurityJacksonModules; +import org.springframework.security.jackson.SimpleGrantedAuthorityMixinTests; +import org.springframework.security.web.authentication.switchuser.SwitchUserGrantedAuthority; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Markus Heiden + * @since 6.3 + */ +public class SwitchUserGrantedAuthorityMixInTests { + + // language=JSON + private static final String SWITCH_JSON = """ + { + "@class": "org.springframework.security.web.authentication.switchuser.SwitchUserGrantedAuthority", + "role": "switched", + "source": { + "@class": "org.springframework.security.authentication.UsernamePasswordAuthenticationToken", + "principal": "principal", + "credentials": "credentials", + "authenticated": true, + "details": null, + "authorities": %s + } + } + """.formatted(SimpleGrantedAuthorityMixinTests.AUTHORITIES_ARRAYLIST_JSON); + + private Authentication source; + + private JsonMapper mapper; + + @BeforeEach + public void setUp() { + this.source = new UsernamePasswordAuthenticationToken("principal", "credentials", + AuthorityUtils.createAuthorityList("ROLE_USER")); + ClassLoader classLoader = SwitchUserGrantedAuthorityMixInTests.class.getClassLoader(); + this.mapper = JsonMapper.builder().addModules(SecurityJacksonModules.getModules(classLoader)).build(); + } + + @Test + public void serializeWhenPrincipalCredentialsAuthoritiesThenSuccess() throws Exception { + SwitchUserGrantedAuthority expected = new SwitchUserGrantedAuthority("switched", this.source); + String serializedJson = this.mapper.writeValueAsString(expected); + JSONAssert.assertEquals(SWITCH_JSON, serializedJson, true); + } + + @Test + public void deserializeWhenSourceIsUsernamePasswordAuthenticationTokenThenSuccess() { + SwitchUserGrantedAuthority deserialized = this.mapper.readValue(SWITCH_JSON, SwitchUserGrantedAuthority.class); + assertThat(deserialized).isNotNull(); + assertThat(deserialized.getAuthority()).isEqualTo("switched"); + assertThat(deserialized.getSource()).isEqualTo(this.source); + } + +} diff --git a/web/src/test/java/org/springframework/security/web/jackson/WebAuthenticationDetailsMixinTests.java b/web/src/test/java/org/springframework/security/web/jackson/WebAuthenticationDetailsMixinTests.java new file mode 100644 index 0000000000..f4f14b5492 --- /dev/null +++ b/web/src/test/java/org/springframework/security/web/jackson/WebAuthenticationDetailsMixinTests.java @@ -0,0 +1,81 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.web.jackson; + +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; +import tools.jackson.core.JacksonException; + +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpSession; +import org.springframework.security.web.authentication.WebAuthenticationDetails; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Jitendra Singh + * @since 4.2 + */ +public class WebAuthenticationDetailsMixinTests extends AbstractMixinTests { + + // @formatter:off + private static final String AUTHENTICATION_DETAILS_JSON = "{" + + "\"@class\": \"org.springframework.security.web.authentication.WebAuthenticationDetails\"," + + "\"sessionId\": \"1\", " + + "\"remoteAddress\": " + + "\"/localhost\"" + + "}"; + // @formatter:on + @Test + public void buildWebAuthenticationDetailsUsingDifferentConstructors() { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setRemoteAddr("localhost"); + request.setSession(new MockHttpSession(null, "1")); + WebAuthenticationDetails details = new WebAuthenticationDetails(request); + WebAuthenticationDetails authenticationDetails = this.mapper.readValue(AUTHENTICATION_DETAILS_JSON, + WebAuthenticationDetails.class); + assertThat(details.equals(authenticationDetails)); + } + + @Test + public void webAuthenticationDetailsSerializeTest() throws JacksonException, JSONException { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setRemoteAddr("/localhost"); + request.setSession(new MockHttpSession(null, "1")); + WebAuthenticationDetails details = new WebAuthenticationDetails(request); + String actualJson = this.mapper.writeValueAsString(details); + JSONAssert.assertEquals(AUTHENTICATION_DETAILS_JSON, actualJson, true); + } + + @Test + public void webAuthenticationDetailsJackson2SerializeTest() throws JacksonException, JSONException { + WebAuthenticationDetails details = new WebAuthenticationDetails("/localhost", "1"); + String actualJson = this.mapper.writeValueAsString(details); + JSONAssert.assertEquals(AUTHENTICATION_DETAILS_JSON, actualJson, true); + } + + @Test + public void webAuthenticationDetailsDeserializeTest() { + WebAuthenticationDetails details = this.mapper.readValue(AUTHENTICATION_DETAILS_JSON, + WebAuthenticationDetails.class); + assertThat(details).isNotNull(); + assertThat(details.getRemoteAddress()).isEqualTo("/localhost"); + assertThat(details.getSessionId()).isEqualTo("1"); + } + +} diff --git a/web/src/test/java/org/springframework/security/web/server/jackson/DefaultCsrfServerTokenMixinTests.java b/web/src/test/java/org/springframework/security/web/server/jackson/DefaultCsrfServerTokenMixinTests.java new file mode 100644 index 0000000000..177cf7d924 --- /dev/null +++ b/web/src/test/java/org/springframework/security/web/server/jackson/DefaultCsrfServerTokenMixinTests.java @@ -0,0 +1,76 @@ +/* + * Copyright 2004-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.web.server.jackson; + +import java.io.IOException; + +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; +import tools.jackson.core.JacksonException; + +import org.springframework.security.web.jackson.AbstractMixinTests; +import org.springframework.security.web.server.csrf.DefaultCsrfToken; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * @author Sebastien Deleuze + * @author Boris Finkelshteyn + */ +public class DefaultCsrfServerTokenMixinTests extends AbstractMixinTests { + + // @formatter:off + private static final String CSRF_JSON = "{" + + "\"@class\": \"org.springframework.security.web.server.csrf.DefaultCsrfToken\", " + + "\"headerName\": \"csrf-header\", " + + "\"parameterName\": \"_csrf\", " + + "\"token\": \"1\"" + + "}"; + // @formatter:on + @Test + public void defaultCsrfTokenSerializedTest() throws JacksonException, JSONException { + DefaultCsrfToken token = new DefaultCsrfToken("csrf-header", "_csrf", "1"); + String serializedJson = this.mapper.writeValueAsString(token); + JSONAssert.assertEquals(CSRF_JSON, serializedJson, true); + } + + @Test + public void defaultCsrfTokenDeserializeTest() throws IOException { + DefaultCsrfToken token = this.mapper.readValue(CSRF_JSON, DefaultCsrfToken.class); + assertThat(token).isNotNull(); + assertThat(token.getHeaderName()).isEqualTo("csrf-header"); + assertThat(token.getParameterName()).isEqualTo("_csrf"); + assertThat(token.getToken()).isEqualTo("1"); + } + + @Test + public void defaultCsrfTokenDeserializeWithoutClassTest() throws IOException { + String tokenJson = "{\"headerName\": \"csrf-header\", \"parameterName\": \"_csrf\", \"token\": \"1\"}"; + assertThatExceptionOfType(JacksonException.class) + .isThrownBy(() -> this.mapper.readValue(tokenJson, DefaultCsrfToken.class)); + } + + @Test + public void defaultCsrfTokenDeserializeNullValuesTest() throws IOException { + String tokenJson = "{\"@class\": \"org.springframework.security.web.server.csrf.DefaultCsrfToken\", \"headerName\": \"\", \"parameterName\": null, \"token\": \"1\"}"; + assertThatExceptionOfType(JacksonException.class) + .isThrownBy(() -> this.mapper.readValue(tokenJson, DefaultCsrfToken.class)); + } + +} diff --git a/webauthn/src/main/java/org/springframework/security/web/webauthn/jackson/AuthenticatorAttachmentDeserializer.java b/webauthn/src/main/java/org/springframework/security/web/webauthn/jackson/AuthenticatorAttachmentDeserializer.java index 895b594af6..46965fe508 100644 --- a/webauthn/src/main/java/org/springframework/security/web/webauthn/jackson/AuthenticatorAttachmentDeserializer.java +++ b/webauthn/src/main/java/org/springframework/security/web/webauthn/jackson/AuthenticatorAttachmentDeserializer.java @@ -16,13 +16,11 @@ package org.springframework.security.web.webauthn.jackson; -import java.io.IOException; - -import com.fasterxml.jackson.core.JacksonException; -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.databind.DeserializationContext; -import com.fasterxml.jackson.databind.deser.std.StdDeserializer; import org.jspecify.annotations.Nullable; +import tools.jackson.core.JacksonException; +import tools.jackson.core.JsonParser; +import tools.jackson.databind.DeserializationContext; +import tools.jackson.databind.deser.std.StdDeserializer; import org.springframework.security.web.webauthn.api.AuthenticatorAttachment; @@ -41,7 +39,7 @@ class AuthenticatorAttachmentDeserializer extends StdDeserializer