UnboundID: The entry contains attribute ‘XXXX’ which is not defined in the schema

PROBLEM

Let’s assume we have the following LDIF file containing custom attribute(s), such as managedBy:-

dn: dc=MyShittyCode
objectClass: top
objectClass: domain
dc: MyShittyCode

dn: CN=ShittyEmployee,DC=MyShittyCode
objectclass: top
objectclass: person
objectclass: organizationalPerson
objectclass: inetOrgPerson
sn: ShittyEmployee
managedBy: CN=ShittyBoss,DC=MyShittyCode

When running the code on UnboundID’s In-Memory Directory Server, the following exception is thrown:-

LDAPException(resultCode=65 (object class violation), 
errorMessage='Unable to add entry 'CN=ShittyEmployee,DC=MyShittyCode' 
because it violates the provided schema:  The entry contains attribute 
managedBy which is not defined in the schema.')
	at com.unboundid.ldap.listener.InMemoryRequestHandler.addEntry(InMemoryRequestHandler.java:4055)
	at com.unboundid.ldap.listener.InMemoryRequestHandler.importFromLDIF(InMemoryRequestHandler.java:3876)
	at com.unboundid.ldap.listener.InMemoryDirectoryServer.importFromLDIF(InMemoryDirectoryServer.java:1226)
	at com.unboundid.ldap.listener.InMemoryDirectoryServer.importFromLDIF(InMemoryDirectoryServer.java:1198)

SOLUTION

The problem is caused by the fact that the default schema does not match Microsoft’s Active Directory schema. Hence, attribute(s), such as managedBy, would cause an error.

While we can set a modified schema, which is very convoluted, the easiest solution is to completely disable the schema:-

def config = new InMemoryDirectoryServerConfig(base)
config.setListenerConfigs(new InMemoryListenerConfig("myListener", null, port, null, null, null))
config.setSchema(null)

def server = new InMemoryDirectoryServer(config)
server.startListening()
server.importFromLDIF(true, "target/test-classes/unboundid-test-data.ldif")

LdapTemplate: javax.naming.PartialResultException: Unprocessed Continuation Reference(s); remaining name ‘…’

BACKGROUND

Let’s assume we have the following LDAP configuration…

@Configuration
class LdapConfig {
    @Bean
    AuthenticationSource getAuthenticationSource(AppConfigService appConfigService) {
        return new AuthenticationSource() { ... }
    }

    @Bean
    ContextSource contextSource(AuthenticationSource authenticationSource) {
        return new LdapContextSource(
                authenticationSource: authenticationSource,
                url: 'ldap://server:389',
                base: 'dc=domain'
        )
    }

    @Bean
    LdapTemplate getLdapTemplate(ContextSource contextSource) {
        return new LdapTemplate(contextSource: contextSource)
    }
}

When running any LDAP query, the following exception is thrown:-

Caused by: javax.naming.PartialResultException: Unprocessed Continuation Reference(s); remaining name '/'
	at com.sun.jndi.ldap.LdapCtx.processReturnCode(LdapCtx.java:2846)
	at com.sun.jndi.ldap.LdapCtx.processReturnCode(LdapCtx.java:2820)
	at com.sun.jndi.ldap.LdapNamingEnumeration.getNextBatch(LdapNamingEnumeration.java:129)
	at com.sun.jndi.ldap.LdapNamingEnumeration.hasMoreImpl(LdapNamingEnumeration.java:198)
	at com.sun.jndi.ldap.LdapNamingEnumeration.hasMore(LdapNamingEnumeration.java:171)
	at org.springframework.ldap.core.LdapTemplate.search(LdapTemplate.java:365)

SOLUTION

There are 3 solutions to this problem.

Query against Gobal Catalog

To prevent the referral issues when dealing with Active Directory, we may query against the Global Catalog by using port 3268.

@Bean
ContextSource contextSource(AuthenticationSource authenticationSource) {
    return new LdapContextSource(
            authenticationSource: authenticationSource,
            url: 'ldap://server:3268',
            base: 'dc=domain'
    )
}

The possible downside to this approach is the Global Catalog may not have the pertinent data we need, such as employeeID, etc.

Configure Referral to Follow

We can configure LdapTemplate to automatically follow any referrals.

@Bean
ContextSource contextSource(AuthenticationSource authenticationSource) {
    return new LdapContextSource(
            authenticationSource: authenticationSource,
            url: 'ldap://server:389',
            base: 'dc=domain',
            referral: 'follow'
    )
}

The downside to this approach is it makes the query much slower. Based on my testing, it is at least 5 to 10 seconds slower.

Ignore Exception

Sometimes, it pays to read the JavaDoc. Based on the LdapTemplate’s documentation, it says…

Note for Active Directory (AD) users: AD servers are apparently unable to handle referrals automatically, which causes a PartialResultException to be thrown whenever a referral is encountered in a search. To avoid this, set the ignorePartialResultException property to true. There is currently no way of manually handling these referrals in the form of ReferralException, i.e. either you get the exception (and your results are lost) or all referrals are ignored (if the server is unable to handle them properly. Neither is there any simple way to get notified that a PartialResultException has been ignored (other than in the log).

Bada Bing, Bada Boom…

@Bean
LdapTemplate getLdapTemplate(ContextSource contextSource) {
    return new LdapTemplate(
            contextSource: contextSource,
            ignorePartialResultException: true
    )
}

LdapTemplate: AttributesMapper vs ContextMapper

BACKGROUND

When using Spring’s LdapTemplate, there are two ways to transform the queried results: AttributesMapper and ContextMapper.

List<MyBean> list = ldapTemplate.search(
    '',
    '(cn=some-group-name)',
    // AttributesMapper or ContextMapper 
)

Here’s the comparison between these mapper classes.

AttributesMapper

If you are migrating your existing LDAP queries to Spring’s LdapTemplate, AttributesMapper seems ideal because you can copy most of the code over because it provides javax.naming.directory.Attributes.

List<MyBean> list = ldapTemplate.search(
    '',
    '(cn=some-group-name)',
    new AttributesMapper<MyBean>() {
        @Override
        MyBean mapFromAttributes(final Attributes attributes) throws NamingException {
            return new MyBean(
                cn: attributes.get('cn')?.get(),
                members: attributes.get('member')?.getAll()?.toSet() as Set<String> ?: []
            )
        }
    }
)

However, you have to handle possible null values if the attribute keys do not exist.

ContextMapper

With ContextMapper, it handles null values for us. Spring also provides an abstract class called AbstractContextMapper to further simplify the code.

List<MyBean> list = ldapTemplate.search(
    '',
    '(cn=some-group-name)',
    new AbstractContextMapper<MyBean>() {
        @Override
        protected MyBean doMapFromContext(final DirContextOperations ctx) {
            return new MyBean(
                cn: ctx.getStringAttribute('cn'),
                members: ctx.getStringAttributes('member')
            )
        }
    }
)

Spring: Component Scan Selected Classes

PROBLEM

Let’s assume we have a package with the following classes where each class is either annotated with Spring’s @Service, @Component, @Controller or @Repository.

app
├── A.groovy
├── B.groovy
├── C.groovy
├── D.groovy
└── E.groovy

When writing unit test, we want Spring to component scan class A and class B.

SOLUTION

Before we begin, we configure Log4j to log Spring in debug level.

<logger name="org.springframework">
    <level value="debug"/>
</logger>

Step 1

If we configure the test class like this…

@ContextConfiguration
class ASpec extends Specification {
    @Configuration
    @ComponentScan(
            basePackageClasses = [A]
	)
    static class TestConfig {
    }

    def "..."() {
        // ...
    }
}

It will scan all Spring components that reside in the same package as class A.

Debugging log:-

[DEBUG] [ClassPathBeanDefinitionScanner] [findCandidateComponents:294] - Identified candidate component class: file [/path/target/classes/app/A.class]
[DEBUG] [ClassPathBeanDefinitionScanner] [findCandidateComponents:294] - Identified candidate component class: file [/path/target/classes/app/B.class]
[DEBUG] [ClassPathBeanDefinitionScanner] [findCandidateComponents:294] - Identified candidate component class: file [/path/target/classes/app/C.class]
[DEBUG] [ClassPathBeanDefinitionScanner] [findCandidateComponents:294] - Identified candidate component class: file [/path/target/classes/app/D.class]
[DEBUG] [ClassPathBeanDefinitionScanner] [findCandidateComponents:294] - Identified candidate component class: file [/path/target/classes/app/E.class]

Step 2

We can set includeFilters to include just class A and class B…

@ContextConfiguration
class ASpec extends Specification {
    @Configuration
    @ComponentScan(
            basePackageClasses = [A],
            includeFilters = [@ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, value = [A, B])]
	)
    static class TestConfig {
    }

    def "..."() {
        // ...
    }
}

… but it doesn’t do anything.

Debugging log:-

[DEBUG] [ClassPathBeanDefinitionScanner] [findCandidateComponents:294] - Identified candidate component class: file [/path/target/classes/app/A.class]
[DEBUG] [ClassPathBeanDefinitionScanner] [findCandidateComponents:294] - Identified candidate component class: file [/path/target/classes/app/B.class]
[DEBUG] [ClassPathBeanDefinitionScanner] [findCandidateComponents:294] - Identified candidate component class: file [/path/target/classes/app/C.class]
[DEBUG] [ClassPathBeanDefinitionScanner] [findCandidateComponents:294] - Identified candidate component class: file [/path/target/classes/app/D.class]
[DEBUG] [ClassPathBeanDefinitionScanner] [findCandidateComponents:294] - Identified candidate component class: file [/path/target/classes/app/E.class]

Step 3

To fix this, we set useDefaultFilters to false to disable any automatic detection of classes annotated with Spring’s @Service, @Component, @Controller or @Repository.

@ContextConfiguration
class ASpec extends Specification {
    @Configuration
    @ComponentScan(
            basePackageClasses = [A],
            useDefaultFilters = false,
            includeFilters = [@ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, value = [A, B])]
    )
    static class TestConfig {
    }

    def "..."() {
        // ...
    }
}

