1

I´m tying to add two servlets to an embedded tomcat. In this scenario one of the servlets should be "protected" by basic authentication. I want to add the security constraints via code only. Regarding to this Link it shouldn´t be to hard.

I build a test scenario:

Project: EmbeddedTomcatTest

-Source Packages
--tomcat.test
---ServletOne.java
---ServletTwo.java
---StartEmbeddedTomcat.java (contains main method)
-Test Packages
-Other Sources
--src/main/resources
---Tomcat-users.xml
-Project Files
--pom.xml

The dependencies of my pom looks like:

  <dependencies>
       <dependency>
            <groupId>org.apache.tomcat.embed</groupId>
            <artifactId>tomcat-embed-core</artifactId>
            <version>8.5.4</version>
        </dependency>
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <version>3.1.0</version>
        </dependency>
   </dependencies>   

So far I added an embedded tomcat with two servlets which are accessible via different url patterns, which works fine.

public class StartEmbeddedTomcat {
  private static final String AUTH_ROLE = "test";

  public static void main(String[] args) throws LifecycleException {
    Tomcat tomcat = new Tomcat();
    tomcat.setPort(8080);
    // adding a context
    Context ctx = tomcat.addContext("/", new File(".").getAbsolutePath());

    // Login Config
    LoginConfig config = new LoginConfig();
    config.setAuthMethod("BASIC");    

    // adding constraint with role "test"
    SecurityConstraint constraint = new SecurityConstraint();
    constraint.addAuthRole(AUTH_ROLE);

    // add constraint to a collection with pattern /second  
    SecurityCollection collection = new SecurityCollection();
    collection.addPattern("/second");
    constraint.addCollection(collection);

    ctx.setLoginConfig(config);
    // does the context need a auth role too?
    ctx.addSecurityRole(AUTH_ROLE);
    ctx.addConstraint(constraint);

    // add servlet with pattenr /first and /second
    Tomcat.addServlet(ctx, "one", new ServletOne());
    ctx.addServletMapping("/first", "one");

    Tomcat.addServlet(ctx, "two", new ServletTwo());
    ctx.addServletMapping("/second", "two");

    // add tomcat users to realm
    String path = "tomcat-users.xml";
    URL uri = ServletOne.class.getClassLoader().getResource(path);
    MemoryRealm realm = new MemoryRealm();
    realm.setPathname(uri.toString());
    tomcat.getEngine().setRealm(realm);
    tomcat.start();
    tomcat.getServer().await();
  }
}

The tomcat-users.xml isn´t very special.

<tomcat-users>
  <role rolename="manager-gui"/>
  <role rolename="test"/>

  <user name="admin" password="" roles="manager-gui"/>
  <user name="test" password="test" roles="test"/>
</tomcat-users>

I think the code of the servlets is not important since they just response "Im servlet one" or "Im servlet two".

In my opinion the whole application or at least every request onto /second should be protected. What did I do wrong? Is there something more I´ve to do?

----edit---

Today I found this question of stack overflow. I added noauthCTX.setAltDDName("Path\\to\\web.xml"); to my test scenario. The web.xml looks like:

