jetty / jetty.project

Eclipse Jetty® - Web Container & Clients - supports HTTP/2, HTTP/1.1, HTTP/1.0, websocket, servlets, and more
https://eclipse.dev/jetty
Other
3.86k stars 1.91k forks source link

Is there an example for ConfigurableSpnegoLoginService? #4982

Closed xresch closed 4 years ago

xresch commented 4 years ago

Jetty version 9.4.15

Java version 1.8

Question The Class SpnegoLoginService is marked as deprecated and it says it should be replaced with ConfigurableSpnegoLoginService. I started to implement it but had troubles as the Constructor asks for an instance of the interface AuthorizationService, that I don't know how to implement.

Do you have any example on how to use ConfigurableSpnegoLoginService and ConfigurableSpnegoAuthenticator?

My goal is to authenticate against Kerberos and get the principals username and other details like firstname, lastname and email.

Below is what I tried so far based on some code I found on this stackoverflow question: https://stackoverflow.com/questions/27427654/how-to-use-embedded-jetty-server-9-with-kerberos-authentication

public static ConstraintSecurityHandler createSPNegoSecurityHandler() throws Exception {

    System.setProperty("javax.security.auth.useSubjectCredsOnly", "false");
    System.setProperty("java.security.auth.login.config", "./config/kerberos/spnego.conf");
    System.setProperty("java.security.krb5.conf", "./config/kerberos/krb5.ini");

    String domainRealm = "MY.COM";

    Constraint constraint = new Constraint();
    constraint.setName(Constraint.__SPNEGO_AUTH);
    constraint.setRoles(new String[]{domainRealm});
    constraint.setAuthenticate(true);

    ConstraintMapping cm = new ConstraintMapping();
    cm.setConstraint(constraint);
    cm.setPathSpec("/*");

    ConfigurableSpnegoLoginService loginService = new ConfigurableSpnegoLoginService("realm", ???AuthorizationService???));

    loginService.setKeyTabPath(Paths.get(new URI("./config/kerberos/cfw.keytab")));
    loginService.setServiceName("HTTP");
    loginService.setHostName("example.com");

    ConstraintSecurityHandler sh = new ConstraintSecurityHandler();
    sh.setAuthenticator(new SpnegoAuthenticator());
    sh.setLoginService(loginService);
    sh.setConstraintMappings(new ConstraintMapping[]{cm});
    sh.setRealmName(domainRealm);

    return sh;
}
ajleetch89 commented 4 years ago

I have the same issue. There is no example of how to use this new class ConfigurableSpnegoLoginService along with AuthorizationService.

I tried new ConfigurableSpnegoLoginService("realm", AuthorizationService.from(null, null)));

But it doesn't work :(

joakime commented 4 years ago

AuthorizationService is a way to get a UserIdentity from a the current HttpServletRequest and username.

When you use AuthorizationService.from(LoginService, Object) you use the LoginService implementation of your choice to establish a lambda that will perform the login. Note: LoginService.login(String username, Object credentials, ServletRequest request) is used when you call AuthorizationService.from().

For list of LoginService choices, see https://www.eclipse.org/jetty/javadoc/current/org/eclipse/jetty/security/LoginService.html

Example of usage:

String realm = "my.site.org";
Path realmPropsPath = Paths.get("/path/to/realm.properties");
HashLoginService authorizationService = new HashLoginService(realm, realmPropsPath.toString());
ConfigurableSpnegoLoginService loginService = new ConfigurableSpnegoLoginService(realm, AuthorizationService.from(authorizationService, ""));

Which is perfectly reasonable configuration.

ajleetch89 commented 4 years ago

I'm trying to upgrade my current code which is working fine with Jetty 9.4.12.v20180830, to the new one - Jetty 9.4.27.v20200227 (SpnegoAuthenticator and SpnegoLoginService are deprecated)

ConstraintSecurityHandler securityHandler = new ConstraintSecurityHandler();
securityHandler.setAuthenticator(new SpnegoAuthenticator());
securityHandler.setLoginService(new SpnegoLoginService("realm", "/path/to/spnego.properties"));
securityHandler.setRealmName("realm");
securityHandler.setConstraintMappings(mappings);
ServletContextHandler contextHandler = server.getBean(ServletContextHandler.class);
contextHandler.setSecurityHandler(securityHandler);

The new code is not working at all

