This article introduces you to securing Netty applications using Elytron security framework, which is a core component of WildFly application server.
Pre-requisites:
- Firstly, you should already know the basics of Netty client-server framework. If you are new to it, check the following article to learn more: Getting started with Netty
- Then, you should be familiar with Elytron security framework. Check this article for a quick introduction to it: Creating an Elytron Security Realm for WildFly
Setting up the Netty Project
The example project we will discuss here is available in the elytron examples repository. To set up a secured Netty server we will add the following components:
- An InboundHandler which allows to handle a specific type of messages. In this example, we will be handling incoming HttpObject.
- A Channel Initializer: This is a special handler whose purpose is to initialize the connection applying the Security layer in the Channel pipeline.
- A Main class which bootstraps the Netty Server, adding as Child Handler the Channel Initializer, which in turns contains a reference to Elytron Security domain
Let’s start checking each single component, starting from the Main class.
Coding the BootStrap Class
To bootstrap a Netty server, you need to use a ServerBootstrap Class. The difference from a plain text Netty server is that, in this example, we are adding a Security Handler as Child Handler.
To create the Security Handler we use the ElytronHandlers which is backed by a SimpleMapBackedSecurityRealm:
public class HelloWorld { private static final WildFlyElytronProvider elytronProvider = new WildFlyElytronProvider(); private static final int PORT = 7776; /** * @param args */ public static void main(String[] args) throws Exception { System.out.println("Here we go"); SecurityDomain securityDomain = createSecurityDomain(); securityDomain.registerWithClassLoader(HelloWorld.class.getClassLoader()); EventLoopGroup parentGroup = new NioEventLoopGroup(1); EventLoopGroup childGroup = new NioEventLoopGroup(1); ElytronHandlers securityHandlers = ElytronHandlers.newInstance() .setSecurityDomain(securityDomain) .setFactory(createHttpAuthenticationFactory()) .setMechanismConfigurationSelector(MechanismConfigurationSelector.constantSelector( MechanismConfiguration.builder() .addMechanismRealm(MechanismRealmConfiguration.builder().setRealmName("Elytron Realm").build()) .build())); ServerBootstrap bootstrap = new ServerBootstrap(); bootstrap.option(ChannelOption.SO_BACKLOG, 1024); bootstrap.group(parentGroup, childGroup) .channel(NioServerSocketChannel.class) .handler(new LoggingHandler(LogLevel.INFO)) .childHandler(new TestInitialiser(securityHandlers)); bootstrap.bind(PORT).sync(); } private static HttpServerAuthenticationMechanismFactory createHttpAuthenticationFactory() { HttpServerAuthenticationMechanismFactory factory = new SecurityProviderServerMechanismFactory(() -> new Provider[] {elytronProvider}); return new FilterServerMechanismFactory(factory, true, "BASIC"); } private static SecurityDomain createSecurityDomain() throws Exception { // Create an Elytron map-backed security realm SimpleMapBackedSecurityRealm simpleRealm = new SimpleMapBackedSecurityRealm(() -> new Provider[] { elytronProvider }); Map<String, SimpleRealmEntry> identityMap = new HashMap<>(); // Add user alice identityMap.put("alice", new SimpleRealmEntry(getCredentialsForClearPassword("alice123+"), getAttributesForRoles("employee", "admin"))); // Add user bob identityMap.put("bob", new SimpleRealmEntry(getCredentialsForClearPassword("bob123+"), getAttributesForRoles("employee"))); simpleRealm.setIdentityMap(identityMap); // Add the map-backed security realm to a new security domain's list of realms SecurityDomain.Builder builder = SecurityDomain.builder() .addRealm("ExampleRealm", simpleRealm).build() .setPermissionMapper((principal, roles) -> PermissionVerifier.from(new LoginPermission())) .setDefaultRealmName("ExampleRealm"); return builder.build(); } private static List<Credential> getCredentialsForClearPassword(String clearPassword) throws Exception { PasswordFactory passwordFactory = PasswordFactory.getInstance(ALGORITHM_CLEAR, elytronProvider); return Collections.singletonList(new PasswordCredential(passwordFactory.generatePassword(new ClearPasswordSpec(clearPassword.toCharArray())))); } private static MapAttributes getAttributesForRoles(String... roles) { MapAttributes attributes = new MapAttributes(); HashSet<String> rolesSet = new HashSet<>(); if (roles != null) { for (String role : roles) { rolesSet.add(role); } } attributes.addAll(RoleDecoder.KEY_ROLES, rolesSet); return attributes; } }
If you want to switch to another kind of Realm, update the method createSecurityDomain accordingly. For example, here is how to create a FileSystemSecurityRealm:
FileSystemSecurityRealm fsRealm = new FileSystemSecurityRealm(fsRealmPath, NameRewriter.IDENTITY_REWRITER, 2, true);
Coding the Content Handler
Next, we will be adding the InBoundHandler which reads the incoming HTTP Connections and produces a Response. The response is an FullHttpResponse which also includes the Security’s Principal Name:
class TestContentHandler extends SimpleChannelInboundHandler<HttpObject> { private static final AsciiString CONTENT_TYPE = AsciiString.cached("Content-Type"); private static final AsciiString CONTENT_LENGTH = AsciiString.cached("Content-Length"); private static final AsciiString CONNECTION = AsciiString.cached("Connection"); private static final AsciiString KEEP_ALIVE = AsciiString.cached("keep-alive"); TestContentHandler() { System.out.println("TestContentHandler:new"); } @Override public void channelReadComplete(ChannelHandlerContext ctx) { System.out.println("TestContentHandler:channelReadComplete"); ctx.flush(); } @Override protected void channelRead0(ChannelHandlerContext ctx, HttpObject msg) throws Exception { System.out.println("TestContentHandler:read0"); if (msg instanceof HttpRequest) { HttpRequest req = (HttpRequest) msg; boolean keepAlive = HttpUtil.isKeepAlive(req); final String identity = getElytronIdentity(); FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, OK, Unpooled.wrappedBuffer(getContent(identity))); response.headers().set(CONTENT_TYPE, "text/plain"); response.headers().setInt(CONTENT_LENGTH, response.content().readableBytes()); if (!keepAlive) { ctx.write(response).addListener(ChannelFutureListener.CLOSE); } else { response.headers().set(CONNECTION, KEEP_ALIVE); ctx.write(response); } } else { System.out.println("TestContentHandler - Not a HttpRequest"); } } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { cause.printStackTrace(); ctx.close(); } private byte[] getContent(final String identity) { return String.format("Current identity '%s'", identity).getBytes(StandardCharsets.UTF_8); } private String getElytronIdentity() { SecurityDomain securityDomain = SecurityDomain.getCurrent(); if (securityDomain != null) { SecurityIdentity securityIdentity = securityDomain.getCurrentSecurityIdentity(); if (securityIdentity != null) { return securityIdentity.getPrincipal().getName(); } } return null; } }
Coding the ChannelInitializer
Finally, to bootstrap our Netty Server we need a ChannelInitializer. The ChannelInitializer contains a Pipeline of Handlers. In this example, we are adding at the end of the Pipeline the Handler from our previous section:
class TestInitialiser extends ChannelInitializer<SocketChannel> { private final Function<ChannelPipeline, ChannelPipeline> securityHandler; TestInitialiser(final Function<ChannelPipeline, ChannelPipeline> securityHandler) { this.securityHandler = securityHandler; } @Override protected void initChannel(SocketChannel ch) throws Exception { ChannelPipeline pipeline = ch.pipeline(); pipeline.addLast(new HttpServerCodec()); pipeline.addLast(new HttpServerExpectContinueHandler()); securityHandler.apply(pipeline); pipeline.addLast(new TestContentHandler()); } }
Testing the example
The example project includes a Maven Java exec plugin, therefore you can build and run it as follows:
mvn clean install exec:exec
As you can see from the following snapshot, the servers starts and binds Netty to all available network addresses on port 7776:
Then, from the browser or the command line, use one of the available identities to reach the Netty Server. For example:
$ curl -u alice:alice123+ http://localhost:7776 Current identity 'alice'
Conclusion
That’s all. This article was a quick walk through an example of a Netty Server which uses an Elytron Security Domain in the Pipiline of its Socket Handlers