<web-app>
    <display-name>Test Service</display-name>   
    <servlet>
        <servlet-name>ServletOne</servlet-name>
        <servlet-class>tomcat.test.test.ServletOne</servlet-class>
        <load-on-startup>1</load-on-startup>
    </servlet>   
    <servlet-mapping>
        <servlet-name>ServletOne</servlet-name>
        <url-pattern>/servletOne/*</url-pattern>
    </servlet-mapping>
</web-app>

I thought my servlet one would be accessable via localhost:8080/servletOne/* too. It isn´t.. actually I´m very confused. I hope someone can help me..

Community
  • 1
  • 1
monti
  • 455
  • 7
  • 25

2 Answers2

2

Probably it's a little bit late to answer this question but it may be useful for others. Recently I faced similar problem when tried to configure security for embedded Tomcat with SpringBoot, security constraints simply didn't work and my Realm wasn't invoked at all. The problem turned out to be in org.apache.catalina.authenticator.SSLAuthenticator, this valve was not added to Tomcat pipeline and thus SecurityConstraints weren't checked.

Adding tomcatFactory.addContextValves(new SSLAuthenticator()); solved the problem.

If you need Basic authentication valve class is

org.apache.catalina.authenticator.BasicAuthenticator

Full working config for two-way SSL and security realm on embedded tomcat:

Java Config

@Configuration
public class EmbeddedTomcatConfiguration {

    private static final String LOGIN_CONFIG_AUTH_METHOD = "CLIENT-CERT";

    @Bean
    public RealmBase createPkiRealm() {
        //Create your custom realm or use some standard one here
        return realm;
    }

    @Bean
    public EmbeddedServletContainerCustomizer createTomcatCustomizer(RealmBase pkiRealm) {
        return (container) -> {
            TomcatEmbeddedServletContainerFactory tomcatFactory = (TomcatEmbeddedServletContainerFactory) container;
            //tomcatFactory.addAdditionalTomcatConnectors(createSslConnector()); //connector is configured in application.properties
            tomcatFactory.addContextCustomizers(createTomcatContextCustomizer(pkiRealm));
            tomcatFactory.addEngineValves(new SingleSignOn());
            tomcatFactory.addContextValves(new SSLAuthenticator()); //this is where PKI realm is invoked
        };
    }

    private TomcatContextCustomizer createTomcatContextCustomizer(RealmBase pkiRealm) {
        return (context) ->  {
            context.setRealm(pkiRealm);
            context.setLoginConfig(createLoginConfig());
            context.addSecurityRole("new_role");
            context.addConstraint(createSecurityConstraint());
        };
    }

    private LoginConfig createLoginConfig() {
        LoginConfig config = new LoginConfig();
        config.setAuthMethod(LOGIN_CONFIG_AUTH_METHOD);
        return config;
    }

    private SecurityConstraint createSecurityConstraint() {
        SecurityConstraint securityConstraint = new SecurityConstraint();
        securityConstraint.setDisplayName("Employee certificate required");
        SecurityCollection securityCollection = new SecurityCollection("sec_collection");
        securityCollection.addPattern("/test/*");
        securityConstraint.addCollection(securityCollection);
        securityConstraint.addAuthRole("new_role");
        securityConstraint.setAuthConstraint(true);
        securityConstraint.setUserConstraint("CONFIDENTIAL");
        return securityConstraint;
    }

}

application.properties with new SSL connector config

server.port=8443

server.ssl.enabled=true
server.ssl.client-auth=need

server.ssl.key-store=classpath:certs/serverkeystore.p12
server.ssl.key-store-password=changeit
server.ssl.key-store-type=PKCS12
server.ssl.key-alias=serverkey

server.ssl.trust-store=classpath:certs/servertruststore.jks
server.ssl.trust-store-password=changeit
server.ssl.trust-store-type=jks

With standalone Tomcat and same config written in xml no additional SSLAuthenticator valve is needed.

troy
  • 704
  • 8
  • 16
0

The problem is that you are trying to secure a context. A context is not securable, i.e.,

tomcat.addContext("/", new File(".").getAbsolutePath());

Must be changed to

tomcat.addWebapp("/", new File(".").getAbsolutePath());

You will have to deal with some exception handling, because the adding of a new web-app can throw a ServletException, but nevertheless it should work afterwards as you expect.

If you don't want to load the defaults (i.e., MIME type mappings, static-content servlet, JSP, ...) you can simply override the getDefaultWebXmlListener method.

tomcat = new Tomcat() {

    @Override
    public LifecycleListener getDefaultWebXmlListener() {
        return event -> {};
    }
};
Philipp
  • 1,289
  • 1
  • 16
  • 37