Reputation: 201
I would like to create a certificate-based authentication on top of websocket communication. So I created a websocket serverEndpoint, and set up SSL for client authentication with the help of jetty, like this:
Server server = new Server();
//Create SSL ContextFactory with appropriate attributes
SslContextFactory sslContextFactory = new SslContextFactory();
//Set up keystore path, truststore path, passwords, etc
...
sslContextFactory.setNeedClientAuth(true);
//Create the connector
ServerConnector localhostConnector = new ServerConnector(server, sslContextFactory);
localhostConnector.setHost(...);
localhostConnector.setPort(...);
server.addConnector(localhostConnector);
//Create ContextHandler
ServletContextHandler context = new ServletContextHandler(ServletContextHandler.SESSIONS);
context.setContextPath("/example");
server.setHandler(context);
// Initialize the JSR-356 layer and add custom Endpoints
ServerContainer container = WebSocketServerContainerInitializer.configureContext(context);
container.addEndpoint(Endpoint1.class); //Annotated class
container.addEndpoint(Endpoint2.class);
The SSL configuration seems to be correct, since I can connect to the different endpoints with a SSL client that I wrote (a wrong certificate leads to the connection beeing terminated).
Now, I would like to extract the information contained in the client certificate. I saw I could get the certificate from a SSLSession, but the only session I have access to in the endpoint is a "normal" Session:
@OnOpen
@Override
public void open(final Session session, final EndpointConfig config)
Is there a way somehow to store the certificate or the information contained and to pass it along to the endpoints ?
Thanks for any help :)
Upvotes: 1
Views: 998
Reputation: 201
I found a solution to get the client registered as the UserPrincipal of the session, accessible by session.getUserPrincipal()
.
The UserPricipal is "the authenticated user for the session". You nneed then to add an authentiation service to your ServletContextHandler, as following:
//Create SSL ContextFactory with appropriate attributes
...
//Create the connector
...
//Create ContextHandler
ServletContextHandler context = new ServletContextHandler(ServletContextHandler.SESSIONS);
context.setContextPath("/example");
//Add security contraint to the context => authentication
ConstraintSecurityHandler security = new ConstraintSecurityHandler();
Constraint constraint = new Constraint();
constraint.setName("auth");
constraint.setAuthenticate(true);
constraint.setRoles(new String[]{"user"});
Set<String> knownRoles = new HashSet<String>();
knownRoles.add("user");
ConstraintMapping mapping = new ConstraintMapping();
mapping.setPathSpec("/*");
mapping.setConstraint(constraint);
security.setConstraintMappings(Collections.singletonList(mapping), knownRoles);
security.setAuthMethod("CLIENT-CERT");
LoginService loginService = new HashLoginService();
security.setLoginService(loginService);
security.setAuthenticator(new ClientCertAuthenticator());
context.setSecurityHandler(security);
This way, when a client connects to the websocket endpoint, the security handler ensures that the client must be authenticated. As I understood, the ClientCertAuthenticator will check the client request to extract information (DN of the certificate) and then pass it to the LoginService, where the client is authenticated and the UserPricipal of the session set.
The problem here is that you must have a working loginService (For instance, HashLoginService is a in-memory Loginservice working with password and usernames, JDBCLoginService works with a database). For those who, like me, just want to extract the required information from the certificate and perform authentication afterwards with this information, you can provide your own implementation of the LoginService interface.
Here is what I did:
During the definition of your security Handler:
LoginService loginService = new CustomLoginService();
loginService.setIdentityService(new DefaultIdentityService());
security.setLoginService(loginService);
CustomLoginService Class
public class CustomLoginService implements LoginService {
IdentityService identityService = null;
@Override
public String getName() {
return "";
}
@Override
public UserIdentity login(String username, Object credentials) {
//you need to return a UserIdentity, which takes as argument:
// 1. A Subjet, containing a set of principals, a set of private credentials and a set of public ones (type Object)
// 2. A Principal of this Subject
// 3. A set of roles (String)
LdapPrincipal principal = null;
try {
principal = new LdapPrincipal(username);
//you need to have a Principal. I chose LDAP because it is specifically intended for user identified with a DN.
} catch (InvalidNameException e) {
e.printStackTrace();
}
String[] roles = new String[]{"user"};
return new DefaultUserIdentity(
new Subject(false,
new HashSet<LdapPrincipal>(Arrays.asList(new LdapPrincipal[]{principal}) ),
new HashSet<Object>(Arrays.asList(new Object[]{credentials})),
new HashSet<Object>(Arrays.asList(new Object[]{credentials}))),
principal,
roles);
}
@Override
public boolean validate(UserIdentity user) {
return false;
}
@Override
public IdentityService getIdentityService() {
return identityService;
}
@Override
public void setIdentityService(IdentityService service) {
identityService = service;
}
@Override
public void logout(UserIdentity user) {
}
And that's it :)
Upvotes: 1