Add table-based authentication module; update javalin to support Authentication: Basic header; Move authentication classes

This commit is contained in:
2022-12-28 16:52:04 -06:00
parent 428f48602b
commit 7fae3e2329
28 changed files with 1872 additions and 65 deletions

View File

@ -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;

View File

@ -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");

View File

@ -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;

View File

@ -19,11 +19,13 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
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());
}

View File

@ -19,7 +19,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.kingsrook.qqq.backend.core.modules.authentication.metadata;
package com.kingsrook.qqq.backend.core.model.metadata.authentication;
import java.util.HashMap;

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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);
}
}

View File

@ -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<String, String> authenticationTypeToModuleClassNameMap;
private static Map<String, String> 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);
}

View File

@ -19,7 +19,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
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));
}
}
//////////////////////////////////////////////////

View File

@ -19,7 +19,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
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;
/*******************************************************************************

View File

@ -19,7 +19,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
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;

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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<String, String> 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<Instant> 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;
}
}
}

View File

@ -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...
*******************************************************************************/

View File

@ -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;

View File

@ -19,7 +19,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
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;

View File

@ -19,7 +19,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
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;

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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<String, Integer> 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<String, Integer> 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);
}
}

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;
/*******************************************************************************

View File

@ -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<String, String> 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);
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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<String> 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<String> response = Unirest.get(BASE_URL + "/metaData")
.header("Authorization", "Basic " + encodeBasicAuth("juser", "987zyx"))
.asString();
assertEquals(200, response.getStatus());
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testAuthentication_basicAuthBadCredentials()
{
HttpResponse<String> response = Unirest.get(BASE_URL + "/metaData")
.header("Authorization", "Basic " + encodeBasicAuth("not-juser", "987zyx"))
.asString();
assertEquals(401, response.getStatus());
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testAuthentication_authorizationNotBasic()
{
HttpResponse<String> response = Unirest.get(BASE_URL + "/metaData")
.header("Authorization", "not-Basic " + encodeBasicAuth("juser", "987zyx"))
.asString();
assertEquals(401, response.getStatus());
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testAuthentication_basicAuthSuccessThenSessionIdFromCookie()
{
HttpResponse<String> 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<String> 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()));
}
}

View File

@ -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);
}
}

View File

@ -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<QRecord> records) throws QException
{
InsertInput insertInput = new InsertInput(qInstance);
insertInput.setSession(new QSession());
insertInput.setTableName(table.getName());
insertInput.setRecords(records);
new InsertAction().execute(insertInput);
}
}

View File

@ -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))
{

View File

@ -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;

View File

@ -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;