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