diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/AbstractActionInput.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/AbstractActionInput.java
index 3f6e6c95..9fd77e29 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/AbstractActionInput.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/AbstractActionInput.java
@@ -28,8 +28,8 @@ import com.kingsrook.qqq.backend.core.actions.async.AsyncJobStatus;
import com.kingsrook.qqq.backend.core.exceptions.QInstanceValidationException;
import com.kingsrook.qqq.backend.core.instances.QInstanceValidator;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
+import com.kingsrook.qqq.backend.core.model.metadata.authentication.QAuthenticationMetaData;
import com.kingsrook.qqq.backend.core.model.session.QSession;
-import com.kingsrook.qqq.backend.core.modules.authentication.metadata.QAuthenticationMetaData;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QAuthenticationType.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QAuthenticationType.java
index 0550ee66..9ae00fbd 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QAuthenticationType.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QAuthenticationType.java
@@ -29,6 +29,7 @@ package com.kingsrook.qqq.backend.core.model.metadata;
public enum QAuthenticationType
{
AUTH_0("auth0"),
+ TABLE_BASED("tableBased"),
FULLY_ANONYMOUS("fullyAnonymous"),
MOCK("mock");
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QInstance.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QInstance.java
index 147ca32c..482ca631 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QInstance.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QInstance.java
@@ -34,6 +34,7 @@ import com.kingsrook.qqq.backend.core.instances.QInstanceValidationKey;
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput;
import com.kingsrook.qqq.backend.core.model.actions.metadata.MetaDataInput;
import com.kingsrook.qqq.backend.core.model.actions.metadata.MetaDataOutput;
+import com.kingsrook.qqq.backend.core.model.metadata.authentication.QAuthenticationMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.automation.QAutomationProviderMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.branding.QBrandingMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaDataInterface;
@@ -48,7 +49,6 @@ import com.kingsrook.qqq.backend.core.model.metadata.queues.QQueueMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.queues.QQueueProviderMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
-import com.kingsrook.qqq.backend.core.modules.authentication.metadata.QAuthenticationMetaData;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import io.github.cdimascio.dotenv.Dotenv;
import io.github.cdimascio.dotenv.DotenvEntry;
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/metadata/Auth0AuthenticationMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/authentication/Auth0AuthenticationMetaData.java
similarity index 88%
rename from qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/metadata/Auth0AuthenticationMetaData.java
rename to qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/authentication/Auth0AuthenticationMetaData.java
index 274a4f3f..e0d46f5e 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/metadata/Auth0AuthenticationMetaData.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/authentication/Auth0AuthenticationMetaData.java
@@ -19,11 +19,13 @@
* along with this program. If not, see .
*/
-package com.kingsrook.qqq.backend.core.modules.authentication.metadata;
+package com.kingsrook.qqq.backend.core.model.metadata.authentication;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.kingsrook.qqq.backend.core.model.metadata.QAuthenticationType;
+import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModuleDispatcher;
+import com.kingsrook.qqq.backend.core.modules.authentication.implementations.Auth0AuthenticationModule;
/*******************************************************************************
@@ -49,6 +51,11 @@ public class Auth0AuthenticationMetaData extends QAuthenticationMetaData
{
super();
setType(QAuthenticationType.AUTH_0);
+
+ //////////////////////////////////////////////////////////
+ // ensure this module is registered with the dispatcher //
+ //////////////////////////////////////////////////////////
+ QAuthenticationModuleDispatcher.registerModule(QAuthenticationType.AUTH_0.getName(), Auth0AuthenticationModule.class.getName());
}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/metadata/QAuthenticationMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/authentication/QAuthenticationMetaData.java
similarity index 98%
rename from qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/metadata/QAuthenticationMetaData.java
rename to qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/authentication/QAuthenticationMetaData.java
index 7e259de5..3f0b1bdb 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/metadata/QAuthenticationMetaData.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/authentication/QAuthenticationMetaData.java
@@ -19,7 +19,7 @@
* along with this program. If not, see .
*/
-package com.kingsrook.qqq.backend.core.modules.authentication.metadata;
+package com.kingsrook.qqq.backend.core.model.metadata.authentication;
import java.util.HashMap;
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/authentication/TableBasedAuthenticationMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/authentication/TableBasedAuthenticationMetaData.java
new file mode 100644
index 00000000..be60a2d7
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/authentication/TableBasedAuthenticationMetaData.java
@@ -0,0 +1,454 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2022. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.core.model.metadata.authentication;
+
+
+import com.kingsrook.qqq.backend.core.model.metadata.QAuthenticationType;
+import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
+import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
+import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
+import com.kingsrook.qqq.backend.core.model.metadata.tables.UniqueKey;
+import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModuleDispatcher;
+import com.kingsrook.qqq.backend.core.modules.authentication.implementations.TableBasedAuthenticationModule;
+
+
+/*******************************************************************************
+ ** Meta-data to provide details of an Auth0 Authentication module
+ *******************************************************************************/
+public class TableBasedAuthenticationMetaData extends QAuthenticationMetaData
+{
+ private String userTableName = "user";
+ private String userTablePrimaryKeyField = "id";
+ private String userTableUsernameField = "username";
+ private String userTablePasswordHashField = "passwordHash";
+ private String userTableFullNameField = "fullName";
+
+ private String sessionTableName = "session";
+ private String sessionTablePrimaryKeyField = "id";
+ private String sessionTableUuidField = "id";
+ private String sessionTableUserIdField = "userId";
+ private String sessionTableAccessTimestampField = "accessTimestamp";
+
+ private Integer inactivityTimeoutSeconds = 14_400; // 4 hours
+
+
+
+ /*******************************************************************************
+ ** Default Constructor.
+ *******************************************************************************/
+ public TableBasedAuthenticationMetaData()
+ {
+ super();
+ setType(QAuthenticationType.TABLE_BASED);
+
+ //////////////////////////////////////////////////////////
+ // ensure this module is registered with the dispatcher //
+ //////////////////////////////////////////////////////////
+ QAuthenticationModuleDispatcher.registerModule(QAuthenticationType.TABLE_BASED.getName(), TableBasedAuthenticationModule.class.getName());
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public QTableMetaData defineStandardUserTable(String backendName)
+ {
+ return (new QTableMetaData()
+ .withName(getUserTableName())
+ .withBackendName(backendName)
+ .withPrimaryKeyField(getUserTablePrimaryKeyField())
+ .withRecordLabelFormat("%s")
+ .withRecordLabelFields(getUserTableUsernameField())
+ .withUniqueKey(new UniqueKey(getUserTableUsernameField()))
+ .withField(new QFieldMetaData(getUserTablePrimaryKeyField(), QFieldType.INTEGER))
+ .withField(new QFieldMetaData("createDate", QFieldType.DATE_TIME))
+ .withField(new QFieldMetaData("modifyDate", QFieldType.DATE_TIME))
+ .withField(new QFieldMetaData(getUserTablePrimaryKeyField(), QFieldType.INTEGER))
+ .withField(new QFieldMetaData(getUserTableUsernameField(), QFieldType.STRING))
+ .withField(new QFieldMetaData(getUserTablePasswordHashField(), QFieldType.STRING))
+ .withField(new QFieldMetaData(getUserTableFullNameField(), QFieldType.STRING)));
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public QTableMetaData defineStandardSessionTable(String backendName)
+ {
+ return (new QTableMetaData()
+ .withName(getSessionTableName())
+ .withBackendName(backendName)
+ .withPrimaryKeyField(getSessionTablePrimaryKeyField())
+ .withRecordLabelFormat("%s")
+ .withRecordLabelFields(getSessionTableUuidField())
+ .withUniqueKey(new UniqueKey(getSessionTableUuidField()))
+ .withField(new QFieldMetaData(getSessionTableUuidField(), QFieldType.STRING))
+ .withField(new QFieldMetaData("createDate", QFieldType.DATE_TIME))
+ .withField(new QFieldMetaData("modifyDate", QFieldType.DATE_TIME))
+ .withField(new QFieldMetaData(getSessionTableUserIdField(), QFieldType.INTEGER))
+ .withField(new QFieldMetaData(getSessionTableAccessTimestampField(), QFieldType.DATE_TIME)));
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for userTableName
+ *******************************************************************************/
+ public String getUserTableName()
+ {
+ return (this.userTableName);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for userTableName
+ *******************************************************************************/
+ public void setUserTableName(String userTableName)
+ {
+ this.userTableName = userTableName;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for userTableName
+ *******************************************************************************/
+ public TableBasedAuthenticationMetaData withUserTableName(String userTableName)
+ {
+ this.userTableName = userTableName;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for sessionTableName
+ *******************************************************************************/
+ public String getSessionTableName()
+ {
+ return (this.sessionTableName);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for sessionTableName
+ *******************************************************************************/
+ public void setSessionTableName(String sessionTableName)
+ {
+ this.sessionTableName = sessionTableName;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for sessionTableName
+ *******************************************************************************/
+ public TableBasedAuthenticationMetaData withSessionTableName(String sessionTableName)
+ {
+ this.sessionTableName = sessionTableName;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for userTablePrimaryKeyField
+ *******************************************************************************/
+ public String getUserTablePrimaryKeyField()
+ {
+ return (this.userTablePrimaryKeyField);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for userTablePrimaryKeyField
+ *******************************************************************************/
+ public void setUserTablePrimaryKeyField(String userTablePrimaryKeyField)
+ {
+ this.userTablePrimaryKeyField = userTablePrimaryKeyField;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for userTablePrimaryKeyField
+ *******************************************************************************/
+ public TableBasedAuthenticationMetaData withUserTablePrimaryKeyField(String userTablePrimaryKeyField)
+ {
+ this.userTablePrimaryKeyField = userTablePrimaryKeyField;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for userTableUsernameField
+ *******************************************************************************/
+ public String getUserTableUsernameField()
+ {
+ return (this.userTableUsernameField);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for userTableUsernameField
+ *******************************************************************************/
+ public void setUserTableUsernameField(String userTableUsernameField)
+ {
+ this.userTableUsernameField = userTableUsernameField;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for userTableUsernameField
+ *******************************************************************************/
+ public TableBasedAuthenticationMetaData withUserTableUsernameField(String userTableUsernameField)
+ {
+ this.userTableUsernameField = userTableUsernameField;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for sessionTablePrimaryKeyField
+ *******************************************************************************/
+ public String getSessionTablePrimaryKeyField()
+ {
+ return (this.sessionTablePrimaryKeyField);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for sessionTablePrimaryKeyField
+ *******************************************************************************/
+ public void setSessionTablePrimaryKeyField(String sessionTablePrimaryKeyField)
+ {
+ this.sessionTablePrimaryKeyField = sessionTablePrimaryKeyField;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for sessionTablePrimaryKeyField
+ *******************************************************************************/
+ public TableBasedAuthenticationMetaData withSessionTablePrimaryKeyField(String sessionTablePrimaryKeyField)
+ {
+ this.sessionTablePrimaryKeyField = sessionTablePrimaryKeyField;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for sessionTableUserIdField
+ *******************************************************************************/
+ public String getSessionTableUserIdField()
+ {
+ return (this.sessionTableUserIdField);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for sessionTableUserIdField
+ *******************************************************************************/
+ public void setSessionTableUserIdField(String sessionTableUserIdField)
+ {
+ this.sessionTableUserIdField = sessionTableUserIdField;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for sessionTableUserIdField
+ *******************************************************************************/
+ public TableBasedAuthenticationMetaData withSessionTableUserIdField(String sessionTableUserIdField)
+ {
+ this.sessionTableUserIdField = sessionTableUserIdField;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for sessionTableUuidField
+ *******************************************************************************/
+ public String getSessionTableUuidField()
+ {
+ return (this.sessionTableUuidField);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for sessionTableUuidField
+ *******************************************************************************/
+ public void setSessionTableUuidField(String sessionTableUuidField)
+ {
+ this.sessionTableUuidField = sessionTableUuidField;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for sessionTableUuidField
+ *******************************************************************************/
+ public TableBasedAuthenticationMetaData withSessionTableUuidField(String sessionTableUuidField)
+ {
+ this.sessionTableUuidField = sessionTableUuidField;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for userTableFullNameField
+ *******************************************************************************/
+ public String getUserTableFullNameField()
+ {
+ return (this.userTableFullNameField);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for userTableFullNameField
+ *******************************************************************************/
+ public void setUserTableFullNameField(String userTableFullNameField)
+ {
+ this.userTableFullNameField = userTableFullNameField;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for userTableFullNameField
+ *******************************************************************************/
+ public TableBasedAuthenticationMetaData withUserTableFullNameField(String userTableFullNameField)
+ {
+ this.userTableFullNameField = userTableFullNameField;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for userTablePasswordHashField
+ *******************************************************************************/
+ public String getUserTablePasswordHashField()
+ {
+ return (this.userTablePasswordHashField);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for userTablePasswordHashField
+ *******************************************************************************/
+ public void setUserTablePasswordHashField(String userTablePasswordHashField)
+ {
+ this.userTablePasswordHashField = userTablePasswordHashField;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for userTablePasswordHashField
+ *******************************************************************************/
+ public TableBasedAuthenticationMetaData withUserTablePasswordHashField(String userTablePasswordHashField)
+ {
+ this.userTablePasswordHashField = userTablePasswordHashField;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for sessionTableAccessTimestampField
+ *******************************************************************************/
+ public String getSessionTableAccessTimestampField()
+ {
+ return (this.sessionTableAccessTimestampField);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for sessionTableAccessTimestampField
+ *******************************************************************************/
+ public void setSessionTableAccessTimestampField(String sessionTableAccessTimestampField)
+ {
+ this.sessionTableAccessTimestampField = sessionTableAccessTimestampField;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for sessionTableAccessTimestampField
+ *******************************************************************************/
+ public TableBasedAuthenticationMetaData withSessionTableAccessTimestampField(String sessionTableAccessTimestampField)
+ {
+ this.sessionTableAccessTimestampField = sessionTableAccessTimestampField;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for inactivityTimeoutSeconds
+ *******************************************************************************/
+ public Integer getInactivityTimeoutSeconds()
+ {
+ return (this.inactivityTimeoutSeconds);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for inactivityTimeoutSeconds
+ *******************************************************************************/
+ public void setInactivityTimeoutSeconds(Integer inactivityTimeoutSeconds)
+ {
+ this.inactivityTimeoutSeconds = inactivityTimeoutSeconds;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for inactivityTimeoutSeconds
+ *******************************************************************************/
+ public TableBasedAuthenticationMetaData withInactivityTimeoutSeconds(Integer inactivityTimeoutSeconds)
+ {
+ this.inactivityTimeoutSeconds = inactivityTimeoutSeconds;
+ return (this);
+ }
+
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/QAuthenticationModuleDispatcher.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/QAuthenticationModuleDispatcher.java
index 258509f2..8d925847 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/QAuthenticationModuleDispatcher.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/QAuthenticationModuleDispatcher.java
@@ -22,11 +22,14 @@
package com.kingsrook.qqq.backend.core.modules.authentication;
+import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import com.kingsrook.qqq.backend.core.exceptions.QModuleDispatchException;
import com.kingsrook.qqq.backend.core.model.metadata.QAuthenticationType;
-import com.kingsrook.qqq.backend.core.modules.authentication.metadata.QAuthenticationMetaData;
+import com.kingsrook.qqq.backend.core.model.metadata.authentication.QAuthenticationMetaData;
+import com.kingsrook.qqq.backend.core.modules.authentication.implementations.FullyAnonymousAuthenticationModule;
+import com.kingsrook.qqq.backend.core.modules.authentication.implementations.MockAuthenticationModule;
/*******************************************************************************
@@ -38,20 +41,35 @@ import com.kingsrook.qqq.backend.core.modules.authentication.metadata.QAuthentic
*******************************************************************************/
public class QAuthenticationModuleDispatcher
{
- private final Map authenticationTypeToModuleClassNameMap;
-
+ private static Map authenticationTypeToModuleClassNameMap = Collections.synchronizedMap(new HashMap<>());
+ static
+ {
+ //////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ // ensure our 2 default modules are registered. //
+ // Note that for "real" implementations, the pattern is for their MetaData class's constructor to register. //
+ // the idea being, any qInstance using such a module, surely will have some MetaData defined for it. //
+ //////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ registerModule(QAuthenticationType.MOCK.getName(), MockAuthenticationModule.class.getName());
+ registerModule(QAuthenticationType.FULLY_ANONYMOUS.getName(), FullyAnonymousAuthenticationModule.class.getName());
+ }
/*******************************************************************************
**
*******************************************************************************/
public QAuthenticationModuleDispatcher()
{
- authenticationTypeToModuleClassNameMap = new HashMap<>();
- authenticationTypeToModuleClassNameMap.put(QAuthenticationType.MOCK.getName(), "com.kingsrook.qqq.backend.core.modules.authentication.MockAuthenticationModule");
- authenticationTypeToModuleClassNameMap.put(QAuthenticationType.FULLY_ANONYMOUS.getName(), "com.kingsrook.qqq.backend.core.modules.authentication.FullyAnonymousAuthenticationModule");
- authenticationTypeToModuleClassNameMap.put(QAuthenticationType.AUTH_0.getName(), "com.kingsrook.qqq.backend.core.modules.authentication.Auth0AuthenticationModule");
- // todo - let user define custom type -> classes
+
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public static void registerModule(String name, String className)
+ {
+ authenticationTypeToModuleClassNameMap.putIfAbsent(name, className);
}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/Auth0AuthenticationModule.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/Auth0AuthenticationModule.java
similarity index 95%
rename from qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/Auth0AuthenticationModule.java
rename to qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/Auth0AuthenticationModule.java
index aaa9bc36..f5264730 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/Auth0AuthenticationModule.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/Auth0AuthenticationModule.java
@@ -19,7 +19,7 @@
* along with this program. If not, see .
*/
-package com.kingsrook.qqq.backend.core.modules.authentication;
+package com.kingsrook.qqq.backend.core.modules.authentication.implementations;
import java.nio.charset.StandardCharsets;
@@ -45,9 +45,10 @@ import com.auth0.jwt.interfaces.DecodedJWT;
import com.auth0.jwt.interfaces.JWTVerifier;
import com.kingsrook.qqq.backend.core.exceptions.QAuthenticationException;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
+import com.kingsrook.qqq.backend.core.model.metadata.authentication.Auth0AuthenticationMetaData;
import com.kingsrook.qqq.backend.core.model.session.QSession;
import com.kingsrook.qqq.backend.core.model.session.QUser;
-import com.kingsrook.qqq.backend.core.modules.authentication.metadata.Auth0AuthenticationMetaData;
+import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModuleInterface;
import com.kingsrook.qqq.backend.core.state.AbstractStateKey;
import com.kingsrook.qqq.backend.core.state.InMemoryStateProvider;
import com.kingsrook.qqq.backend.core.state.StateProviderInterface;
@@ -93,15 +94,15 @@ public class Auth0AuthenticationModule implements QAuthenticationModuleInterface
if(context.containsKey(BASIC_AUTH_KEY))
{
Auth0AuthenticationMetaData metaData = (Auth0AuthenticationMetaData) qInstance.getAuthentication();
- AuthAPI auth = new AuthAPI(metaData.getBaseUrl(), metaData.getClientId(), metaData.getClientSecret());
+ AuthAPI auth = new AuthAPI(metaData.getBaseUrl(), metaData.getClientId(), metaData.getClientSecret());
try
{
/////////////////////////////////////////////////
// decode the credentials from the header auth //
/////////////////////////////////////////////////
String base64Credentials = context.get(BASIC_AUTH_KEY).trim();
- byte[] credDecoded = Base64.getDecoder().decode(base64Credentials);
- String credentials = new String(credDecoded, StandardCharsets.UTF_8);
+ byte[] credDecoded = Base64.getDecoder().decode(base64Credentials);
+ String credentials = new String(credDecoded, StandardCharsets.UTF_8);
/////////////////////////////////////
// call auth0 with a login request //
@@ -117,11 +118,10 @@ public class Auth0AuthenticationModule implements QAuthenticationModuleInterface
////////////////
// ¯\_(ツ)_/¯ //
////////////////
- String message = "An unknown error occurred during handling basic auth";
+ String message = "Error handling basic authentication: " + e.getMessage();
LOG.error(message, e);
throw (new QAuthenticationException(message));
}
-
}
//////////////////////////////////////////////////
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/FullyAnonymousAuthenticationModule.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/FullyAnonymousAuthenticationModule.java
similarity index 93%
rename from qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/FullyAnonymousAuthenticationModule.java
rename to qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/FullyAnonymousAuthenticationModule.java
index 8a0e7078..01f582d7 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/FullyAnonymousAuthenticationModule.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/FullyAnonymousAuthenticationModule.java
@@ -19,7 +19,7 @@
* along with this program. If not, see .
*/
-package com.kingsrook.qqq.backend.core.modules.authentication;
+package com.kingsrook.qqq.backend.core.modules.authentication.implementations;
import java.util.Map;
@@ -27,6 +27,7 @@ import java.util.UUID;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.session.QSession;
import com.kingsrook.qqq.backend.core.model.session.QUser;
+import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModuleInterface;
/*******************************************************************************
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/MockAuthenticationModule.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/MockAuthenticationModule.java
similarity index 94%
rename from qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/MockAuthenticationModule.java
rename to qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/MockAuthenticationModule.java
index f490d0a4..afc2cc39 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/MockAuthenticationModule.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/MockAuthenticationModule.java
@@ -19,7 +19,7 @@
* along with this program. If not, see .
*/
-package com.kingsrook.qqq.backend.core.modules.authentication;
+package com.kingsrook.qqq.backend.core.modules.authentication.implementations;
import java.util.Map;
@@ -27,6 +27,7 @@ import java.util.UUID;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.session.QSession;
import com.kingsrook.qqq.backend.core.model.session.QUser;
+import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModuleInterface;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/TableBasedAuthenticationModule.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/TableBasedAuthenticationModule.java
new file mode 100644
index 00000000..14202fbc
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/TableBasedAuthenticationModule.java
@@ -0,0 +1,612 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2022. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.core.modules.authentication.implementations;
+
+
+import javax.crypto.SecretKeyFactory;
+import javax.crypto.spec.PBEKeySpec;
+import java.nio.charset.StandardCharsets;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.security.spec.InvalidKeySpecException;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.Base64;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.UUID;
+import com.kingsrook.qqq.backend.core.actions.tables.GetAction;
+import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
+import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction;
+import com.kingsrook.qqq.backend.core.exceptions.QAuthenticationException;
+import com.kingsrook.qqq.backend.core.exceptions.QException;
+import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetInput;
+import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetOutput;
+import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
+import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertOutput;
+import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput;
+import com.kingsrook.qqq.backend.core.model.data.QRecord;
+import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
+import com.kingsrook.qqq.backend.core.model.metadata.authentication.TableBasedAuthenticationMetaData;
+import com.kingsrook.qqq.backend.core.model.session.QSession;
+import com.kingsrook.qqq.backend.core.model.session.QUser;
+import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModuleInterface;
+import com.kingsrook.qqq.backend.core.state.AbstractStateKey;
+import com.kingsrook.qqq.backend.core.state.InMemoryStateProvider;
+import com.kingsrook.qqq.backend.core.state.StateProviderInterface;
+import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public class TableBasedAuthenticationModule implements QAuthenticationModuleInterface
+{
+ private static final Logger LOG = LogManager.getLogger(TableBasedAuthenticationModule.class);
+
+ /////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ // 30 minutes - ideally this would be lower, but right now we've been dealing with re-validation issues... //
+ /////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ public static final int ID_TOKEN_VALIDATION_INTERVAL_SECONDS = 1800;
+
+ public static final String SESSION_ID_KEY = "sessionId";
+ public static final String BASIC_AUTH_KEY = "basicAuthString";
+
+ public static final String SESSION_ID_NOT_PROVIDED_ERROR = "Session ID was not provided";
+
+
+ ////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ // this is how we allow the actions within this class to work without themselves having a logged-in user. //
+ ////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ private static QSession chickenAndEggSession = new QSession()
+ {
+
+ };
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ public QSession createSession(QInstance qInstance, Map context) throws QAuthenticationException
+ {
+ TableBasedAuthenticationMetaData metaData = (TableBasedAuthenticationMetaData) qInstance.getAuthentication();
+ String sessionUuid = context.get(SESSION_ID_KEY);
+
+ ///////////////////////////////////////////////////////////
+ // check if we are processing a Basic Auth Session first //
+ ///////////////////////////////////////////////////////////
+ if(context.containsKey(BASIC_AUTH_KEY))
+ {
+ try
+ {
+ /////////////////////////////////////////////////
+ // decode the credentials from the header auth //
+ /////////////////////////////////////////////////
+ String base64Credentials = context.get(BASIC_AUTH_KEY).trim();
+ byte[] credDecoded = Base64.getDecoder().decode(base64Credentials);
+ String credentials = new String(credDecoded, StandardCharsets.UTF_8);
+
+ ///////////////////////////
+ // fetch the user record //
+ ///////////////////////////
+ GetInput getInput = new GetInput(qInstance);
+ getInput.setSession(chickenAndEggSession);
+ getInput.setTableName(metaData.getUserTableName());
+ getInput.setUniqueKey(Map.of(metaData.getUserTableUsernameField(), credentials.split(":")[0]));
+ GetOutput getOutput = new GetAction().execute(getInput);
+ if(getOutput.getRecord() == null)
+ {
+ throw (new QAuthenticationException("Incorrect username or password."));
+ }
+
+ //////////////////////////////////////////////////////////
+ // compare the hashed input password to the stored hash //
+ //////////////////////////////////////////////////////////
+ QRecord user = getOutput.getRecord();
+ String inputPassword = credentials.split(":")[1];
+ String storedHash = user.getValueString(metaData.getUserTablePasswordHashField());
+
+ // if(!inputHash.equals(storedHash))
+ if(!PasswordHasher.validatePassword(inputPassword, storedHash))
+ {
+ throw (new QAuthenticationException("Incorrect username or password."));
+ }
+
+ //////////////////////
+ // insert a session //
+ //////////////////////
+ sessionUuid = UUID.randomUUID().toString();
+ QRecord sessionRecord = new QRecord()
+ .withValue(metaData.getSessionTableUuidField(), sessionUuid)
+ .withValue(metaData.getSessionTableAccessTimestampField(), Instant.now())
+ .withValue(metaData.getSessionTableUserIdField(), user.getValue(metaData.getUserTablePrimaryKeyField()));
+ InsertInput insertInput = new InsertInput(qInstance);
+ insertInput.setSession(chickenAndEggSession);
+ insertInput.setTableName(metaData.getSessionTableName());
+ insertInput.setRecords(List.of(sessionRecord));
+ InsertOutput insertOutput = new InsertAction().execute(insertInput);
+ if(CollectionUtils.nullSafeHasContents(insertOutput.getRecords().get(0).getErrors()))
+ {
+ LOG.warn("Inserting session failed: " + insertOutput.getRecords().get(0).getErrors());
+ throw (new QAuthenticationException("Incorrect username or password."));
+ }
+ }
+ catch(QAuthenticationException ae)
+ {
+ // todo - sleep to obscure what was the issue.
+ throw (ae);
+ }
+ catch(Exception e)
+ {
+ ////////////////
+ // ¯\_(ツ)_/¯ //
+ ////////////////
+ // todo - sleep to obscure what was the issue.
+ String message = "Error handling basic authentication: " + e.getMessage();
+ LOG.error(message, e);
+ throw (new QAuthenticationException(message));
+ }
+ }
+
+ //////////////////////////////////////////////////
+ // get the session uuid from the context object //
+ //////////////////////////////////////////////////
+ if(sessionUuid == null)
+ {
+ LOG.warn(SESSION_ID_NOT_PROVIDED_ERROR);
+ throw (new QAuthenticationException(SESSION_ID_NOT_PROVIDED_ERROR));
+ }
+
+ try
+ {
+ /////////////////////////////////////////////////////
+ // try to build session to see if still valid //
+ // then call method to check more session validity //
+ /////////////////////////////////////////////////////
+ QSession qSession = buildQSessionFromUuid(qInstance, metaData, sessionUuid);
+ if(isSessionValid(qInstance, qSession))
+ {
+ return (qSession);
+ }
+
+ ///////////////////////////////////////////////////////////////////////////////////////
+ // if we make it here it means we have never validated this token or its been a long //
+ // enough duration so we need to re-verify the token //
+ ///////////////////////////////////////////////////////////////////////////////////////
+ qSession = revalidateSession(qInstance, sessionUuid);
+
+ ////////////////////////////////////////////////////////////////////
+ // put now into state so we dont check until next interval passes //
+ ///////////////////////////////////////////////////////////////////
+ StateProviderInterface spi = getStateProvider();
+ SessionIdStateKey key = new SessionIdStateKey(qSession.getIdReference());
+ spi.put(key, Instant.now());
+
+ return (qSession);
+ }
+ catch(QAuthenticationException ae)
+ {
+ LOG.info("Authentication exception", ae);
+ throw (ae);
+ }
+ catch(Exception e)
+ {
+ ////////////////
+ // ¯\_(ツ)_/¯ //
+ ////////////////
+ String message = "An unknown error occurred";
+ LOG.error(message, e);
+ throw (new QAuthenticationException(message));
+ }
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ public boolean isSessionValid(QInstance instance, QSession session)
+ {
+ if(session == chickenAndEggSession)
+ {
+ ////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ // this is how we allow the actions within this class to work without themselves having a logged-in user. //
+ ////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ return (true);
+ }
+
+ if(session == null)
+ {
+ return (false);
+ }
+
+ if(session.getIdReference() == null)
+ {
+ return (false);
+ }
+
+ StateProviderInterface stateProvider = getStateProvider();
+ SessionIdStateKey key = new SessionIdStateKey(session.getIdReference());
+ Optional lastTimeCheckedOptional = stateProvider.get(Instant.class, key);
+ if(lastTimeCheckedOptional.isPresent())
+ {
+ Instant lastTimeChecked = lastTimeCheckedOptional.get();
+
+ ///////////////////////////////////////////////////////////////////////////////////////////////////
+ // returns negative int if less than compared duration, 0 if equal, positive int if greater than //
+ // - so this is basically saying, if the time between the last time we checked the token and //
+ // right now is more than ID_TOKEN_VALIDATION_INTERVAL_SECTIONS, then session needs revalidated //
+ ///////////////////////////////////////////////////////////////////////////////////////////////////
+ if(Duration.between(lastTimeChecked, Instant.now()).compareTo(Duration.ofSeconds(ID_TOKEN_VALIDATION_INTERVAL_SECONDS)) < 0)
+ {
+ return (true);
+ }
+ }
+
+ try
+ {
+ LOG.debug("Re-validating token due to validation interval [" + lastTimeCheckedOptional + "] being passed (or never being set): " + session.getIdReference());
+ revalidateSession(instance, session.getIdReference());
+
+ //////////////////////////////////////////////////////////////////
+ // update the timestamp in state provider, to avoid re-checking //
+ //////////////////////////////////////////////////////////////////
+ stateProvider.put(key, Instant.now());
+
+ return (true);
+ }
+ catch(QAuthenticationException ae)
+ {
+ return (false);
+ }
+ catch(Exception e)
+ {
+ LOG.warn("Error validating session", e);
+ return (false);
+ }
+ }
+
+
+
+ /*******************************************************************************
+ ** makes request to check if a session is still valid and build new qSession if it is
+ **
+ *******************************************************************************/
+ private QSession revalidateSession(QInstance qInstance, String sessionUuid) throws QException
+ {
+ TableBasedAuthenticationMetaData metaData = (TableBasedAuthenticationMetaData) qInstance.getAuthentication();
+
+ GetInput getSessionInput = new GetInput(qInstance);
+ getSessionInput.setSession(chickenAndEggSession);
+ getSessionInput.setTableName(metaData.getSessionTableName());
+ getSessionInput.setUniqueKey(Map.of(metaData.getSessionTableUuidField(), sessionUuid));
+ GetOutput getSessionOutput = new GetAction().execute(getSessionInput);
+ if(getSessionOutput.getRecord() == null)
+ {
+ throw (new QAuthenticationException("Session not found."));
+ }
+ QRecord sessionRecord = getSessionOutput.getRecord();
+ Instant lastAccess = sessionRecord.getValueInstant(metaData.getSessionTableAccessTimestampField());
+
+ ///////////////////////////////////////////////////////////////////////////////////////////////////
+ // returns negative int if less than compared duration, 0 if equal, positive int if greater than //
+ // - so this is basically saying, if the time between the last time the session was marked as //
+ // active, and right now is more than the timeout seconds, then the session is expired //
+ ///////////////////////////////////////////////////////////////////////////////////////////////////
+ if(lastAccess.plus(Duration.ofSeconds(metaData.getInactivityTimeoutSeconds())).isBefore(Instant.now()))
+ {
+ throw (new QAuthenticationException("Session is expired."));
+ }
+
+ ///////////////////////////////////////////////
+ // update the timestamp in the session table //
+ ///////////////////////////////////////////////
+ UpdateInput updateInput = new UpdateInput(qInstance);
+ updateInput.setSession(chickenAndEggSession);
+ updateInput.setTableName(metaData.getSessionTableName());
+ updateInput.setRecords(List.of(new QRecord()
+ .withValue(metaData.getSessionTablePrimaryKeyField(), sessionRecord.getValue(metaData.getSessionTablePrimaryKeyField()))
+ .withValue(metaData.getSessionTableAccessTimestampField(), Instant.now())));
+ new UpdateAction().execute(updateInput);
+
+ return (buildQSessionFromUuid(qInstance, metaData, sessionUuid));
+ }
+
+
+
+ /*******************************************************************************
+ ** extracts info from token creating a QSession
+ **
+ *******************************************************************************/
+ private QSession buildQSessionFromUuid(QInstance qInstance, TableBasedAuthenticationMetaData metaData, String sessionUuid) throws QException
+ {
+ GetInput getSessionInput = new GetInput(qInstance);
+ getSessionInput.setSession(chickenAndEggSession);
+ getSessionInput.setTableName(metaData.getSessionTableName());
+ getSessionInput.setUniqueKey(Map.of(metaData.getSessionTableUuidField(), sessionUuid));
+ GetOutput getSessionOutput = new GetAction().execute(getSessionInput);
+ if(getSessionOutput.getRecord() == null)
+ {
+ throw (new QAuthenticationException("Session not found."));
+ }
+ QRecord sessionRecord = getSessionOutput.getRecord();
+
+ GetInput getUserInput = new GetInput(qInstance);
+ getUserInput.setSession(chickenAndEggSession);
+ getUserInput.setTableName(metaData.getUserTableName());
+ getUserInput.setPrimaryKey(sessionRecord.getValue(metaData.getSessionTableUserIdField()));
+ GetOutput getUserOutput = new GetAction().execute(getUserInput);
+ if(getUserOutput.getRecord() == null)
+ {
+ throw (new QAuthenticationException("User for session not found."));
+ }
+ QRecord userRecord = getUserOutput.getRecord();
+
+ QUser qUser = new QUser();
+ qUser.setFullName(userRecord.getValueString(metaData.getUserTableFullNameField()));
+ qUser.setIdReference(userRecord.getValueString(metaData.getUserTableUsernameField()));
+
+ QSession qSession = new QSession();
+ qSession.setIdReference(sessionUuid);
+ qSession.setUser(qUser);
+
+ return (qSession);
+ }
+
+
+
+ /*******************************************************************************
+ ** Load an instance of the appropriate state provider
+ **
+ *******************************************************************************/
+ public static StateProviderInterface getStateProvider()
+ {
+ // TODO - read this from somewhere in meta data eh?
+ return (InMemoryStateProvider.getInstance());
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public static class SessionIdStateKey extends AbstractStateKey
+ {
+ private final String key;
+
+
+
+ /*******************************************************************************
+ ** Constructor.
+ **
+ *******************************************************************************/
+ SessionIdStateKey(String key)
+ {
+ this.key = key;
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ public String toString()
+ {
+ return (this.key);
+ }
+
+
+
+ /*******************************************************************************
+ ** Make the key give a unique string to identify itself.
+ *
+ *******************************************************************************/
+ @Override
+ public String getUniqueIdentifier()
+ {
+ return (this.key);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ public boolean equals(Object o)
+ {
+ if(this == o)
+ {
+ return true;
+ }
+ if(o == null || getClass() != o.getClass())
+ {
+ return false;
+ }
+ SessionIdStateKey that = (SessionIdStateKey) o;
+ return key.equals(that.key);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ public int hashCode()
+ {
+ return key.hashCode();
+ }
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public static class PasswordHasher
+ {
+ private static final String PBKDF2_ALGORITHM = "PBKDF2WithHmacSHA1";
+ private static final int SALT_BYTE_SIZE = 32;
+ private static final int HASH_BYTE_SIZE = 32;
+ private static final int PBKDF2_ITERATIONS = 1000;
+
+
+
+ /*******************************************************************************
+ ** Returns a salted, hashed version of a raw password.
+ **
+ *******************************************************************************/
+ public static String createHashedPassword(String password) throws NoSuchAlgorithmException, InvalidKeySpecException
+ {
+ ////////////////////////////
+ // Generate a random salt //
+ ////////////////////////////
+ SecureRandom random = new SecureRandom();
+ byte[] salt = new byte[SALT_BYTE_SIZE];
+ random.nextBytes(salt);
+
+ ///////////////////////
+ // Hash the password //
+ ///////////////////////
+ byte[] passwordHash = computePbkdf2Hash(password.toCharArray(), salt, PBKDF2_ITERATIONS, HASH_BYTE_SIZE);
+
+ //////////////////////////////////////////////////////
+ // return string in the format iterations:salt:hash //
+ //////////////////////////////////////////////////////
+ return (PBKDF2_ITERATIONS + ":" + toHex(salt) + ":" + toHex(passwordHash));
+ }
+
+
+
+ /*******************************************************************************
+ ** Computes the PBKDF2 hash.
+ **
+ *******************************************************************************/
+ private static byte[] computePbkdf2Hash(char[] password, byte[] salt, int iterations, int bytes) throws NoSuchAlgorithmException, InvalidKeySpecException
+ {
+ PBEKeySpec spec = new PBEKeySpec(password, salt, iterations, bytes * 8);
+ SecretKeyFactory skf = SecretKeyFactory.getInstance(PBKDF2_ALGORITHM);
+
+ return skf.generateSecret(spec).getEncoded();
+ }
+
+
+
+ /*******************************************************************************
+ ** Thanks to Baeldung for this and related methods
+ ** https://www.baeldung.com/java-byte-arrays-hex-strings
+ *******************************************************************************/
+ private static String toHex(byte[] array)
+ {
+ StringBuilder hexStringBuffer = new StringBuilder();
+ for(byte b : array)
+ {
+ hexStringBuffer.append(Character.forDigit((b >> 4) & 0xF, 16));
+ hexStringBuffer.append(Character.forDigit((b & 0xF), 16));
+ }
+ return hexStringBuffer.toString();
+ }
+
+
+
+ /*******************************************************************************
+ ** Validates a password against a hash.
+ **
+ *******************************************************************************/
+ private static boolean validatePassword(String password, String passwordHash) throws NoSuchAlgorithmException, InvalidKeySpecException
+ {
+ String[] params = passwordHash.split(":");
+ int iterations = Integer.parseInt(params[0]);
+ byte[] salt = fromHex(params[1]);
+ byte[] hash = fromHex(params[2]);
+
+ byte[] testHash = computePbkdf2Hash(password.toCharArray(), salt, iterations, hash.length);
+ return slowEquals(hash, testHash);
+ }
+
+
+
+ /*******************************************************************************
+ ** Compares two byte arrays in length-constant time. This comparison method
+ ** is used so that password hashes cannot be extracted from an on-line
+ ** system using a timing attack and then attacked off-line.
+ **
+ *******************************************************************************/
+ private static boolean slowEquals(byte[] a, byte[] b)
+ {
+ int diff = a.length ^ b.length;
+
+ for(int i = 0; i < a.length && i < b.length; i++)
+ {
+ diff |= a[i] ^ b[i];
+ }
+
+ return diff == 0;
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private static int toHexDigit(char hexChar)
+ {
+ int digit = Character.digit(hexChar, 16);
+ if(digit == -1)
+ {
+ throw new IllegalArgumentException("Invalid Hexadecimal Character: " + hexChar);
+ }
+ return digit;
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private static byte[] fromHex(String hexString)
+ {
+ if(hexString.length() % 2 == 1)
+ {
+ throw new IllegalArgumentException("Invalid hexadecimal String supplied.");
+ }
+
+ byte[] bytes = new byte[hexString.length() / 2];
+ for(int i = 0; i < hexString.length(); i += 2)
+ {
+ int firstDigit = toHexDigit(hexString.charAt(i));
+ int secondDigit = toHexDigit(hexString.charAt(i + 1));
+ bytes[i / 2] = (byte) ((firstDigit << 4) + secondDigit);
+ }
+ return bytes;
+ }
+ }
+
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryRecordStore.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryRecordStore.java
index 11940619..be82ec82 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryRecordStore.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryRecordStore.java
@@ -74,6 +74,18 @@ public class MemoryRecordStore
+ /*******************************************************************************
+ ** forget all data AND statistics
+ *******************************************************************************/
+ public static void fullReset()
+ {
+ getInstance().reset();
+ resetStatistics();
+ setCollectStatistics(false);
+ }
+
+
+
/*******************************************************************************
** Forget all data in the memory store...
*******************************************************************************/
diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/authentication/QAuthenticationModuleDispatcherTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/authentication/QAuthenticationModuleDispatcherTest.java
index 44a0ae3b..f498bfe7 100644
--- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/authentication/QAuthenticationModuleDispatcherTest.java
+++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/authentication/QAuthenticationModuleDispatcherTest.java
@@ -23,7 +23,7 @@ package com.kingsrook.qqq.backend.core.modules.authentication;
import com.kingsrook.qqq.backend.core.exceptions.QModuleDispatchException;
-import com.kingsrook.qqq.backend.core.modules.authentication.metadata.QAuthenticationMetaData;
+import com.kingsrook.qqq.backend.core.model.metadata.authentication.QAuthenticationMetaData;
import com.kingsrook.qqq.backend.core.utils.TestUtils;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertNotNull;
diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/authentication/Auth0AuthenticationModuleTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/Auth0AuthenticationModuleTest.java
similarity index 95%
rename from qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/authentication/Auth0AuthenticationModuleTest.java
rename to qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/Auth0AuthenticationModuleTest.java
index bc69be9d..b2992cbe 100644
--- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/authentication/Auth0AuthenticationModuleTest.java
+++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/Auth0AuthenticationModuleTest.java
@@ -19,7 +19,7 @@
* along with this program. If not, see .
*/
-package com.kingsrook.qqq.backend.core.modules.authentication;
+package com.kingsrook.qqq.backend.core.modules.authentication.implementations;
import java.time.Instant;
@@ -28,17 +28,17 @@ import java.util.HashMap;
import java.util.Map;
import com.kingsrook.qqq.backend.core.exceptions.QAuthenticationException;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
+import com.kingsrook.qqq.backend.core.model.metadata.authentication.Auth0AuthenticationMetaData;
+import com.kingsrook.qqq.backend.core.model.metadata.authentication.QAuthenticationMetaData;
import com.kingsrook.qqq.backend.core.model.session.QSession;
-import com.kingsrook.qqq.backend.core.modules.authentication.metadata.Auth0AuthenticationMetaData;
-import com.kingsrook.qqq.backend.core.modules.authentication.metadata.QAuthenticationMetaData;
import com.kingsrook.qqq.backend.core.state.InMemoryStateProvider;
import com.kingsrook.qqq.backend.core.utils.TestUtils;
import org.junit.jupiter.api.Test;
-import static com.kingsrook.qqq.backend.core.modules.authentication.Auth0AuthenticationModule.AUTH0_ID_TOKEN_KEY;
-import static com.kingsrook.qqq.backend.core.modules.authentication.Auth0AuthenticationModule.COULD_NOT_DECODE_ERROR;
-import static com.kingsrook.qqq.backend.core.modules.authentication.Auth0AuthenticationModule.EXPIRED_TOKEN_ERROR;
-import static com.kingsrook.qqq.backend.core.modules.authentication.Auth0AuthenticationModule.INVALID_TOKEN_ERROR;
-import static com.kingsrook.qqq.backend.core.modules.authentication.Auth0AuthenticationModule.TOKEN_NOT_PROVIDED_ERROR;
+import static com.kingsrook.qqq.backend.core.modules.authentication.implementations.Auth0AuthenticationModule.AUTH0_ID_TOKEN_KEY;
+import static com.kingsrook.qqq.backend.core.modules.authentication.implementations.Auth0AuthenticationModule.COULD_NOT_DECODE_ERROR;
+import static com.kingsrook.qqq.backend.core.modules.authentication.implementations.Auth0AuthenticationModule.EXPIRED_TOKEN_ERROR;
+import static com.kingsrook.qqq.backend.core.modules.authentication.implementations.Auth0AuthenticationModule.INVALID_TOKEN_ERROR;
+import static com.kingsrook.qqq.backend.core.modules.authentication.implementations.Auth0AuthenticationModule.TOKEN_NOT_PROVIDED_ERROR;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/authentication/FullyAnonymousAuthenticationModuleTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/FullyAnonymousAuthenticationModuleTest.java
similarity index 96%
rename from qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/authentication/FullyAnonymousAuthenticationModuleTest.java
rename to qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/FullyAnonymousAuthenticationModuleTest.java
index f775511f..5785bc93 100644
--- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/authentication/FullyAnonymousAuthenticationModuleTest.java
+++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/FullyAnonymousAuthenticationModuleTest.java
@@ -19,7 +19,7 @@
* along with this program. If not, see .
*/
-package com.kingsrook.qqq.backend.core.modules.authentication;
+package com.kingsrook.qqq.backend.core.modules.authentication.implementations;
import com.kingsrook.qqq.backend.core.model.session.QSession;
diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/TableBasedAuthenticationModuleTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/TableBasedAuthenticationModuleTest.java
new file mode 100644
index 00000000..cd0976e2
--- /dev/null
+++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/TableBasedAuthenticationModuleTest.java
@@ -0,0 +1,404 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2022. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.core.modules.authentication.implementations;
+
+
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+import java.util.Base64;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import com.kingsrook.qqq.backend.core.actions.tables.GetAction;
+import com.kingsrook.qqq.backend.core.exceptions.QAuthenticationException;
+import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetInput;
+import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetOutput;
+import com.kingsrook.qqq.backend.core.model.data.QRecord;
+import com.kingsrook.qqq.backend.core.model.metadata.QAuthenticationType;
+import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
+import com.kingsrook.qqq.backend.core.model.metadata.authentication.Auth0AuthenticationMetaData;
+import com.kingsrook.qqq.backend.core.model.metadata.authentication.QAuthenticationMetaData;
+import com.kingsrook.qqq.backend.core.model.metadata.authentication.TableBasedAuthenticationMetaData;
+import com.kingsrook.qqq.backend.core.model.session.QSession;
+import com.kingsrook.qqq.backend.core.modules.backend.implementations.memory.MemoryRecordStore;
+import com.kingsrook.qqq.backend.core.state.InMemoryStateProvider;
+import com.kingsrook.qqq.backend.core.utils.TestUtils;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+
+/*******************************************************************************
+ ** Unit test for the TableBasedAuthenticationModule
+ *******************************************************************************/
+public class TableBasedAuthenticationModuleTest
+{
+ public static final String USERNAME = "jdoe";
+ public static final String PASSWORD = "abc123";
+ public static final String FULL_NAME = "John Doe";
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @BeforeEach
+ @AfterEach
+ void beforeAndAfterEach()
+ {
+ MemoryRecordStore.getInstance().reset();
+ MemoryRecordStore.resetStatistics();
+ MemoryRecordStore.setCollectStatistics(false);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testSuccessfulLogin() throws Exception
+ {
+ QInstance qInstance = getQInstance();
+ insertTestUser(qInstance, USERNAME, PASSWORD, FULL_NAME);
+
+ QSession session = new TableBasedAuthenticationModule().createSession(qInstance, Map.of(TableBasedAuthenticationModule.BASIC_AUTH_KEY, encodeBasicAuth(USERNAME, PASSWORD)));
+
+ assertNotNull(session);
+ assertNotNull(session.getIdReference());
+ assertNotNull(session.getUser());
+ assertEquals(USERNAME, session.getUser().getIdReference());
+ assertEquals(FULL_NAME, session.getUser().getFullName());
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testBadUsernameAndPassword() throws Exception
+ {
+ QInstance qInstance = getQInstance();
+ insertTestUser(qInstance, USERNAME, PASSWORD, FULL_NAME);
+
+ TableBasedAuthenticationModule authModule = new TableBasedAuthenticationModule();
+
+ assertThatThrownBy(() -> authModule.createSession(qInstance, Map.of(TableBasedAuthenticationModule.BASIC_AUTH_KEY, encodeBasicAuth("not-" + USERNAME, PASSWORD))))
+ .isInstanceOf(QAuthenticationException.class)
+ .hasMessageContaining("Incorrect username or password");
+
+ assertThatThrownBy(() -> authModule.createSession(qInstance, Map.of(TableBasedAuthenticationModule.BASIC_AUTH_KEY, encodeBasicAuth(USERNAME, "not-" + PASSWORD))))
+ .isInstanceOf(QAuthenticationException.class)
+ .hasMessageContaining("Incorrect username or password");
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testNoContextProvided() throws Exception
+ {
+ QInstance qInstance = getQInstance();
+ insertTestUser(qInstance, USERNAME, PASSWORD, FULL_NAME);
+
+ assertThatThrownBy(() -> new TableBasedAuthenticationModule().createSession(qInstance, Collections.emptyMap()))
+ .isInstanceOf(QAuthenticationException.class)
+ .hasMessageContaining("Session ID was not provided");
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testUseExistingSession() throws Exception
+ {
+ QInstance qInstance = getQInstance();
+
+ insertTestUser(qInstance, USERNAME, PASSWORD, FULL_NAME);
+ String uuid = insertTestSession(qInstance, USERNAME, Instant.now());
+
+ QSession session = new TableBasedAuthenticationModule().createSession(qInstance, Map.of(TableBasedAuthenticationModule.SESSION_ID_KEY, uuid));
+ assertNotNull(session);
+ assertEquals(uuid, session.getIdReference());
+ assertNotNull(session.getUser());
+ assertEquals(USERNAME, session.getUser().getIdReference());
+ assertEquals(FULL_NAME, session.getUser().getFullName());
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testCreatingAlmostExpiredSession() throws Exception
+ {
+ QInstance qInstance = getQInstance();
+
+ insertTestUser(qInstance, USERNAME, PASSWORD, FULL_NAME);
+ String uuid = insertTestSession(qInstance, USERNAME, Instant.now().minus(4, ChronoUnit.HOURS).plus(1, ChronoUnit.MINUTES));
+
+ QSession session = new TableBasedAuthenticationModule().createSession(qInstance, Map.of(TableBasedAuthenticationModule.SESSION_ID_KEY, uuid));
+ assertNotNull(session);
+ assertEquals(uuid, session.getIdReference());
+ assertNotNull(session.getUser());
+ assertEquals(USERNAME, session.getUser().getIdReference());
+ assertEquals(FULL_NAME, session.getUser().getFullName());
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testValidatingAlmostExpiredSession() throws Exception
+ {
+ QInstance qInstance = getQInstance();
+
+ insertTestUser(qInstance, USERNAME, PASSWORD, FULL_NAME);
+ String uuid = insertTestSession(qInstance, USERNAME, Instant.now().minus(4, ChronoUnit.HOURS).plus(1, ChronoUnit.MINUTES));
+
+ QSession session = new QSession();
+ session.setIdReference(uuid);
+ InMemoryStateProvider.getInstance().put(new TableBasedAuthenticationModule.SessionIdStateKey(session.getIdReference()), Instant.now());
+ assertTrue(new TableBasedAuthenticationModule().isSessionValid(qInstance, session));
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testCreatingJustExpiredSession() throws Exception
+ {
+ QInstance qInstance = getQInstance();
+
+ insertTestUser(qInstance, USERNAME, PASSWORD, FULL_NAME);
+ String uuid = insertTestSession(qInstance, USERNAME, Instant.now().minus(4, ChronoUnit.HOURS).minus(1, ChronoUnit.MINUTES));
+
+ assertThatThrownBy(() -> new TableBasedAuthenticationModule().createSession(qInstance, Map.of(TableBasedAuthenticationModule.SESSION_ID_KEY, uuid)))
+ .isInstanceOf(QAuthenticationException.class)
+ .hasMessageContaining("Session is expired");
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testValidatingJustExpiredSession() throws Exception
+ {
+ QInstance qInstance = getQInstance();
+
+ insertTestUser(qInstance, USERNAME, PASSWORD, FULL_NAME);
+ String uuid = insertTestSession(qInstance, USERNAME, Instant.now().minus(4, ChronoUnit.HOURS).minus(1, ChronoUnit.MINUTES));
+
+ QSession session = new QSession();
+ session.setIdReference(uuid);
+ assertFalse(new TableBasedAuthenticationModule().isSessionValid(qInstance, session));
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testValidatingNullInputs()
+ {
+ assertFalse(new TableBasedAuthenticationModule().isSessionValid(getQInstance(), null));
+ assertFalse(new TableBasedAuthenticationModule().isSessionValid(getQInstance(), new QSession()));
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testNonExistingSessionUUID() throws Exception
+ {
+ QInstance qInstance = getQInstance();
+
+ insertTestUser(qInstance, USERNAME, PASSWORD, FULL_NAME);
+ String uuid = insertTestSession(qInstance, USERNAME, Instant.now());
+
+ assertThatThrownBy(() -> new TableBasedAuthenticationModule().createSession(qInstance, Map.of(TableBasedAuthenticationModule.SESSION_ID_KEY, "not-" + uuid)))
+ .isInstanceOf(QAuthenticationException.class)
+ .hasMessageContaining("Session not found");
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testExistingSessionWithBadUserId() throws Exception
+ {
+ QInstance qInstance = getQInstance();
+
+ insertTestUser(qInstance, USERNAME, PASSWORD, FULL_NAME);
+ String uuid = insertTestSession(qInstance, "not-" + USERNAME, Instant.now());
+
+ assertThatThrownBy(() -> new TableBasedAuthenticationModule().createSession(qInstance, Map.of(TableBasedAuthenticationModule.SESSION_ID_KEY, uuid)))
+ .isInstanceOf(QAuthenticationException.class)
+ .hasMessageContaining("User for session not found");
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testWeDontAlwaysRevalidate() throws Exception
+ {
+ QInstance qInstance = getQInstance();
+
+ insertTestUser(qInstance, USERNAME, PASSWORD, FULL_NAME);
+ String uuid = insertTestSession(qInstance, USERNAME, Instant.now().minus(4, ChronoUnit.HOURS).plus(1, ChronoUnit.MINUTES));
+
+ QSession session = new TableBasedAuthenticationModule().createSession(qInstance, Map.of(TableBasedAuthenticationModule.SESSION_ID_KEY, uuid));
+
+ MemoryRecordStore.setCollectStatistics(true);
+
+ assertTrue(new TableBasedAuthenticationModule().isSessionValid(qInstance, session));
+ Map statistics = MemoryRecordStore.getStatistics();
+ assertEquals(0, statistics.size()); // should be no stats of any type!
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testWeDoAlwaysRevalidateIfNeeded() throws Exception
+ {
+ QInstance qInstance = getQInstance();
+
+ insertTestUser(qInstance, USERNAME, PASSWORD, FULL_NAME);
+ String uuid = insertTestSession(qInstance, USERNAME, Instant.now().minus(4, ChronoUnit.HOURS).plus(1, ChronoUnit.MINUTES));
+
+ QSession session = new TableBasedAuthenticationModule().createSession(qInstance, Map.of(TableBasedAuthenticationModule.SESSION_ID_KEY, uuid));
+
+ assertTrue(new TableBasedAuthenticationModule().isSessionValid(qInstance, session));
+
+ InMemoryStateProvider.getInstance().put(new TableBasedAuthenticationModule.SessionIdStateKey(session.getIdReference()), Instant.now().minus(TableBasedAuthenticationModule.ID_TOKEN_VALIDATION_INTERVAL_SECONDS + 10, ChronoUnit.SECONDS));
+
+ MemoryRecordStore.setCollectStatistics(true);
+ assertTrue(new TableBasedAuthenticationModule().isSessionValid(qInstance, session));
+ Map statistics = MemoryRecordStore.getStatistics();
+ assertEquals(3, statistics.get(MemoryRecordStore.STAT_QUERIES_RAN));
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private static void insertTestUser(QInstance qInstance, String username, String password, String fullName) throws Exception
+ {
+ QAuthenticationMetaData tableBasedAuthentication = qInstance.getAuthentication();
+ qInstance.setAuthentication(new Auth0AuthenticationMetaData().withName("mock").withType(QAuthenticationType.MOCK));
+ TestUtils.insertRecords(qInstance, qInstance.getTable("user"), List.of(new QRecord()
+ .withValue("username", username)
+ .withValue("fullName", fullName)
+ .withValue("passwordHash", TableBasedAuthenticationModule.PasswordHasher.createHashedPassword(password))));
+ qInstance.setAuthentication(tableBasedAuthentication);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private static String insertTestSession(QInstance qInstance, String username, Instant accessTimestamp) throws Exception
+ {
+ QAuthenticationMetaData tableBasedAuthentication = qInstance.getAuthentication();
+ qInstance.setAuthentication(new Auth0AuthenticationMetaData().withName("mock").withType(QAuthenticationType.MOCK));
+
+ String uuid = UUID.randomUUID().toString();
+
+ GetInput getUserInput = new GetInput(qInstance);
+ getUserInput.setSession(new QSession());
+ getUserInput.setTableName("user");
+ getUserInput.setUniqueKey(Map.of("username", username));
+ GetOutput getUserOutput = new GetAction().execute(getUserInput);
+
+ TestUtils.insertRecords(qInstance, qInstance.getTable("session"), List.of(new QRecord()
+ .withValue("id", uuid)
+ .withValue("userId", getUserOutput.getRecord() == null ? -1 : getUserOutput.getRecord().getValueInteger("id"))
+ .withValue("accessTimestamp", accessTimestamp)
+ .withValue("passwordHash", TableBasedAuthenticationModule.PasswordHasher.createHashedPassword(PASSWORD))));
+
+ qInstance.setAuthentication(tableBasedAuthentication);
+
+ return (uuid);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private String encodeBasicAuth(String username, String password)
+ {
+ Base64.Encoder encoder = Base64.getEncoder();
+ String originalString = username + ":" + password;
+ return (encoder.encodeToString(originalString.getBytes()));
+ }
+
+
+
+ /*******************************************************************************
+ ** utility method to prime a qInstance for these tests
+ **
+ *******************************************************************************/
+ private QInstance getQInstance()
+ {
+ TableBasedAuthenticationMetaData authenticationMetaData = new TableBasedAuthenticationMetaData();
+
+ QInstance qInstance = TestUtils.defineInstance();
+ qInstance.setAuthentication(authenticationMetaData);
+ qInstance.addTable(authenticationMetaData.defineStandardUserTable(TestUtils.MEMORY_BACKEND_NAME));
+ qInstance.addTable(authenticationMetaData.defineStandardSessionTable(TestUtils.MEMORY_BACKEND_NAME));
+
+ return (qInstance);
+ }
+
+}
diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/TestUtils.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/TestUtils.java
index 68d8cbe8..2178c865 100644
--- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/TestUtils.java
+++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/TestUtils.java
@@ -95,8 +95,8 @@ import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.TriggerEv
import com.kingsrook.qqq.backend.core.model.metadata.tables.cache.CacheOf;
import com.kingsrook.qqq.backend.core.model.metadata.tables.cache.CacheUseCase;
import com.kingsrook.qqq.backend.core.model.session.QSession;
-import com.kingsrook.qqq.backend.core.modules.authentication.MockAuthenticationModule;
-import com.kingsrook.qqq.backend.core.modules.authentication.metadata.QAuthenticationMetaData;
+import com.kingsrook.qqq.backend.core.modules.authentication.implementations.MockAuthenticationModule;
+import com.kingsrook.qqq.backend.core.model.metadata.authentication.QAuthenticationMetaData;
import com.kingsrook.qqq.backend.core.modules.backend.implementations.memory.MemoryBackendModule;
import com.kingsrook.qqq.backend.core.modules.backend.implementations.mock.MockBackendModule;
import com.kingsrook.qqq.backend.core.processes.implementations.basepull.BasepullConfiguration;
diff --git a/qqq-backend-module-api/src/test/java/com/kingsrook/qqq/backend/module/api/TestUtils.java b/qqq-backend-module-api/src/test/java/com/kingsrook/qqq/backend/module/api/TestUtils.java
index 5388297f..cf358633 100644
--- a/qqq-backend-module-api/src/test/java/com/kingsrook/qqq/backend/module/api/TestUtils.java
+++ b/qqq-backend-module-api/src/test/java/com/kingsrook/qqq/backend/module/api/TestUtils.java
@@ -31,7 +31,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeUsage;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
-import com.kingsrook.qqq.backend.core.modules.authentication.metadata.QAuthenticationMetaData;
+import com.kingsrook.qqq.backend.core.model.metadata.authentication.QAuthenticationMetaData;
import com.kingsrook.qqq.backend.module.api.model.AuthorizationType;
import com.kingsrook.qqq.backend.module.api.model.metadata.APIBackendMetaData;
import com.kingsrook.qqq.backend.module.api.model.metadata.APITableBackendDetails;
diff --git a/qqq-backend-module-filesystem/src/test/java/com/kingsrook/qqq/backend/module/filesystem/TestUtils.java b/qqq-backend-module-filesystem/src/test/java/com/kingsrook/qqq/backend/module/filesystem/TestUtils.java
index 3b174399..ef53bb8b 100644
--- a/qqq-backend-module-filesystem/src/test/java/com/kingsrook/qqq/backend/module/filesystem/TestUtils.java
+++ b/qqq-backend-module-filesystem/src/test/java/com/kingsrook/qqq/backend/module/filesystem/TestUtils.java
@@ -37,8 +37,8 @@ import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaD
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.session.QSession;
-import com.kingsrook.qqq.backend.core.modules.authentication.MockAuthenticationModule;
-import com.kingsrook.qqq.backend.core.modules.authentication.metadata.QAuthenticationMetaData;
+import com.kingsrook.qqq.backend.core.modules.authentication.implementations.MockAuthenticationModule;
+import com.kingsrook.qqq.backend.core.model.metadata.authentication.QAuthenticationMetaData;
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamed.StreamedETLProcess;
import com.kingsrook.qqq.backend.module.filesystem.base.model.metadata.Cardinality;
import com.kingsrook.qqq.backend.module.filesystem.base.model.metadata.RecordFormat;
diff --git a/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/TestUtils.java b/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/TestUtils.java
index a5470939..84aff27d 100644
--- a/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/TestUtils.java
+++ b/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/TestUtils.java
@@ -36,7 +36,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.PVSValueForm
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSourceType;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
-import com.kingsrook.qqq.backend.core.modules.authentication.metadata.QAuthenticationMetaData;
+import com.kingsrook.qqq.backend.core.model.metadata.authentication.QAuthenticationMetaData;
import com.kingsrook.qqq.backend.module.rdbms.actions.RDBMSActionTest;
import com.kingsrook.qqq.backend.module.rdbms.jdbc.ConnectionManager;
import com.kingsrook.qqq.backend.module.rdbms.jdbc.QueryManager;
diff --git a/qqq-language-support-javascript/src/test/java/com/kingsrook/qqq/languages/javascript/TestUtils.java b/qqq-language-support-javascript/src/test/java/com/kingsrook/qqq/languages/javascript/TestUtils.java
index 8d414889..eb29781a 100644
--- a/qqq-language-support-javascript/src/test/java/com/kingsrook/qqq/languages/javascript/TestUtils.java
+++ b/qqq-language-support-javascript/src/test/java/com/kingsrook/qqq/languages/javascript/TestUtils.java
@@ -28,7 +28,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
-import com.kingsrook.qqq.backend.core.modules.authentication.metadata.QAuthenticationMetaData;
+import com.kingsrook.qqq.backend.core.model.metadata.authentication.QAuthenticationMetaData;
/*******************************************************************************
diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java
index f4783a6a..aa723c23 100644
--- a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java
+++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java
@@ -88,7 +88,6 @@ import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.session.QSession;
-import com.kingsrook.qqq.backend.core.modules.authentication.Auth0AuthenticationModule;
import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModuleDispatcher;
import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModuleInterface;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
@@ -122,6 +121,7 @@ public class QJavalinImplementation
private static final int SESSION_COOKIE_AGE = 60 * 60 * 24;
private static final String SESSION_ID_COOKIE_NAME = "sessionId";
+ private static final String BASIC_AUTH_NAME = "basicAuthString";
static QInstance qInstance;
static QJavalinMetaData javalinMetaData = new QJavalinMetaData();
@@ -326,35 +326,55 @@ public class QJavalinImplementation
/*******************************************************************************
**
*******************************************************************************/
- static void setupSession(Context context, AbstractActionInput input) throws QModuleDispatchException
+ static void setupSession(Context context, AbstractActionInput input) throws QModuleDispatchException, QAuthenticationException
{
QAuthenticationModuleDispatcher qAuthenticationModuleDispatcher = new QAuthenticationModuleDispatcher();
QAuthenticationModuleInterface authenticationModule = qAuthenticationModuleDispatcher.getQModule(input.getAuthenticationMetaData());
+ boolean needToSetSessionIdCookie = false;
try
{
Map authenticationContext = new HashMap<>();
- /////////////////////////////////////////////////////////////////////////////////
- // look for a token in either the sessionId cookie, or an Authorization header //
- /////////////////////////////////////////////////////////////////////////////////
- String sessionIdCookieValue = context.cookie(SESSION_ID_COOKIE_NAME);
+ String sessionIdCookieValue = context.cookie(SESSION_ID_COOKIE_NAME);
+ String authorizationHeaderValue = context.header("Authorization");
+
if(StringUtils.hasContent(sessionIdCookieValue))
{
+ ////////////////////////////////////////
+ // first, look for a sessionId cookie //
+ ////////////////////////////////////////
authenticationContext.put(SESSION_ID_COOKIE_NAME, sessionIdCookieValue);
+ needToSetSessionIdCookie = true;
+ }
+ else if(authorizationHeaderValue != null)
+ {
+ /////////////////////////////////////////////////////////////////////////////////////////////////
+ // second, look for the authorization header: //
+ // either with a "Basic " prefix (for a username:password pair) //
+ // or with a "Bearer " prefix (for a token that can be handled the same as a sessionId cookie) //
+ /////////////////////////////////////////////////////////////////////////////////////////////////
+ String basicPrefix = "Basic ";
+ String bearerPrefix = "Bearer ";
+ if(authorizationHeaderValue.startsWith(basicPrefix))
+ {
+ authorizationHeaderValue = authorizationHeaderValue.replaceFirst(basicPrefix, "");
+ authenticationContext.put(BASIC_AUTH_NAME, authorizationHeaderValue);
+ needToSetSessionIdCookie = true;
+ }
+ else if(authorizationHeaderValue.startsWith(bearerPrefix))
+ {
+ authorizationHeaderValue = authorizationHeaderValue.replaceFirst(bearerPrefix, "");
+ authenticationContext.put(SESSION_ID_COOKIE_NAME, authorizationHeaderValue);
+ }
+ else
+ {
+ LOG.debug("Authorization header value did not have Basic or Bearer prefix. [" + authorizationHeaderValue + "]");
+ }
}
else
{
- String authorizationHeaderValue = context.header("Authorization");
- if(authorizationHeaderValue != null)
- {
- String bearerPrefix = "Bearer ";
- if(authorizationHeaderValue.startsWith(bearerPrefix))
- {
- authorizationHeaderValue = authorizationHeaderValue.replaceFirst(bearerPrefix, "");
- }
- authenticationContext.put(SESSION_ID_COOKIE_NAME, authorizationHeaderValue);
- }
+ LOG.debug("Neither [" + SESSION_ID_COOKIE_NAME + "] cookie nor [Authorization] header was present in request.");
}
QSession session = authenticationModule.createSession(qInstance, authenticationContext);
@@ -363,7 +383,7 @@ public class QJavalinImplementation
/////////////////////////////////////////////////////////////////////////////////
// if we got a session id cookie in, then send it back with updated cookie age //
/////////////////////////////////////////////////////////////////////////////////
- if(StringUtils.hasContent(sessionIdCookieValue))
+ if(needToSetSessionIdCookie)
{
context.cookie(SESSION_ID_COOKIE_NAME, session.getIdReference(), SESSION_COOKIE_AGE);
}
@@ -375,10 +395,12 @@ public class QJavalinImplementation
////////////////////////////////////////////////////////////////////////////////
// if exception caught, clear out the cookie so the frontend will reauthorize //
////////////////////////////////////////////////////////////////////////////////
- if(authenticationModule instanceof Auth0AuthenticationModule)
+ if(needToSetSessionIdCookie)
{
context.removeCookie(SESSION_ID_COOKIE_NAME);
}
+
+ throw (qae);
}
}
diff --git a/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementationAuthenticationTest.java b/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementationAuthenticationTest.java
new file mode 100644
index 00000000..bbfee5dc
--- /dev/null
+++ b/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementationAuthenticationTest.java
@@ -0,0 +1,224 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2022. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.javalin;
+
+
+import java.time.ZonedDateTime;
+import java.util.Base64;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+import com.kingsrook.qqq.backend.core.exceptions.QInstanceValidationException;
+import com.kingsrook.qqq.backend.core.model.data.QRecord;
+import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
+import com.kingsrook.qqq.backend.core.model.metadata.authentication.TableBasedAuthenticationMetaData;
+import com.kingsrook.qqq.backend.core.modules.authentication.implementations.TableBasedAuthenticationModule;
+import com.kingsrook.qqq.backend.core.utils.JsonUtils;
+import com.kingsrook.qqq.backend.core.utils.SleepUtils;
+import kong.unirest.Cookie;
+import kong.unirest.Cookies;
+import kong.unirest.HttpResponse;
+import kong.unirest.Unirest;
+import org.json.JSONObject;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.fail;
+
+
+/*******************************************************************************
+ ** Tests of QJavalinImplementation, but specifically, of the authentication
+ ** code - which uses a different qInstance, and hence javalin server instance
+ ** than the other tests in this package - hence its own before/after, etc.
+ *******************************************************************************/
+public class QJavalinImplementationAuthenticationTest extends QJavalinTestBase
+{
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @BeforeEach
+ public void beforeEach() throws QInstanceValidationException
+ {
+ Unirest.config().reset().enableCookieManagement(false);
+ setupTableBasedAuthenticationInstance();
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @AfterAll
+ public static void afterAll()
+ {
+ if(qJavalinImplementation != null)
+ {
+ qJavalinImplementation.stopJavalinServer();
+ }
+ Unirest.config().reset();
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testAuthentication_noCredentialsProvided()
+ {
+ HttpResponse response = Unirest.get(BASE_URL + "/metaData").asString();
+ assertEquals(401, response.getStatus());
+ JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody());
+ assertNotNull(jsonObject);
+ assertEquals("Session ID was not provided", jsonObject.getString("error"));
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testAuthentication_basicAuthSuccess()
+ {
+ HttpResponse response = Unirest.get(BASE_URL + "/metaData")
+ .header("Authorization", "Basic " + encodeBasicAuth("juser", "987zyx"))
+ .asString();
+ assertEquals(200, response.getStatus());
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testAuthentication_basicAuthBadCredentials()
+ {
+ HttpResponse response = Unirest.get(BASE_URL + "/metaData")
+ .header("Authorization", "Basic " + encodeBasicAuth("not-juser", "987zyx"))
+ .asString();
+ assertEquals(401, response.getStatus());
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testAuthentication_authorizationNotBasic()
+ {
+ HttpResponse response = Unirest.get(BASE_URL + "/metaData")
+ .header("Authorization", "not-Basic " + encodeBasicAuth("juser", "987zyx"))
+ .asString();
+ assertEquals(401, response.getStatus());
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testAuthentication_basicAuthSuccessThenSessionIdFromCookie()
+ {
+ HttpResponse response = Unirest.get(BASE_URL + "/metaData")
+ .header("Authorization", "Basic " + encodeBasicAuth("juser", "987zyx"))
+ .asString();
+ assertEquals(200, response.getStatus());
+
+ Cookies cookies = response.getCookies();
+ String sessionId = cookies.getNamed("sessionId").getValue();
+ ZonedDateTime originalExpiration = cookies.getNamed("sessionId").getExpiration();
+ assertNotNull(sessionId);
+
+ SleepUtils.sleep(1, TimeUnit.SECONDS);
+
+ response = Unirest.get(BASE_URL + "/metaData")
+ .cookie(new Cookie("sessionId", sessionId))
+ .asString();
+ assertEquals(200, response.getStatus());
+ assertEquals(sessionId, response.getCookies().getNamed("sessionId").getValue());
+ assertNotEquals(originalExpiration, response.getCookies().getNamed("sessionId").getExpiration());
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testAuthentication_badSessionIdCookie()
+ {
+ HttpResponse response = Unirest.get(BASE_URL + "/metaData")
+ .cookie(new Cookie("sessionId", "not-a-sessionId"))
+ .asString();
+ assertEquals(401, response.getStatus());
+ JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody());
+ assertEquals("Session not found.", jsonObject.getString("error"));
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ static void setupTableBasedAuthenticationInstance() throws QInstanceValidationException
+ {
+ QInstance qInstance = TestUtils.defineInstance();
+ TableBasedAuthenticationMetaData tableBasedAuthenticationMetaData = new TableBasedAuthenticationMetaData();
+ qInstance.addTable(tableBasedAuthenticationMetaData.defineStandardUserTable(TestUtils.BACKEND_NAME_MEMORY));
+ qInstance.addTable(tableBasedAuthenticationMetaData.defineStandardSessionTable(TestUtils.BACKEND_NAME_MEMORY));
+
+ try
+ {
+ TestUtils.insertRecords(qInstance, qInstance.getTable("user"), List.of(new QRecord()
+ .withValue("username", "juser")
+ .withValue("fullName", "Johnny User")
+ .withValue("passwordHash", TableBasedAuthenticationModule.PasswordHasher.createHashedPassword("987zyx"))));
+ }
+ catch(Exception e)
+ {
+ fail("Error inserting test user.");
+ }
+
+ qInstance.setAuthentication(tableBasedAuthenticationMetaData);
+
+ restartServerWithInstance(qInstance);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private String encodeBasicAuth(String username, String password)
+ {
+ Base64.Encoder encoder = Base64.getEncoder();
+ String originalString = username + ":" + password;
+ return (encoder.encodeToString(originalString.getBytes()));
+ }
+}
diff --git a/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinTestBase.java b/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinTestBase.java
index 571013fa..2eb91ff1 100644
--- a/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinTestBase.java
+++ b/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinTestBase.java
@@ -23,7 +23,10 @@ package com.kingsrook.qqq.backend.javalin;
import com.kingsrook.qqq.backend.core.exceptions.QInstanceValidationException;
+import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
+import com.kingsrook.qqq.backend.core.modules.backend.implementations.memory.MemoryRecordStore;
import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
@@ -36,7 +39,19 @@ public class QJavalinTestBase
private static final int PORT = 6262;
protected static final String BASE_URL = "http://localhost:" + PORT;
- private static QJavalinImplementation qJavalinImplementation;
+ protected static QJavalinImplementation qJavalinImplementation;
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @BeforeEach
+ @AfterEach
+ void beforeAndAfterEach()
+ {
+ MemoryRecordStore.fullReset();
+ }
@@ -76,4 +91,20 @@ public class QJavalinTestBase
TestUtils.primeTestDatabase();
}
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ static protected void restartServerWithInstance(QInstance qInstance) throws QInstanceValidationException
+ {
+ if(qJavalinImplementation != null)
+ {
+ qJavalinImplementation.stopJavalinServer();
+ }
+ qJavalinImplementation = new QJavalinImplementation(qInstance);
+ QJavalinProcessHandler.setAsyncStepTimeoutMillis(250);
+ qJavalinImplementation.startJavalinServer(PORT);
+ }
+
}
diff --git a/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/TestUtils.java b/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/TestUtils.java
index 3d9d44e6..8a2eeb89 100644
--- a/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/TestUtils.java
+++ b/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/TestUtils.java
@@ -26,16 +26,20 @@ import java.io.InputStream;
import java.sql.Connection;
import java.util.List;
import com.kingsrook.qqq.backend.core.actions.processes.BackendStep;
+import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.exceptions.QValueException;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput;
+import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
+import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.QAuthenticationType;
import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
+import com.kingsrook.qqq.backend.core.model.metadata.authentication.QAuthenticationMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeType;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeUsage;
@@ -59,7 +63,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.reporting.ReportType;
import com.kingsrook.qqq.backend.core.model.metadata.tables.AssociatedScript;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.scripts.ScriptsMetaDataProvider;
-import com.kingsrook.qqq.backend.core.modules.authentication.metadata.QAuthenticationMetaData;
+import com.kingsrook.qqq.backend.core.model.session.QSession;
import com.kingsrook.qqq.backend.core.processes.implementations.mock.MockBackendStep;
import com.kingsrook.qqq.backend.module.rdbms.jdbc.ConnectionManager;
import com.kingsrook.qqq.backend.module.rdbms.jdbc.QueryManager;
@@ -74,6 +78,8 @@ import static org.junit.jupiter.api.Assertions.assertNotNull;
*******************************************************************************/
public class TestUtils
{
+ public static final String BACKEND_NAME_MEMORY = "memory";
+
public static final String TABLE_NAME_PERSON = "person";
public static final String PROCESS_NAME_GREET_PEOPLE_INTERACTIVE = "greetInteractive";
@@ -209,7 +215,7 @@ public class TestUtils
{
return new QBackendMetaData()
.withBackendType("memory")
- .withName("memory");
+ .withName(BACKEND_NAME_MEMORY);
}
@@ -504,4 +510,18 @@ public class TestUtils
);
}
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public static void insertRecords(QInstance qInstance, QTableMetaData table, List records) throws QException
+ {
+ InsertInput insertInput = new InsertInput(qInstance);
+ insertInput.setSession(new QSession());
+ insertInput.setTableName(table.getName());
+ insertInput.setRecords(records);
+ new InsertAction().execute(insertInput);
+ }
+
}
diff --git a/qqq-middleware-picocli/src/main/java/com/kingsrook/qqq/frontend/picocli/QPicoCliImplementation.java b/qqq-middleware-picocli/src/main/java/com/kingsrook/qqq/frontend/picocli/QPicoCliImplementation.java
index ae0eb6bd..deebfbad 100644
--- a/qqq-middleware-picocli/src/main/java/com/kingsrook/qqq/frontend/picocli/QPicoCliImplementation.java
+++ b/qqq-middleware-picocli/src/main/java/com/kingsrook/qqq/frontend/picocli/QPicoCliImplementation.java
@@ -82,9 +82,9 @@ import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.session.QSession;
-import com.kingsrook.qqq.backend.core.modules.authentication.Auth0AuthenticationModule;
import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModuleDispatcher;
import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModuleInterface;
+import com.kingsrook.qqq.backend.core.modules.authentication.implementations.Auth0AuthenticationModule;
import com.kingsrook.qqq.backend.core.utils.JsonUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import io.github.cdimascio.dotenv.Dotenv;
@@ -676,7 +676,7 @@ public class QPicoCliImplementation
{
QQueryFilter filter = new QQueryFilter();
- String[] criteria = subParseResult.matchedOptionValue("criteria", new String[] {});
+ String[] criteria = subParseResult.matchedOptionValue("criteria", new String[] { });
for(String criterion : criteria)
{
// todo - parse!
@@ -803,7 +803,7 @@ public class QPicoCliImplementation
boolean anyFields = false;
String primaryKeyOption = subParseResult.matchedOptionValue("--primaryKey", "");
- String[] criteria = subParseResult.matchedOptionValue("criteria", new String[] {});
+ String[] criteria = subParseResult.matchedOptionValue("criteria", new String[] { });
if(StringUtils.hasContent(primaryKeyOption))
{
@@ -896,7 +896,7 @@ public class QPicoCliImplementation
// get the pKeys that the user specified //
/////////////////////////////////////////////
String primaryKeyOption = subParseResult.matchedOptionValue("--primaryKey", "");
- String[] criteria = subParseResult.matchedOptionValue("criteria", new String[] {});
+ String[] criteria = subParseResult.matchedOptionValue("criteria", new String[] { });
if(StringUtils.hasContent(primaryKeyOption))
{
diff --git a/qqq-middleware-picocli/src/test/java/com/kingsrook/qqq/frontend/picocli/TestUtils.java b/qqq-middleware-picocli/src/test/java/com/kingsrook/qqq/frontend/picocli/TestUtils.java
index fb2aced0..13998c96 100644
--- a/qqq-middleware-picocli/src/test/java/com/kingsrook/qqq/frontend/picocli/TestUtils.java
+++ b/qqq-middleware-picocli/src/test/java/com/kingsrook/qqq/frontend/picocli/TestUtils.java
@@ -27,7 +27,7 @@ import java.sql.Connection;
import java.util.List;
import com.kingsrook.qqq.backend.core.model.metadata.QAuthenticationType;
import com.kingsrook.qqq.backend.core.processes.implementations.mock.MockBackendStep;
-import com.kingsrook.qqq.backend.core.modules.authentication.metadata.QAuthenticationMetaData;
+import com.kingsrook.qqq.backend.core.model.metadata.authentication.QAuthenticationMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeType;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeUsage;
diff --git a/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/SampleMetaDataProvider.java b/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/SampleMetaDataProvider.java
index 1436b819..297488ee 100644
--- a/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/SampleMetaDataProvider.java
+++ b/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/SampleMetaDataProvider.java
@@ -37,6 +37,7 @@ import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInpu
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput;
import com.kingsrook.qqq.backend.core.model.metadata.QAuthenticationType;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
+import com.kingsrook.qqq.backend.core.model.metadata.authentication.QAuthenticationMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.branding.QBrandingMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeType;
@@ -60,7 +61,6 @@ import com.kingsrook.qqq.backend.core.model.metadata.processes.QRecordListMetaDa
import com.kingsrook.qqq.backend.core.model.metadata.tables.QFieldSection;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.Tier;
-import com.kingsrook.qqq.backend.core.modules.authentication.metadata.QAuthenticationMetaData;
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.ExtractViaQueryStep;
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.LoadViaInsertStep;
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.StreamedETLWithFrontendProcess;