mirror of
https://github.com/Kingsrook/qqq.git
synced 2025-07-17 20:50:44 +00:00
Add table-based authentication module; update javalin to support Authentication: Basic header; Move authentication classes
This commit is contained in:
@ -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;
|
||||
|
||||
|
@ -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");
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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());
|
||||
}
|
||||
|
||||
|
@ -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;
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////
|
@ -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;
|
||||
|
||||
|
||||
/*******************************************************************************
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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...
|
||||
*******************************************************************************/
|
||||
|
@ -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;
|
||||
|
@ -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;
|
@ -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;
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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()));
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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))
|
||||
{
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
Reference in New Issue
Block a user