Now, we get the intended behavior.

Debugging log:-

[DEBUG] [ClassPathBeanDefinitionScanner] [findCandidateComponents:294] - Identified candidate component class: file [/path/target/classes/app/A.class]
[DEBUG] [ClassPathBeanDefinitionScanner] [findCandidateComponents:294] - Identified candidate component class: file [/path/target/classes/app/B.class]

JAXB2: Adding toString() to Generated Java Classes

PROBLEM

By default, the generated Java class prints the memory address when toString() is invoked.

However, sometimes it is helpful to have a more meaningful toString() for debugging purposes.

SOLUTION

To fix this, configure maven-jaxb2-plugin to generate toString() based on the fields in the class:-

<project ...>
  ...
  
  <properties>
    <org.jvnet.jaxb2_commons.version>0.11.1</org.jvnet.jaxb2_commons.version>
  </properties>
  
  <dependencies>
    <dependency>
      <groupId>org.jvnet.jaxb2_commons</groupId>
      <artifactId>jaxb2-basics-runtime</artifactId>
      <version>${org.jvnet.jaxb2_commons.version}</version>
    </dependency>
  </dependencies>
  
  <build>
    <plugins>
      <plugin>
        <groupId>org.jvnet.jaxb2.maven2</groupId>
        <artifactId>maven-jaxb2-plugin</artifactId>
        <version>0.13.1</version>
        <executions>
          <execution>
            <goals>
              <goal>generate</goal>
            </goals>
          </execution>
        </executions>
        <configuration>
          <schemaLanguage>WSDL</schemaLanguage>
          <generatePackage>my.package.wsdl</generatePackage>
          <bindingDirectory>${project.basedir}/src/main/resources</bindingDirectory>
          <bindingIncludes>
            <include>jaxb-binding.xjb</include>
          </bindingIncludes>
          <schemas>
            <schema>
              <fileset>
                <directory>${project.basedir}/src/main/resources</directory>
                <includes>
                  <include>web-service.wsdl</include>
                </includes>
              </fileset>
            </schema>
          </schemas>
          <args>
            <arg>-XtoString</arg>
          </args>
          <plugins>
            <plugin>
              <groupId>org.jvnet.jaxb2_commons</groupId>
              <artifactId>jaxb2-basics</artifactId>
              <version>${org.jvnet.jaxb2_commons.version}</version>
            </plugin>
          </plugins>
        </configuration>
      </plugin>
    </plugins>
  </build>