SpnegoLoginService authorizationService = new SpnegoLoginService("realm", "/path/to/spnego.properties");
ConfigurableSpnegoLoginService loginService = new ConfigurableSpnegoLoginService(properties.getDomainRealm(), AuthorizationService.from(authorizationService, ""));
loginService.setKeyTabPath(Paths.get("/path/to/keytab-file"));
loginService.setServiceName("HTTP");
loginService.setHostName("host");
ConstraintSecurityHandler securityHandler = new ConstraintSecurityHandler();
securityHandler.setAuthenticator(new ConfigurableSpnegoAuthenticator());
securityHandler.setLoginService(loginService);
securityHandler.setRealmName("realm");
securityHandler.setConstraintMappings(mappings);
ServletContextHandler contextHandler = server.getBean(ServletContextHandler.class);
contextHandler.setSecurityHandler(securityHandler);

And BTW SpnegoLoginService which I'm using in the AuthorizationService is deprecated.

Would appreciate any help.

Thanks, -AJ

sbordet commented 4 years ago

Example usages can be found in this test case:

https://github.com/eclipse/jetty.project/blob/jetty-9.4.30.v20200611/jetty-client/src/test/java/org/eclipse/jetty/client/util/SPNEGOAuthenticationTest.java

Look especially at the prepare() method to setup the KDC, and at the startSPNEGO() method to setup Jetty on the server side. The Jetty client side configuration is present in every test.

Note that the test case interoperates correctly with Apache Kerby so it should interoperate with any other compliant KDC implementation.

joakime commented 4 years ago

in the AuthorizationService is deprecated.

org.eclipse.jetty.security.authentication.AuthorizationService is not deprecated.

https://github.com/eclipse/jetty.project/blob/jetty-9.4.30.v20200611/jetty-security/src/main/java/org/eclipse/jetty/security/authentication/AuthorizationService.java

ajleetch89 commented 4 years ago

AuthorizationService is not deprecated but SpnegoLoginService does, and I'm using it in the AuthorizationService.from....

SpnegoLoginService authorizationService = new SpnegoLoginService("realm", "/path/to/spnego.properties");
ConfigurableSpnegoLoginService loginService = new ConfigurableSpnegoLoginService("realm", AuthorizationService.from(authorizationService, ""));
joakime commented 4 years ago

The SpnegoLoginService was deprecated as part of the work on Issue #2868

ajleetch89 commented 4 years ago

Example usages can be found in this test case:

https://github.com/eclipse/jetty.project/blob/jetty-9.4.30.v20200611/jetty-client/src/test/java/org/eclipse/jetty/client/util/SPNEGOAuthenticationTest.java

Look especially at the prepare() method to setup the KDC, and at the startSPNEGO() method to setup Jetty on the server side. The Jetty client side configuration is present in every test.

Note that the test case interoperates correctly with Apache Kerby so it should interoperate with any other compliant KDC implementation.

Thanks, but in my case I'm not using properties file with usernames/pwd. I want to use GSSAPI Negotiation, since user already authenticated via AD (windows ntlm).

I don't know but it might be important, code is running on Solaris 10. Again no issues with old code - works perfectly.

ajleetch89 commented 4 years ago

The SpnegoLoginService was deprecated as part of the work on Issue #2868

Thanks. So then what LoginService should I use in AuthorizationService.from(...), to kick off GSSAPI Negotiation...

joakime commented 4 years ago
SpnegoLoginService authorizationService = new SpnegoLoginService("realm", "/path/to/spnego.properties");
ConfigurableSpnegoLoginService loginService = new ConfigurableSpnegoLoginService("realm", AuthorizationService.from(authorizationService, ""));

That's non-sensical code. You can't have ConfigurableSpnegoLoginService be based on SpnegoLoginService. It's a loop with no authorization service, no source for user identities, and no roles.

Your AuthorizationService needs to be something that handles the realm and login for you, which isn't Spnego.

Your old code did this ...

securityHandler.setLoginService(new SpnegoLoginService("realm", "/path/to/spnego.properties"));

That's a SpnegoLoginService with a realm called "realm" and a spnego.properties that simply sets the "targetName" to whatever you have/need it for (which you configured for {serviceName}@{hostName}). That old SpnegoLoginService still needed an IdentityService, and your code snippets do not show you setting one. It has to be there, otherwise you have no UserIdentities, no roles, no successful logins, no Spnego established, zilch. Pretty much an environment with no real constraints applied on it. My bet is that you have a LDAP source declared as your IdentityService.

