diff --git a/authhub/pom.xml b/authhub/pom.xml index 88db212f..6c4420b3 100644 --- a/authhub/pom.xml +++ b/authhub/pom.xml @@ -82,6 +82,18 @@ com.networknt correlation + + com.networknt + cluster + + + com.networknt + consul + + + com.networknt + balance + com.networknt service diff --git a/authhub/src/main/java/com/networknt/oauth/auth/LightPortalAuth.java b/authhub/src/main/java/com/networknt/oauth/auth/LightPortalAuth.java new file mode 100644 index 00000000..69f9a7d2 --- /dev/null +++ b/authhub/src/main/java/com/networknt/oauth/auth/LightPortalAuth.java @@ -0,0 +1,4 @@ +package com.networknt.oauth.auth; + +public interface LightPortalAuth { +} diff --git a/authhub/src/main/java/com/networknt/oauth/auth/LightPortalAuthenticator.java b/authhub/src/main/java/com/networknt/oauth/auth/LightPortalAuthenticator.java new file mode 100644 index 00000000..3939a6c8 --- /dev/null +++ b/authhub/src/main/java/com/networknt/oauth/auth/LightPortalAuthenticator.java @@ -0,0 +1,123 @@ +package com.networknt.oauth.auth; + +import com.networknt.client.Http2Client; +import com.networknt.cluster.Cluster; +import com.networknt.config.JsonMapper; +import com.networknt.oauth.security.LightPasswordCredential; +import com.networknt.server.Server; +import com.networknt.service.SingletonServiceFactory; +import io.undertow.UndertowOptions; +import io.undertow.client.ClientConnection; +import io.undertow.client.ClientRequest; +import io.undertow.client.ClientResponse; +import io.undertow.security.idm.Account; +import io.undertow.security.idm.Credential; +import io.undertow.util.Headers; +import io.undertow.util.Methods; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.xnio.OptionMap; + +import java.net.URI; +import java.net.URLEncoder; +import java.security.Principal; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicReference; + +/** + * This is the light-portal authentication class. It should be defined in the authenticate_class + * column in client table when register the light-portal application so that this class will be + * invoked when a user is login from light-portal site lightapi.net. + *

+ * The current implementation for light-portal will use portal user-query to do authentication and + * roles in the user profile for authorization. + * + * Unlike other corporate applications, there is only one type of user for the light-portal. + * + * The assumption is that the light-oauth2 and light-portal are deployed to the same cloud and register + * to the same Consul cluster. If that is not the case, then you cannot use service lookup but use direct + * url like lightapi.net/portal/query to access the portal query service. + * + * @author Steve Hu + */ +public class LightPortalAuthenticator extends AuthenticatorBase { + private static final Logger logger = LoggerFactory.getLogger(LightPortalAuthenticator.class); + private static final String cmd = "{\"host\":\"lightapi.net\",\"service\":\"user\",\"action\":\"loginUser\",\"version\":\"0.1.0\",\"data\":{\"email\":\"%s\",\"password\":\"%s\"}}"; + private static final String queryServiceId = "com.networknt.portal.hybrid.query-1.0.0"; + private static String tag = Server.getServerConfig().getEnvironment(); + // Get the singleton Http2Client instance + static Http2Client client = Http2Client.getInstance(); + static ClientConnection connection; + static Cluster cluster; + + public LightPortalAuthenticator() { + // Get the singleton Cluster instance + cluster = SingletonServiceFactory.getBean(Cluster.class); + String host = cluster.serviceToUrl("https", queryServiceId, tag, null); + try { + connection = client.connect(new URI(host), Http2Client.WORKER, Http2Client.SSL, Http2Client.BUFFER_POOL, OptionMap.create(UndertowOptions.ENABLE_HTTP2, true)).get(); + } catch (Exception e) { + logger.error("Exception:", e); + } + } + + @Override + public Account authenticate(String id, Credential credential) { + LightPasswordCredential passwordCredential = (LightPasswordCredential) credential; + char[] password = passwordCredential.getPassword(); + // user-query service authentication and authorization + try { + if(connection == null || !connection.isOpen()) { + // The connection is close or not created. + String host = cluster.serviceToUrl("https", queryServiceId, tag, null); + connection = client.connect(new URI(host), Http2Client.WORKER, Http2Client.SSL, Http2Client.BUFFER_POOL, OptionMap.create(UndertowOptions.ENABLE_HTTP2, true)).get(); + } + final CountDownLatch latch = new CountDownLatch(1); + final AtomicReference reference = new AtomicReference<>(); + final String s = String.format(cmd, id, new String(password)); + String message = "/portal/query?cmd=" + URLEncoder.encode(s, "UTF-8"); + final ClientRequest request = new ClientRequest().setMethod(Methods.GET).setPath(message); + request.getRequestHeaders().put(Headers.HOST, "localhost"); + connection.sendRequest(request, client.createClientCallback(reference, latch)); + latch.await(); + int statusCode = reference.get().getResponseCode(); + String body = reference.get().getAttachment(Http2Client.RESPONSE_BODY); + if(statusCode == 200) { + Map map = JsonMapper.string2Map(body); + // {"roles":"user","id":"stevehu@gmail.com"} + String roles = (String)map.get("roles"); + Account account = new Account() { + private Set roles = splitRoles((String)map.get("roles")); + private final Principal principal = () -> id; + @Override + public Principal getPrincipal() { + return principal; + } + + @Override + public Set getRoles() { + return roles; + } + }; + return account; + } + } catch (Exception e) { + logger.error("Exception:", e); + return null; + } + return null; + } + + public Set splitRoles(String roles) { + Set set = new HashSet<>(); + if(roles != null) { + String[] splited = roles.split("\\s+"); + set = new HashSet<>(Arrays.asList(splited)); + } + return set; + } +} diff --git a/authhub/src/test/java/com/networknt/oauth/auth/LightPortalAuthenticatorTest.java b/authhub/src/test/java/com/networknt/oauth/auth/LightPortalAuthenticatorTest.java new file mode 100644 index 00000000..cfa4fc4a --- /dev/null +++ b/authhub/src/test/java/com/networknt/oauth/auth/LightPortalAuthenticatorTest.java @@ -0,0 +1,44 @@ +package com.networknt.oauth.auth; + +import com.networknt.oauth.security.LightPasswordCredential; +import com.networknt.service.SingletonServiceFactory; +import io.undertow.security.idm.Account; +import org.junit.Assert; +import org.junit.Test; + +import java.util.Set; + +/** + * These are all live test case and they are depending on the light-portal hybrid-query running locally + * and registered on a local Consul server running in Docker container. In most of the cases, these + * test cases will be disabled. They are created for developers to debug the implementation. + * + * @author Steve Hu + */ +public class LightPortalAuthenticatorTest { + //@Test + public void testSplitRoles() { + String s = "user admin lightapi.net"; + LightPortalAuthenticator auth = new LightPortalAuthenticator(); + Set set = auth.splitRoles(s); + Assert.assertEquals(set.size(), 3); + Assert.assertTrue(set.contains("user")); + } + + /** + * Manually inject the authenticator and test with a constructed LightPasswordCredential. + */ + //@Test + public void testAuthenticate() { + Class clazz = DefaultAuth.class; + try { + clazz = Class.forName("com.networknt.oauth.auth.LightPortalAuth"); + } catch (ClassNotFoundException e) { + e.printStackTrace(); + } + Authenticator authenticator = SingletonServiceFactory.getBean(Authenticator.class, clazz); + Assert.assertTrue(authenticator != null); + Account account = authenticator.authenticate("stevehu@gmail.com", new LightPasswordCredential("123456".toCharArray(), null, null)); + Assert.assertTrue(account != null); + } +} diff --git a/authhub/src/test/resources/config/consul.yml b/authhub/src/test/resources/config/consul.yml new file mode 100644 index 00000000..73d01edc --- /dev/null +++ b/authhub/src/test/resources/config/consul.yml @@ -0,0 +1,32 @@ +# Consul URL for accessing APIs +consulUrl: http://192.168.1.144:8500 +# access token to the consul server +consulToken: the_one_ring +# number of requests before reset the shared connection. +maxReqPerConn: 1000000 +# deregister the service after the amount of time after health check failed. +deregisterAfter: 2m +# health check interval for TCP or HTTP check. Or it will be the TTL for TTL check. Every 10 seconds, +# TCP or HTTP check request will be sent. Or if there is no heart beat request from service after 10 seconds, +# then mark the service is critical. +checkInterval: 10s +# One of the following health check approach will be selected. Two passive (TCP and HTTP) and one active (TTL) +# enable health check TCP. Ping the IP/port to ensure that the service is up. This should be used for most of +# the services with simple dependencies. If the port is open on the address, it indicates that the service is up. +tcpCheck: false +# enable health check HTTP. A http get request will be sent to the service to ensure that 200 response status is +# coming back. This is suitable for service that depending on database or other infrastructure services. You should +# implement a customized health check handler that checks dependencies. i.e. if db is down, return status 400. +httpCheck: false +# enable health check TTL. When this is enabled, Consul won't actively check your service to ensure it is healthy, +# but your service will call check endpoint with heart beat to indicate it is alive. This requires that the service +# is built on top of light-4j and the above options are not available. For example, your service is behind NAT. +ttlCheck: true +# endpoints that support blocking will also honor a wait parameter specifying a maximum duration for the blocking request. +# This is limited to 10 minutes.This value can be specified in the form of "10s" or "5m" (i.e., 10 seconds or 5 minutes, +# respectively). +wait: 600s +# enable HTTP/2 +# must disable when using HTTP with Consul (mostly using local Consul agent), Consul only supports HTTP/1.1 when not using TLS +# optional to enable when using HTTPS with Consul, it will have better performance +enableHttp2: false diff --git a/authhub/src/test/resources/config/service.yml b/authhub/src/test/resources/config/service.yml index 81b9b72c..a000a84a 100644 --- a/authhub/src/test/resources/config/service.yml +++ b/authhub/src/test/resources/config/service.yml @@ -1,9 +1,27 @@ singletons: +- com.networknt.registry.URL: + - com.networknt.registry.URLImpl: + protocol: light + host: localhost + port: 8080 + path: consul + parameters: + registryRetryPeriod: '30000' +- com.networknt.consul.client.ConsulClient: + - com.networknt.consul.client.ConsulClientImpl +- com.networknt.registry.Registry: + - com.networknt.consul.ConsulRegistry +- com.networknt.balance.LoadBalance: + - com.networknt.balance.RoundRobinLoadBalance +- com.networknt.cluster.Cluster: + - com.networknt.cluster.LightCluster # Authenticator implementation mapping - com.networknt.oauth.auth.Authenticator: - com.networknt.oauth.auth.DefaultAuthenticator - com.networknt.oauth.auth.Authenticator: - com.networknt.oauth.auth.MarketPlaceAuthenticator +- com.networknt.oauth.auth.Authenticator: + - com.networknt.oauth.auth.LightPortalAuthenticator - javax.sql.DataSource: - com.zaxxer.hikari.HikariDataSource: DriverClassName: org.h2.jdbcx.JdbcDataSource