</project>

Git + Linux: (gnome-ssh-askpass:24871): Gtk-WARNING **: cannot open display:

PROBLEM

When running git clone on Linux, the following error appears:-

-bash-4.1$ git clone http://user@tfs:8080/tfs/my-institution/my-domain/_git/my-project
Initialized empty Git repository in /people/my-group/user/my-project/.git/

(gnome-ssh-askpass:24871): Gtk-WARNING **: cannot open display:

SOLUTION

To fix this, run the following command to disable gnome-ssh-askpass:-

-bash-4.1$ unset SSH_ASKPASS

Now, git clone will work just fine:-

-bash-4.1$ git clone http://user@tfs:8080/tfs/my-institution/my-domain/_git/my-project
Initialized empty Git repository in /people/my-group/user/my-project/.git/
Password:
remote:
remote:                    fTfs
remote:                  fSSSSSSSs
remote:                fSSSSSSSSSS
remote: TSSf         fSSSSSSSSSSSS
remote: SSSSSF     fSSSSSSST SSSSS
remote: SSfSSSSSsfSSSSSSSt   SSSSS
remote: SS  tSSSSSSSSSs      SSSSS
remote: SS   fSSSSSSST       SSSSS
remote: SS fSSSSSFSSSSSSf    SSSSS
remote: SSSSSST    FSSSSSSFt SSSSS
remote: SSSSt        FSSSSSSSSSSSS
remote:                FSSSSSSSSSS
remote:                  FSSSSSSs
remote:                    FSFs    (TM)
remote:
remote:  Microsoft (R) Visual Studio (R) Team Foundation Server
remote:
Receiving objects: 100% (504/504), 1.05 MiB, done.
Resolving deltas: 100% (138/138), done.