In ConfiguredSpnegoLoginService you still have to declare the realm, serviceName, and hostName, but not via a properties file (which you have done). But now you have to declare the LoginService implementation you want to use for the AuthorizationService handling (where the roles and UserIdentities come from). You have many choices, you have to pick one, and not rely on any kind of "default" or "unset" behavior (like the old SpnegoLoginService). As pointed out in a prior comment, you have many LoginService implementations to choose from. See "All Known Implementing Classes" at https://www.eclipse.org/jetty/javadoc/current/org/eclipse/jetty/security/LoginService.html Pick one that best fits your needs. Examples: a realm properties file, a JDBC database, a DataSource, a JAAS source (like ldap), an OpenID service, etc...

sbordet commented 4 years ago

@ajleetch89 please show the code and configuration that works, and then the code and configuration that does not work, separately.

Also, you don't really want to do GSS yourself - I think you just need to find the configuration that works in your case.

ajleetch89 commented 4 years ago

@ajleetch89 please show the code and configuration that works, and then the code and configuration that does not work, separately.

Also, you don't really want to do GSS yourself - I think you just need to find the configuration that works in your case.

Exactly !!

sbordet commented 4 years ago

Exactly !!

Well, so far we don't understand what you're trying to do - "does not work" is not very helpful. Nor we have seen clear examples of your code. Nor we have seen DEBUG logs.

ajleetch89 commented 4 years ago
new ConfigurableSpnegoLoginService

That's what I'm trying to figure how use ConfigurableSpnegoLoginService, considering what was "default"/"unset" in the previous SpnegoLoginService. Since I was simple just set it up like this:

SpnegoLoginService authorizationService = new SpnegoLoginService("realm", "/path/to/spnego.properties");

I have no idea what is the "default"/"unset" IdentityService been used. I'm running embbeded Jetty with Spring Boot.

sbordet commented 4 years ago

@ajleetch89 you continue to post half-lines of random code, and we don't understand what you're talking about.

How about, for a change, you post the full code that was working for you before, and the full code that you are writing now? How about you also attach full DEBUG logs so we can see what is not working?

ajleetch89 commented 4 years ago

Exactly !!

Well, so far we don't understand what you're trying to do - "does not work" is not very helpful. Nor we have seen clear examples of your code. Nor we have seen DEBUG logs.

    @Override
    public void customize(JettyServletWebServerFactory factory) {
        factory.addServerCustomizers((Server server) -> {
            List<ConstraintMapping> mappings = new LinkedList<>();

            Constraint portal = ConstraintSecurityHandler.createConstraint(Constraint.__SPNEGO_AUTH, true, new String[]{properties.getDomainRealm()}, Constraint.DC_CONFIDENTIAL);
            ConstraintMapping portalMapping = new ConstraintMapping();
            portalMapping.setConstraint(portal);
            portalMapping.setPathSpec("/*");
            mappings.add(portalMapping);

            ConstraintSecurityHandler securityHandler = new ConstraintSecurityHandler();
            securityHandler.setAuthenticator(new SpnegoAuthenticator());
            securityHandler.setLoginService(new SpnegoLoginService("realm" "/path-to-spengo"));
            securityHandler.setRealmName("realm");
            securityHandler.setConstraintMappings(mappings);
            ServletContextHandler contextHandler = server.getBean(ServletContextHandler.class);
            contextHandler.setSecurityHandler(securityHandler);
        });
    }

Who to make it work with new classes ConfigurableSpnegoLoginService and ConfigurableSpnegoAuthenticator.

joakime commented 4 years ago

That has no IdentityService, where is the IdentityService coming from in your environment?

