From 848a5a4c9889047d723b462851a693373efe3271 Mon Sep 17 00:00:00 2001 From: hcphat Date: Thu, 14 May 2026 13:52:10 +0700 Subject: [PATCH 1/3] =?UTF-8?q?Ref=202.1.IdP=E3=81=8B=E3=82=89=E5=8F=97?= =?UTF-8?q?=E9=A0=98=E3=81=99=E3=82=8B=E8=AA=8D=E8=A8=BC=E6=83=85=E5=A0=B1?= =?UTF-8?q?=E8=A1=A8=E7=A4=BA=E3=81=AE=E6=97=A5=E6=9C=AC=E8=AA=9E=E6=96=87?= =?UTF-8?q?=E5=AD=97=E5=8C=96=E3=81=91=E8=A7=A3=E6=B6=88:=20commit=20code?= =?UTF-8?q?=20fix=20japanese=20encoding=20error?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...tRemoteUserNonInteractiveCredentialsAction.java | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/cas-server-support-osf/src/main/java/io/cos/cas/authentication/handler/support/OpenScienceFrameworkPrincipalFromRequestRemoteUserNonInteractiveCredentialsAction.java b/cas-server-support-osf/src/main/java/io/cos/cas/authentication/handler/support/OpenScienceFrameworkPrincipalFromRequestRemoteUserNonInteractiveCredentialsAction.java index c47bf3fb..fd4eadd5 100644 --- a/cas-server-support-osf/src/main/java/io/cos/cas/authentication/handler/support/OpenScienceFrameworkPrincipalFromRequestRemoteUserNonInteractiveCredentialsAction.java +++ b/cas-server-support-osf/src/main/java/io/cos/cas/authentication/handler/support/OpenScienceFrameworkPrincipalFromRequestRemoteUserNonInteractiveCredentialsAction.java @@ -372,15 +372,25 @@ protected OpenScienceFrameworkCredential constructCredential( for (final String headerName : Collections.list(request.getHeaderNames())) { if (headerName.startsWith(ATTRIBUTE_PREFIX)) { final String headerValue = request.getHeader(headerName); + String decodedValue; + if (headerValue == null) { + decodedValue = headerValue; + } else { + try { + decodedValue = new String(headerValue.getBytes("ISO-8859-1"), "UTF-8"); + } catch (final java.io.UnsupportedEncodingException e) { + decodedValue = headerValue; // UTF-8 is always supported, this never happens + } + } logger.debug( "[SAML Shibboleth] User's institutional identity '{}' - auth header '{}': '{}'", remoteUser, headerName, - headerValue + decodedValue ); credential.getDelegationAttributes().put( headerName.substring(ATTRIBUTE_PREFIX.length()), - headerValue + decodedValue ); } } From 31493c727538164ad055ea8dd21d1f5a3f28850d Mon Sep 17 00:00:00 2001 From: hcphat Date: Mon, 25 May 2026 15:31:48 +0700 Subject: [PATCH 2/3] =?UTF-8?q?Ref=202.1.IdP=E3=81=8B=E3=82=89=E5=8F=97?= =?UTF-8?q?=E9=A0=98=E3=81=99=E3=82=8B=E8=AA=8D=E8=A8=BC=E6=83=85=E5=A0=B1?= =?UTF-8?q?=E8=A1=A8=E7=A4=BA=E3=81=AE=E6=97=A5=E6=9C=AC=E8=AA=9E=E6=96=87?= =?UTF-8?q?=E5=AD=97=E5=8C=96=E3=81=91=E8=A7=A3=E6=B6=88:=20Commit=20UT=20?= =?UTF-8?q?update?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...rNonInteractiveCredentialsActionTests.java | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/cas-server-support-osf/src/test/java/io/cos/cas/authentication/handler/support/OpenScienceFrameworkPrincipalFromRequestRemoteUserNonInteractiveCredentialsActionTests.java b/cas-server-support-osf/src/test/java/io/cos/cas/authentication/handler/support/OpenScienceFrameworkPrincipalFromRequestRemoteUserNonInteractiveCredentialsActionTests.java index 2bd48c19..2306ffca 100644 --- a/cas-server-support-osf/src/test/java/io/cos/cas/authentication/handler/support/OpenScienceFrameworkPrincipalFromRequestRemoteUserNonInteractiveCredentialsActionTests.java +++ b/cas-server-support-osf/src/test/java/io/cos/cas/authentication/handler/support/OpenScienceFrameworkPrincipalFromRequestRemoteUserNonInteractiveCredentialsActionTests.java @@ -10,6 +10,7 @@ import java.util.ArrayList; import java.util.Collections; +import java.util.Enumeration; import java.util.List; import javax.security.auth.login.AccountException; @@ -301,4 +302,73 @@ public void verifyLoginAvailabilityExceptionFlow() throws Exception { .setInstitutionsLoginAvailabilityUrl(AbstractTestUtils.CONST_INSTITUTION_LOGIN_AVAILABILITY_URL); osfRemoteAuthenticate.notifyRemotePrincipalAuthenticated(osfCredential); } + + /** + * Verifies that a Shibboleth (SAML) authentication flow correctly handles an {@code AUTH-} prefixed request header + * whose value is {@code null}. + */ + @Test + public void verifyInstitutionSamlShibbolethFlowWithNullHeaderValue() throws Exception { + + // The name of the AUTH- prefixed header whose value will be forced to null. + final String nullAttributeHeaderName = "AUTH-NullAttribute"; + final String nullAttributeKey = "NullAttribute"; // stripped prefix + + // Build a MockHttpServletRequest that reports nullAttributeHeaderName in getHeaderNames() but + // returns null from getHeader() for that specific header, triggering the null-branch in the loop. + final MockHttpServletRequest mockHttpServletRequest = new MockHttpServletRequest() { + @Override + public String getHeader(final String name) { + if (nullAttributeHeaderName.equalsIgnoreCase(name)) { + return null; + } + return super.getHeader(name); + } + + @Override + public Enumeration getHeaderNames() { + final List names = new ArrayList<>(Collections.list(super.getHeaderNames())); + if (!names.contains(nullAttributeHeaderName)) { + names.add(nullAttributeHeaderName); + } + return Collections.enumeration(names); + } + }; + + // Add standard Shibboleth headers so the SAML Shibboleth branch is entered. + mockHttpServletRequest.addHeader("AUTH-Shib-Session-ID", AbstractTestUtils.CONST_NOT_EMPTY_STRING); + mockHttpServletRequest.addHeader("REMOTE_USER", AbstractTestUtils.CONST_NOT_EMPTY_STRING); + mockHttpServletRequest.addHeader("AUTH-Shib-Identity-Provider", AbstractTestUtils.CONST_INSTITUTION_IDP); + mockHttpServletRequest.addHeader("AUTH-displayName", AbstractTestUtils.CONST_DISPLAY_NAME); + mockHttpServletRequest.addHeader("AUTH-givenName", "James"); + mockHttpServletRequest.addHeader("AUTH-familyName", "Steward"); + mockHttpServletRequest.addHeader("AUTH-mail", AbstractTestUtils.CONST_MAIL); + + final MockRequestContext mockContext = AbstractTestUtils.getContextWithCredentials(mockHttpServletRequest); + + final CentralAuthenticationService centralAuthenticationService = mock(CentralAuthenticationService.class); + final MockNotifyRemotePrincipalAuthenticated osfRemoteAuthenticate + = new MockNotifyRemotePrincipalAuthenticated(centralAuthenticationService); + + final Event event = osfRemoteAuthenticate.doExecute(mockContext); + + final OpenScienceFrameworkCredential credential + = (OpenScienceFrameworkCredential) mockContext.getFlowScope().get(AbstractTestUtils.CONST_CREDENTIAL); + + // The flow must still complete successfully. + assertEquals("success", event.getId()); + assertTrue(credential.isRemotePrincipal()); + assertEquals(DelegationProtocol.SAML_SHIB, credential.getDelegationProtocol()); + + // The null-valued AUTH- header must appear in delegationAttributes with a null value, + // confirming that the `if (headerValue == null) { decodedValue = headerValue; }` branch was taken. + assertTrue( + "delegationAttributes must contain the key for the null-valued header", + credential.getDelegationAttributes().containsKey(nullAttributeKey) + ); + assertNull( + "delegationAttributes value for the null-valued header must be null", + credential.getDelegationAttributes().get(nullAttributeKey) + ); + } } From ce83ac8826f09196e26b9b76678a4e74d14759f9 Mon Sep 17 00:00:00 2001 From: hcphat Date: Wed, 27 May 2026 10:02:58 +0700 Subject: [PATCH 3/3] =?UTF-8?q?Ref=202.1.IdP=E3=81=8B=E3=82=89=E5=8F=97?= =?UTF-8?q?=E9=A0=98=E3=81=99=E3=82=8B=E8=AA=8D=E8=A8=BC=E6=83=85=E5=A0=B1?= =?UTF-8?q?=E8=A1=A8=E7=A4=BA=E3=81=AE=E6=97=A5=E6=9C=AC=E8=AA=9E=E6=96=87?= =?UTF-8?q?=E5=AD=97=E5=8C=96=E3=81=91=E8=A7=A3=E6=B6=88:=20Commit=20UT=20?= =?UTF-8?q?for=20verify=20re-encode=20Japanese=20text?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...rNonInteractiveCredentialsActionTests.java | 196 ++++++++++++++++++ 1 file changed, 196 insertions(+) diff --git a/cas-server-support-osf/src/test/java/io/cos/cas/authentication/handler/support/OpenScienceFrameworkPrincipalFromRequestRemoteUserNonInteractiveCredentialsActionTests.java b/cas-server-support-osf/src/test/java/io/cos/cas/authentication/handler/support/OpenScienceFrameworkPrincipalFromRequestRemoteUserNonInteractiveCredentialsActionTests.java index 2306ffca..64ea4cb3 100644 --- a/cas-server-support-osf/src/test/java/io/cos/cas/authentication/handler/support/OpenScienceFrameworkPrincipalFromRequestRemoteUserNonInteractiveCredentialsActionTests.java +++ b/cas-server-support-osf/src/test/java/io/cos/cas/authentication/handler/support/OpenScienceFrameworkPrincipalFromRequestRemoteUserNonInteractiveCredentialsActionTests.java @@ -371,4 +371,200 @@ public Enumeration getHeaderNames() { credential.getDelegationAttributes().get(nullAttributeKey) ); } + + /** + * Verifies that a Shibboleth (SAML) authentication flow correctly re-encodes ALL Japanese {@code AUTH-*} headers + * (displayName, givenName, familyName, organizationName / o, organizationalUnit / ou, + * jaOrganizationName / jao, jaOrganizationalUnitName / jaou) + * delivered as ISO-8859-1 mojibake back to the original UTF-8 strings. + * + *

Apache / the Servlet container reads HTTP header bytes and constructs Java {@code String} objects using + * ISO-8859-1 (Latin-1), which is the HTTP/1.1 default for header values. When the IdP sends UTF-8 encoded + * Japanese text, the bytes are misinterpreted and the resulting Java Strings are "mojibake". + * The fix re-encodes every AUTH-* header: {@code new String(headerValue.getBytes("ISO-8859-1"), "UTF-8")}.

+ */ + @Test + public void verifyInstitutionSamlShibbolethFlowWithJapaneseDisplayName() throws Exception { + + // Original Japanese values as they should appear after correct decoding. + final String originalDisplayName = "\u5c71\u7530 \u592a\u90ce"; // 山田 太郎 + final String originalGivenName = "\u592a\u90ce"; // 太郎 + final String originalFamilyName = "\u5c71\u7530"; // 山田 + final String originalOrganizationName = "\u5927\u962a\u5927\u5b66"; // 大阪大学 (o) + final String originalOrganizationalUnit = "\u7406\u5b66\u90e8"; // 理学部 (ou) + final String originalJaOrganizationName = "\u5927\u962a\u5927\u5b66"; // 大阪大学 (jao) + final String originalJaOrganizationalUnit = "\u7406\u5b66\u90e8"; // 理学部 (jaou) + + // Simulate the mojibake that Apache/Servlet creates: + // the IdP sends UTF-8 bytes; the container interprets them as ISO-8859-1 (Latin-1). + final String mojibakeDisplayName = new String(originalDisplayName.getBytes("UTF-8"), "ISO-8859-1"); + final String mojibakeGivenName = new String(originalGivenName.getBytes("UTF-8"), "ISO-8859-1"); + final String mojibakeFamilyName = new String(originalFamilyName.getBytes("UTF-8"), "ISO-8859-1"); + final String mojibakeOrganizationName = new String(originalOrganizationName.getBytes("UTF-8"), "ISO-8859-1"); + final String mojibakeOrganizationalUnit = new String(originalOrganizationalUnit.getBytes("UTF-8"), "ISO-8859-1"); + final String mojibakeJaOrganizationName = new String(originalJaOrganizationName.getBytes("UTF-8"), "ISO-8859-1"); + final String mojibakeJaOrganizationalUnit = new String(originalJaOrganizationalUnit.getBytes("UTF-8"), "ISO-8859-1"); + + final MockHttpServletRequest mockHttpServletRequest = new MockHttpServletRequest(); + mockHttpServletRequest.addHeader("AUTH-Shib-Session-ID", AbstractTestUtils.CONST_NOT_EMPTY_STRING); + mockHttpServletRequest.addHeader("REMOTE_USER", AbstractTestUtils.CONST_NOT_EMPTY_STRING); + mockHttpServletRequest.addHeader("AUTH-Shib-Identity-Provider", AbstractTestUtils.CONST_INSTITUTION_IDP); + mockHttpServletRequest.addHeader("AUTH-displayName", mojibakeDisplayName); + mockHttpServletRequest.addHeader("AUTH-givenName", mojibakeGivenName); + mockHttpServletRequest.addHeader("AUTH-familyName", mojibakeFamilyName); + mockHttpServletRequest.addHeader("AUTH-o", mojibakeOrganizationName); // organizationName + mockHttpServletRequest.addHeader("AUTH-ou", mojibakeOrganizationalUnit); // organizationalUnit + mockHttpServletRequest.addHeader("AUTH-jao", mojibakeJaOrganizationName); // jaOrganizationName + mockHttpServletRequest.addHeader("AUTH-jaou", mojibakeJaOrganizationalUnit); // jaOrganizationalUnitName + mockHttpServletRequest.addHeader("AUTH-mail", AbstractTestUtils.CONST_MAIL); + + final MockRequestContext mockContext = AbstractTestUtils.getContextWithCredentials(mockHttpServletRequest); + + final CentralAuthenticationService centralAuthenticationService = mock(CentralAuthenticationService.class); + final MockNotifyRemotePrincipalAuthenticated osfRemoteAuthenticate + = new MockNotifyRemotePrincipalAuthenticated(centralAuthenticationService); + + final Event event = osfRemoteAuthenticate.doExecute(mockContext); + + final OpenScienceFrameworkCredential credential + = (OpenScienceFrameworkCredential) mockContext.getFlowScope().get(AbstractTestUtils.CONST_CREDENTIAL); + + assertEquals("success", event.getId()); + assertTrue(credential.isRemotePrincipal()); + assertEquals(DelegationProtocol.SAML_SHIB, credential.getDelegationProtocol()); + + // All AUTH-* Japanese fields must be re-encoded from ISO-8859-1 mojibake to original UTF-8. + final String decodedDisplayName = (String) credential.getDelegationAttributes().get("displayName"); + final String decodedGivenName = (String) credential.getDelegationAttributes().get("givenName"); + final String decodedFamilyName = (String) credential.getDelegationAttributes().get("familyName"); + final String decodedOrganizationName = (String) credential.getDelegationAttributes().get("o"); + final String decodedOrganizationalUnit = (String) credential.getDelegationAttributes().get("ou"); + final String decodedJaOrganizationName = (String) credential.getDelegationAttributes().get("jao"); + final String decodedJaOrganizationalUnit = (String) credential.getDelegationAttributes().get("jaou"); + + assertEquals( + "AUTH-displayName must be re-encoded from mojibake to original Japanese UTF-8", + originalDisplayName, decodedDisplayName + ); + assertEquals( + "AUTH-givenName must be re-encoded from mojibake to original Japanese UTF-8", + originalGivenName, decodedGivenName + ); + assertEquals( + "AUTH-familyName must be re-encoded from mojibake to original Japanese UTF-8", + originalFamilyName, decodedFamilyName + ); + assertEquals( + "AUTH-o (organizationName) must be re-encoded from mojibake to original Japanese UTF-8", + originalOrganizationName, decodedOrganizationName + ); + assertEquals( + "AUTH-ou (organizationalUnit) must be re-encoded from mojibake to original Japanese UTF-8", + originalOrganizationalUnit, decodedOrganizationalUnit + ); + assertEquals( + "AUTH-jao (jaOrganizationName) must be re-encoded from mojibake to original Japanese UTF-8", + originalJaOrganizationName, decodedJaOrganizationName + ); + assertEquals( + "AUTH-jaou (jaOrganizationalUnitName) must be re-encoded from mojibake to original Japanese UTF-8", + originalJaOrganizationalUnit, decodedJaOrganizationalUnit + ); + } + + /** + * Verifies that a Shibboleth (SAML) authentication flow correctly re-encodes ALL Japanese {@code AUTH-*} headers + * (displayName, givenName, familyName, organizationName / o, organizationalUnit / ou, + * jaOrganizationName / jao, jaOrganizationalUnitName / jaou) + * when the display name contains a full-width organization prefix followed by a full-width Japanese personal name + * — a common real-world format from Japanese IdPs. + */ + @Test + public void verifyInstitutionSamlShibbolethFlowWithJapaneseMultibyteDisplayName() throws Exception { + + // Full-width organization prefix + full-width personal name (real-world IdP format). + final String originalDisplayName + = "\u56fd\u7acb\u60c5\u5831\u5b66\u7814\u7a76\u6240\u3000\u9234\u6728\u4e00\u90ce"; // 国立情報学研究所 鈴木一郎 + final String originalGivenName = "\u4e00\u90ce"; // 一郎 + final String originalFamilyName = "\u9234\u6728"; // 鈴木 + final String originalOrganizationName = "\u56fd\u7acb\u60c5\u5831\u5b66\u7814\u7a76\u6240"; // 国立情報学研究所 (o) + final String originalOrganizationalUnit = "\u30b3\u30f3\u30d4\u30e5\u30fc\u30bf\u79d1\u5b66"; // コンピュータ科学 (ou) + final String originalJaOrganizationName = "\u56fd\u7acb\u60c5\u5831\u5b66\u7814\u7a76\u6240"; // 国立情報学研究所 (jao) + final String originalJaOrganizationalUnit = "\u30b3\u30f3\u30d4\u30e5\u30fc\u30bf\u79d1\u5b66"; // コンピュータ科学 (jaou) + + // Simulate Apache / Servlet container mojibake: UTF-8 bytes read as ISO-8859-1. + final String mojibakeDisplayName = new String(originalDisplayName.getBytes("UTF-8"), "ISO-8859-1"); + final String mojibakeGivenName = new String(originalGivenName.getBytes("UTF-8"), "ISO-8859-1"); + final String mojibakeFamilyName = new String(originalFamilyName.getBytes("UTF-8"), "ISO-8859-1"); + final String mojibakeOrganizationName = new String(originalOrganizationName.getBytes("UTF-8"), "ISO-8859-1"); + final String mojibakeOrganizationalUnit = new String(originalOrganizationalUnit.getBytes("UTF-8"), "ISO-8859-1"); + final String mojibakeJaOrganizationName = new String(originalJaOrganizationName.getBytes("UTF-8"), "ISO-8859-1"); + final String mojibakeJaOrganizationalUnit = new String(originalJaOrganizationalUnit.getBytes("UTF-8"), "ISO-8859-1"); + + final MockHttpServletRequest mockHttpServletRequest = new MockHttpServletRequest(); + mockHttpServletRequest.addHeader("AUTH-Shib-Session-ID", AbstractTestUtils.CONST_NOT_EMPTY_STRING); + mockHttpServletRequest.addHeader("REMOTE_USER", AbstractTestUtils.CONST_NOT_EMPTY_STRING); + mockHttpServletRequest.addHeader("AUTH-Shib-Identity-Provider", AbstractTestUtils.CONST_INSTITUTION_IDP); + mockHttpServletRequest.addHeader("AUTH-displayName", mojibakeDisplayName); + mockHttpServletRequest.addHeader("AUTH-givenName", mojibakeGivenName); + mockHttpServletRequest.addHeader("AUTH-familyName", mojibakeFamilyName); + mockHttpServletRequest.addHeader("AUTH-o", mojibakeOrganizationName); + mockHttpServletRequest.addHeader("AUTH-ou", mojibakeOrganizationalUnit); + mockHttpServletRequest.addHeader("AUTH-jao", mojibakeJaOrganizationName); + mockHttpServletRequest.addHeader("AUTH-jaou", mojibakeJaOrganizationalUnit); + mockHttpServletRequest.addHeader("AUTH-mail", AbstractTestUtils.CONST_MAIL); + + final MockRequestContext mockContext = AbstractTestUtils.getContextWithCredentials(mockHttpServletRequest); + + final CentralAuthenticationService centralAuthenticationService = mock(CentralAuthenticationService.class); + final MockNotifyRemotePrincipalAuthenticated osfRemoteAuthenticate + = new MockNotifyRemotePrincipalAuthenticated(centralAuthenticationService); + + final Event event = osfRemoteAuthenticate.doExecute(mockContext); + + final OpenScienceFrameworkCredential credential + = (OpenScienceFrameworkCredential) mockContext.getFlowScope().get(AbstractTestUtils.CONST_CREDENTIAL); + + assertEquals("success", event.getId()); + assertTrue(credential.isRemotePrincipal()); + assertEquals(DelegationProtocol.SAML_SHIB, credential.getDelegationProtocol()); + + // All AUTH-* Japanese fields must be re-encoded from ISO-8859-1 mojibake to original UTF-8. + final String decodedDisplayName = (String) credential.getDelegationAttributes().get("displayName"); + final String decodedGivenName = (String) credential.getDelegationAttributes().get("givenName"); + final String decodedFamilyName = (String) credential.getDelegationAttributes().get("familyName"); + final String decodedOrganizationName = (String) credential.getDelegationAttributes().get("o"); + final String decodedOrganizationalUnit = (String) credential.getDelegationAttributes().get("ou"); + final String decodedJaOrganizationName = (String) credential.getDelegationAttributes().get("jao"); + final String decodedJaOrganizationalUnit = (String) credential.getDelegationAttributes().get("jaou"); + + assertEquals( + "AUTH-displayName with full-width org prefix must be re-encoded correctly to original Japanese UTF-8", + originalDisplayName, decodedDisplayName + ); + assertEquals( + "AUTH-givenName must be re-encoded from mojibake to original Japanese UTF-8", + originalGivenName, decodedGivenName + ); + assertEquals( + "AUTH-familyName must be re-encoded from mojibake to original Japanese UTF-8", + originalFamilyName, decodedFamilyName + ); + assertEquals( + "AUTH-o (organizationName) must be re-encoded from mojibake to original Japanese UTF-8", + originalOrganizationName, decodedOrganizationName + ); + assertEquals( + "AUTH-ou (organizationalUnit) must be re-encoded from mojibake to original Japanese UTF-8", + originalOrganizationalUnit, decodedOrganizationalUnit + ); + assertEquals( + "AUTH-jao (jaOrganizationName) must be re-encoded from mojibake to original Japanese UTF-8", + originalJaOrganizationName, decodedJaOrganizationName + ); + assertEquals( + "AUTH-jaou (jaOrganizationalUnitName) must be re-encoded from mojibake to original Japanese UTF-8", + originalJaOrganizationalUnit, decodedJaOrganizationalUnit + ); + } }