To prevent this from happening again, add unset SSH_ASKPASS command to .bashrc file.

Spring Security SAML: Replacing SHA-1 with SHA-256 on Signature and Digest Algorithms

PROBLEM

By default, Spring Security SAML’s SAMLBootstrap uses SHA1withRSA for signature algorithm and SHA-1 for digest algorithm.

@Configuration
@EnableWebSecurity
public abstract class AppSAMLConfig extends WebSecurityConfigurerAdapter {
	...

    @Bean
    public static SAMLBootstrap SAMLBootstrap() {
        return new SAMLBootstrap();
    }
	
	...
}

For example, the above configuration will generate the following SAML request payload when using HTTP-POST binding:-

<?xml version="1.0" encoding="UTF-8"?>
<saml2p:AuthnRequest
    AssertionConsumerServiceURL="https://server/app/saml/SSO"
    Destination="https://adfs-server/adfs/ls/" ForceAuthn="true"
    ID="a3bj4e05i70f6946gi85299i51i02a" IsPassive="false"
    IssueInstant="2016-02-23T15:10:26.414Z"
    ProtocolBinding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
    Version="2.0" xmlns:saml2p="urn:oasis:names:tc:SAML:2.0:protocol">
    <saml2:Issuer xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion">https://server/app/saml/metadata</saml2:Issuer>
    <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
        <ds:SignedInfo>
            <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
            <ds:SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>
            <ds:Reference URI="#a3bj4e05i70f6946gi85299i51i02a">
                <ds:Transforms>
                    <ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
                    <ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
                </ds:Transforms>
                <ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
                <ds:DigestValue>u25hV7rk8hIpXYLJQs0aZjkueP0=</ds:DigestValue>
            </ds:Reference>
        </ds:SignedInfo>
        <ds:SignatureValue>YDR9ybi...</ds:SignatureValue>
        <ds:KeyInfo>
            <ds:X509Data>
                <ds:X509Certificate>MIICxz...</ds:X509Certificate>
            </ds:X509Data>
        </ds:KeyInfo>
    </ds:Signature>
    <saml2p:RequestedAuthnContext Comparison="exact">
        <saml2:AuthnContextClassRef xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion">urn:oasis:names:tc:SAML:2.0:ac:classes:Password</saml2:AuthnContextClassRef>
    </saml2p:RequestedAuthnContext>