Since you mentioned SpringBoot, it could be coming from dozens of different locations, including SpringBoot itself. You should probably start debugging the working configuration and figure out what your SpnegoLoginService.getIdentityService() returns AFTER it has been started (not while it's being configured).

ajleetch89 commented 4 years ago

That has no IdentityService, where is the IdentityService coming from in your environment?

Since you mentioned SpringBoot, it could be coming from dozens of different locations, including SpringBoot itself. You should probably start debugging the working configuration and figure out what your SpnegoLoginService.getIdentityService() returns AFTER it has been started (not while it's being configured).

It's DefaultIdentityService. I don't explicitly set it in the code. Jetty set it in doStart() after sometime.

joakime commented 4 years ago

DefaultIdentityService is basically a no-op IdentityService used for tests/testing. Your "working configuration" has no roles, has no real security, and performs no login against spnego. To be fair, this isn't wholly accurate, it did ask GSS for a new server credentials, but those server credential has no meaning / value to a user without a valid login to spengo, and an IdentityService to supply the roles for your users.

Your "working configuration" has the following features non-functional.

This scenario is one of many that caused the change to ConfiguredSpnegoLoginService, making it mandatory to declare how the login/user identity works in the constructor.

The old SpnegoLoginService required you to declare a valid IdentityService that pointed to something for the login (like ldap) for it to actually do something. But that was unclear and many folks (including yourself) didn't bother to setup the IdentityService properly.

Now you have to work with ConfiguredSpnegoLoginService, and it needs a login mechanism (aka a LoginService) to obtain the usernames / roles from, which is wrapped in the ConfiguredSpnegoLoginService via a lambda called AuthorizationService (which calls this LoginService for each attempt to login via spnego). You have to choose where these roles associated to usernames come from. That LoginService you choose will be used to perform the login and obtain the usernames to roles mappings that the Servlet side needs.

To say this a different way, the following steps happend for working with spnego in servlet.

  1. Establish the GSS manager
  2. Establish the GSSName from the configured serviceName and hostName
  3. Establish a new server GSSCredentials from the GSSName
  4. Establish a new GSSContext from the server GSSCredentials
  5. Ask the GSSContext to Accept the user auth token
  6. Create the Spnego User Identity
  7. Obtain the roles associated with the user
  8. Create the Servlet User Identity

In your "working configuration" you stop at step 4 because of the DefaultIdentityService, and you result in an no-op User Identity with no roles or abilities. Which doesn't allow the Constraint you have to be applied in allow or deny mode, so it also results in a no-op.

In the ConfiguredSpnegoLoginService configuration you need to declare a LoginService that handles step 7 and 8.

ajleetch89 commented 4 years ago

I use J2eePreAuthenticatedProcessingFilter in Spring Boot, and the main goal of Spnego/Kerberos in mine Jetty setup to get user principal, since user already authenticated thru windows ntlm. After final negotiation round Jetty/SPNEGO have user principal, I use to lookup datastore where it mapped to specific application role.

joakime commented 4 years ago

Sounds like what you are looking for is a generic spnego library, not a spnego implementation of the servlet spec authentication and authorization layers.

Jetty provides the Servlet spec Authentication and Authorization layers.

The fact that you are using Constraint, ConstraintMapping, and ConstraintSecurityHandler means you want this Servlet spec authentication and authorization layers, not a generic spnego library. All of those require properly defined roles at the time that those specifically execute, which you are not providing with either your old "working configuration" or the new ConfiguredSpnegoLoginService.

Also, all of the Servlet defined authentication and authorization configurations apply before the servlet filter chain. So your J2eePreAuthenticatedProcessingFilter would execute after the Constraint is applied to the incoming request.

xresch commented 4 years ago

Hi joakime and sbordet,

as I started this thread I want to thank you both for all the answers so far. I understood that my approach seems not the correct way to achieve what I need. I have a similar use case as ajleetch89, my employeer asks for the ability to use Kerberos authentication, but the only thing I actually need to fetch from the Kerberos Authentication is the user principal as everything else like roles are already managed in the DB.

As security as quite a topic and you seem to have already a loot of professional knowledge in this area, and I'm not too deep into it in present time, can you give me a hint about the main classes I need to look into to implement the "Servlet spec Authentication and Authorization layers" to get it working with Kerberos and Jetty?

Thanks and Regards Reto

xresch commented 4 years ago

... if you already have a working example that would be helpful too.

joakime commented 4 years ago

I have a similar use case as ajleetch89, my employeer asks for the ability to use Kerberos authentication, but the only thing I actually need to fetch from the Kerberos Authentication is the user principal as everything else like roles are already managed in the DB.

This sounds like ConfiguredSpnegoLoginService (the authentication piece) configured with a AuthorizationService based on JDBCLoginService (the authorization piece where the user principal and roles come from).

xresch commented 4 years ago

Thanks, checked JDBCLoginService, seems not really what I want but I helped me, I will most probably create a custom login service based on that one.

With the following code I'm able to get prompted by the browser for credentials:

private ConstraintSecurityHandler createSPNEGOSecurityHandler() throws Exception {

     System.setProperty("javax.security.auth.useSubjectCredsOnly", "false");
        System.setProperty("java.security.auth.login.config", "./config/kerberos/spnego.conf");
        System.setProperty("java.security.krb5.conf", "./config/kerberos/krb5.conf");
        System.setProperty("sun.security.krb5.debug", "true");
        System.setProperty("sun.security.jgss.debug", "true");
        System.setProperty("java.security.debug", "all");

        String domainRealm = "EXAMPLE.COM";

        CFWLoginService authorizationService = new CFWLoginService();

        ConfigurableSpnegoLoginService loginService = new ConfigurableSpnegoLoginService(domainRealm, AuthorizationService.from(authorizationService, ""));
        loginService.addBean(authorizationService);
        loginService.setKeyTabPath(Paths.get("./config/kerberos/cfw.keytab"));
        loginService.setServiceName("ldap");
        loginService.setHostName("example.net");
        server.addBean(loginService);

        ConstraintSecurityHandler securityHandler = new ConstraintSecurityHandler();

        Constraint constraint = new Constraint();
        constraint.setName(Constraint.__SPNEGO_AUTH);
        constraint.setRoles(new String[]{domainRealm});
        constraint.setAuthenticate(true);

        ConstraintMapping mapping = new ConstraintMapping();
        mapping.setPathSpec("/app/*");
        mapping.setConstraint(constraint);
        securityHandler.addConstraintMapping(mapping);

        ConfigurableSpnegoAuthenticator authenticator = new ConfigurableSpnegoAuthenticator();
        securityHandler.setAuthenticator(authenticator);
        securityHandler.setLoginService(loginService);

        return securityHandler;
}

While CFWLoginService is currently just a stub class extending AbstractLoginService :

public class CFWLoginService extends AbstractLoginService {

    @Override
    protected String[] loadRoleInfo(UserPrincipal user) {
        System.out.println("loadRoleInfo:"+user.toString());
        return null;
    }

    @Override
    protected UserPrincipal loadUserInfo(String username) {
        System.out.println("loadUserInfo:"+username);
        UserPrincipal principal = new UserPrincipal(username, Credential.getCredential(""));
        return principal;
    }

}

As of now I end up with the following exception, so above CFWLoginService is never reached:

GSSException: Defective token detected (Mechanism level: GSSHeader did not find the right tag)

But I think my question is answered so far. Thanks for the support and keep up the good work.

Regards Reto

joakime commented 4 years ago

If you are making a custom LoginService, look at the HashLoginService, it's probably the easiest one to follow (and it also uses AbstractLoginService which does most of the heavy lifting).

Nulls are rarely appropriate for Constraints to work. So make sure you are populating roles at a minimum. (the roles are used in the Constraint you define) Also, the UserPrincipal.authenticate() methods need to function, so that means your Credential needs to be sane.

joakime commented 4 years ago

Alternatively, if you want to make it even simpler, just implement a custom AuthorizationService and skip the LoginService delegation entirely.

package jetty;

import java.security.Principal;
import javax.security.auth.Subject;
import javax.servlet.http.HttpServletRequest;

import org.eclipse.jetty.security.DefaultUserIdentity;
import org.eclipse.jetty.security.authentication.AuthorizationService;
import org.eclipse.jetty.server.UserIdentity;

public class CustomAuthorizationService implements AuthorizationService
{
    /**
     * @param request the current HTTP request
     * @param name the user name
     * @return a {@link UserIdentity} to query for roles of the given user
     */
    @Override
    public UserIdentity getUserIdentity(HttpServletRequest request, String name)
    {
        Subject subject = ...; // TODO
        Principal userPrincipal = ...; // TODO
        String[] roles = ...; // TODO

        return new DefaultUserIdentity(subject, userPrincipal, roles);
    }
}
sbordet commented 4 years ago

@xresch please have a look at https://www.roguelynn.com/words/explain-like-im-5-kerberos/.

Jetty's HttpClient does the client-side steps indicated in the link above. If you use a browser, the browser should do those steps as well.

In either case, the client will have a SGT (Service Granting Ticket) for the HTTP server you want to send requests to.

On the server-side, Jetty must be configured with SPNEGO, similarly to what you have done above, so that it can decrypt the SGT, typically configuring a keyTab for the service. Note also that the service name and the service host play an important role here, but really depends on the Kerberos system you are using - you want to play with those, and see again the test case where the format must be serviceName/serviceHost.

Then you make an HTTP request; client and server exchange a number (1 or more) HTTP requests to authenticate with each other; when they are happy, the server has the client user name, which is passed to AuthorizationService.getUserIdentity(...) to get a UserIdentity, so that it can fulfill the Servlet APIs.

What remains is for you to protect your resources and allow only certain roles to access them, typically in a web.xml or using embedded code like shown in the test case linked above.

janbartel commented 4 years ago

I'm closing this issue, as the OP's question appears to have been answered adequately - if this is not the case, please reopen with new information.