</saml2p:AuthnRequest>

Unfortunately, SHA-1 is now deemed insecure due to “Freestart Collision” attack.

Further, most modern browsers have ceased to trust SHA-1 code signing certificates starting January 2016 and will eventually stop accepting these certificates by January 2017.

SOLUTION

To fix this, we could replace SHA-1 with stronger secure hash algorithm, such as SHA-256.

To do so, create a class that extends SAMLBootstrap that uses SHA256withRSA for signature algorithm and SHA-256 for digest algorithm.

public final class CustomSAMLBootstrap extends SAMLBootstrap {
    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        super.postProcessBeanFactory(beanFactory);
        BasicSecurityConfiguration config = (BasicSecurityConfiguration) Configuration.getGlobalSecurityConfiguration();
        config.registerSignatureAlgorithmURI("RSA", SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA256);
        config.setSignatureReferenceDigestMethod(SignatureConstants.ALGO_ID_DIGEST_SHA256);
    }
}

Then, return CustomSAMLBootstrap instead of SAMLBootstrap

@Configuration
@EnableWebSecurity
public abstract class AppSAMLConfig extends WebSecurityConfigurerAdapter {
	...

    @Bean
    public static SAMLBootstrap SAMLBootstrap() {
        return new CustomSAMLBootstrap();
    }
	
	...
}

Now, the generated SAML request payload using HTTP-POST binding looks like this:-

<?xml version="1.0" encoding="UTF-8"?>
<saml2p:AuthnRequest
    AssertionConsumerServiceURL="https://server/app/saml/SSO"
    Destination="https://adfs-server/adfs/ls/" ForceAuthn="true"
    ID="a2e7f98agfaec7d253714fjdbcf8a83" IsPassive="false"
    IssueInstant="2016-02-23T15:18:43.452Z"
    ProtocolBinding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
    Version="2.0" xmlns:saml2p="urn:oasis:names:tc:SAML:2.0:protocol">
    <saml2:Issuer xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion">https://server/app/saml/metadata</saml2:Issuer>
    <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
        <ds:SignedInfo>
            <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
            <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
            <ds:Reference URI="#a2e7f98agfaec7d253714fjdbcf8a83">
                <ds:Transforms>
                    <ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
                    <ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
                </ds:Transforms>
                <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
                <ds:DigestValue>w4qHFsBxFGifzemEJCYcuGOt+oZJ9N2DQM+Q2aEqJFI=</ds:DigestValue>
            </ds:Reference>
        </ds:SignedInfo>
        <ds:SignatureValue>YDR9ybi...</ds:SignatureValue>
        <ds:KeyInfo>
            <ds:X509Data>
                <ds:X509Certificate>MIICxz...</ds:X509Certificate>
            </ds:X509Data>
        </ds:KeyInfo>
    </ds:Signature>
    <saml2p:RequestedAuthnContext Comparison="exact">
        <saml2:AuthnContextClassRef xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion">urn:oasis:names:tc:SAML:2.0:ac:classes:Password</saml2:AuthnContextClassRef>
    </saml2p:RequestedAuthnContext>
</saml2p:AuthnRequest>