CE-937 Add concept of customizers to authentication modules; basic implementation within auth0 for customizing session/security keys

This commit is contained in:
2024-02-26 14:29:45 -06:00
parent 29a54f5293
commit 878f374cb5
6 changed files with 349 additions and 42 deletions

View File

@ -153,6 +153,7 @@ public class QInstanceValidator
try
{
validateBackends(qInstance);
validateAuthentication(qInstance);
validateAutomationProviders(qInstance);
validateTables(qInstance, joinGraph);
validateProcesses(qInstance);
@ -383,6 +384,23 @@ public class QInstanceValidator
/*******************************************************************************
**
*******************************************************************************/
private void validateAuthentication(QInstance qInstance)
{
QAuthenticationMetaData authentication = qInstance.getAuthentication();
if(authentication != null)
{
if(authentication.getCustomizer() != null)
{
validateSimpleCodeReference("Instance Authentication meta data customizer ", authentication.getCustomizer(), QAuthenticationModuleCustomizerInterface.class);
}
}
}
/*******************************************************************************
**
*******************************************************************************/

View File

@ -28,6 +28,7 @@ import com.fasterxml.jackson.annotation.JsonFilter;
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.TopLevelMetaDataInterface;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
/*******************************************************************************
@ -43,6 +44,7 @@ public class QAuthenticationMetaData implements TopLevelMetaDataInterface
@JsonFilter("secretsFilter")
private Map<String, String> values;
private QCodeReference customizer;
/*******************************************************************************
@ -192,4 +194,35 @@ public class QAuthenticationMetaData implements TopLevelMetaDataInterface
qInstance.setAuthentication(this);
}
/*******************************************************************************
** Getter for customizer
*******************************************************************************/
public QCodeReference getCustomizer()
{
return (this.customizer);
}
/*******************************************************************************
** Setter for customizer
*******************************************************************************/
public void setCustomizer(QCodeReference customizer)
{
this.customizer = customizer;
}
/*******************************************************************************
** Fluent setter for customizer
*******************************************************************************/
public QAuthenticationMetaData withCustomizer(QCodeReference customizer)
{
this.customizer = customizer;
return (this);
}
}

View File

@ -0,0 +1,55 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2024. 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;
import java.io.Serializable;
import java.util.Map;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.session.QSession;
/*******************************************************************************
** Interface for customizing behavior of an Authentication module.
*******************************************************************************/
public interface QAuthenticationModuleCustomizerInterface
{
/*******************************************************************************
**
*******************************************************************************/
default void addSecurityKeyValueToSession(QSession session, String keyName, Serializable value)
{
session.withSecurityKeyValue(keyName, value);
}
/*******************************************************************************
**
*******************************************************************************/
default void customizeSession(QInstance qInstance, QSession qSession, Map<String, Object> context)
{
//////////
// noop //
//////////
}
}

View File

@ -47,6 +47,7 @@ import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.exceptions.TokenExpiredException;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.auth0.net.Response;
import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader;
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.QueryAction;
@ -71,6 +72,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.authentication.Auth0Authent
import com.kingsrook.qqq.backend.core.model.metadata.security.QSecurityKeyType;
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.QAuthenticationModuleCustomizerInterface;
import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModuleInterface;
import com.kingsrook.qqq.backend.core.modules.authentication.implementations.model.UserSession;
import com.kingsrook.qqq.backend.core.state.InMemoryStateProvider;
@ -80,6 +82,7 @@ import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.JsonUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
import com.kingsrook.qqq.backend.core.utils.memoization.Memoization;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
@ -129,6 +132,19 @@ public class Auth0AuthenticationModule implements QAuthenticationModuleInterface
static final String EXPIRED_TOKEN_ERROR = "Token has expired";
static final String INVALID_TOKEN_ERROR = "An invalid token was provided";
private Auth0AuthenticationMetaData metaData;
//////////////////////////////////////////////////////////////////////////////////
// do not use this var directly - rather - always call the getCustomizer method //
//////////////////////////////////////////////////////////////////////////////////
private QAuthenticationModuleCustomizerInterface _customizer = null;
private boolean customizerHasBeenRequested = false;
private static boolean mayMemoize = true;
private static final Memoization<String, String> getAccessTokenFromSessionUUIDMemoization = new Memoization<String, String>()
.withTimeout(Duration.of(1, ChronoUnit.MINUTES))
.withMaxSize(1000);
////////////////////////////////////////////////////////////////////////////////////////////////////////////
// this is how we allow the actions within this class to work without themselves having a logged-in user. //
@ -165,9 +181,12 @@ public class Auth0AuthenticationModule implements QAuthenticationModuleInterface
@Override
public QSession createSession(QInstance qInstance, Map<String, String> context) throws QAuthenticationException
{
QInstance contextInstanceBefore = QContext.getQInstance();
try
{
Auth0AuthenticationMetaData metaData = (Auth0AuthenticationMetaData) qInstance.getAuthentication();
QContext.setQInstance(qInstance);
this.metaData = (Auth0AuthenticationMetaData) qInstance.getAuthentication();
String accessToken = null;
if(CollectionUtils.containsKeyWithNonNullValue(context, SESSION_UUID_KEY))
@ -252,16 +271,6 @@ public class Auth0AuthenticationModule implements QAuthenticationModuleInterface
}
}
/* todo confirm this is deprecated
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// check to see if the session id is a UUID, if so, that means we need to look up the 'actual' token in the access_token table //
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
if(accessToken != null && StringUtils.isUUID(accessToken))
{
accessToken = lookupActualAccessToken(metaData, accessToken);
}
*/
///////////////////////////////////////////
// if token wasn't found by now, give up //
///////////////////////////////////////////
@ -311,6 +320,10 @@ public class Auth0AuthenticationModule implements QAuthenticationModuleInterface
LOG.error(message, e);
throw (new QAuthenticationException(message));
}
finally
{
QContext.setQInstance(contextInstanceBefore);
}
}
@ -346,26 +359,36 @@ public class Auth0AuthenticationModule implements QAuthenticationModuleInterface
*******************************************************************************/
private QSession buildAndValidateSession(QInstance qInstance, String accessToken) throws JwkException
{
QSession qSession = buildQSessionFromToken(accessToken, qInstance);
if(isSessionValid(qInstance, qSession))
QSession beforeSession = QContext.getQSession();
try
{
QContext.setQSession(getChickenAndEggSession());
QSession qSession = buildQSessionFromToken(accessToken, qInstance);
if(isSessionValid(qInstance, qSession))
{
return (qSession);
}
//////////////////////////////////////////////////////////////////////////////////////////
// if we make it here it means we have never validated this token or it has been a long //
// enough duration so we need to re-verify the token //
//////////////////////////////////////////////////////////////////////////////////////////
qSession = revalidateTokenAndBuildSession(qInstance, accessToken);
/////////////////////////////////////////////////////////////////////
// put now into state so we don't check until next interval passes //
/////////////////////////////////////////////////////////////////////
StateProviderInterface spi = getStateProvider();
SimpleStateKey<String> key = new SimpleStateKey<>(qSession.getIdReference());
spi.put(key, Instant.now());
return (qSession);
}
//////////////////////////////////////////////////////////////////////////////////////////
// if we make it here it means we have never validated this token or it has been a long //
// enough duration so we need to re-verify the token //
//////////////////////////////////////////////////////////////////////////////////////////
qSession = revalidateTokenAndBuildSession(qInstance, accessToken);
/////////////////////////////////////////////////////////////////////
// put now into state so we don't check until next interval passes //
/////////////////////////////////////////////////////////////////////
StateProviderInterface spi = getStateProvider();
SimpleStateKey<String> key = new SimpleStateKey<>(qSession.getIdReference());
spi.put(key, Instant.now());
return (qSession);
finally
{
QContext.setQSession(beforeSession);
}
}
@ -509,7 +532,7 @@ public class Auth0AuthenticationModule implements QAuthenticationModuleInterface
*******************************************************************************/
private void validateToken(QInstance qInstance, String tokenString) throws JwkException
{
Auth0AuthenticationMetaData metaData = (Auth0AuthenticationMetaData) qInstance.getAuthentication();
this.metaData = (Auth0AuthenticationMetaData) qInstance.getAuthentication();
DecodedJWT idToken = JWT.decode(tokenString);
JwkProvider provider = new UrlJwkProvider(metaData.getBaseUrl());
@ -567,10 +590,10 @@ public class Auth0AuthenticationModule implements QAuthenticationModuleInterface
qSession.setIdReference(accessToken);
qSession.setUser(qUser);
/////////////////////////////////////////////////////////////////////
// put the user id reference in security key value for usierId key //
/////////////////////////////////////////////////////////////////////
qSession.withSecurityKeyValue("userId", qUser.getIdReference());
////////////////////////////////////////////////////////////////////
// put the user id reference in security key value for userId key //
////////////////////////////////////////////////////////////////////
addSecurityKeyValueToSession(qSession, "userId", qUser.getIdReference());
/////////////////////////////////////////////////
// set permissions in the session from the JWT //
@ -581,12 +604,43 @@ public class Auth0AuthenticationModule implements QAuthenticationModuleInterface
// set security keys in the session from the JWT //
///////////////////////////////////////////////////
setSecurityKeysInSessionFromJwtPayload(qInstance, payload, qSession);
//////////////////////////////////////////////////////////////
// allow customizer to do custom things here, if so desired //
//////////////////////////////////////////////////////////////
if(getCustomizer() != null)
{
getCustomizer().customizeSession(qInstance, qSession, Map.of("jwtPayloadJsonObject", payload));
}
return (qSession);
}
/*******************************************************************************
**
*******************************************************************************/
private void addSecurityKeyValueToSession(QSession qSession, String key, String value)
{
if(getCustomizer() == null)
{
///////////////////////////////////////////////////
// if there's no customizer, do the direct thing //
///////////////////////////////////////////////////
qSession.withSecurityKeyValue(key, value);
}
else
{
///////////////////////////////////////////////////////////
// else have the customizer add the value to the session //
///////////////////////////////////////////////////////////
getCustomizer().addSecurityKeyValueToSession(qSession, key, value);
}
}
/*******************************************************************************
**
*******************************************************************************/
@ -616,7 +670,7 @@ public class Auth0AuthenticationModule implements QAuthenticationModuleInterface
/*******************************************************************************
**
*******************************************************************************/
static void setSecurityKeysInSessionFromJwtPayload(QInstance qInstance, JSONObject payload, QSession qSession)
void setSecurityKeysInSessionFromJwtPayload(QInstance qInstance, JSONObject payload, QSession qSession)
{
for(String payloadKey : List.of("com.kingsrook.qqq.app_metadata", "com.kingsrook.qqq.client_metadata"))
{
@ -668,7 +722,7 @@ public class Auth0AuthenticationModule implements QAuthenticationModuleInterface
/*******************************************************************************
**
*******************************************************************************/
private static void setSecurityKeyValuesFromToken(Set<String> allowedSecurityKeyNames, QSession qSession, String securityKeyName, JSONObject securityKeyValues, String jsonKey)
private void setSecurityKeyValuesFromToken(Set<String> allowedSecurityKeyNames, QSession qSession, String securityKeyName, JSONObject securityKeyValues, String jsonKey)
{
if(!allowedSecurityKeyNames.contains(securityKeyName))
{
@ -686,7 +740,7 @@ public class Auth0AuthenticationModule implements QAuthenticationModuleInterface
Object optValue = valueArray.opt(i);
if(optValue != null)
{
qSession.withSecurityKeyValue(securityKeyName, ValueUtils.getValueAsString(optValue));
addSecurityKeyValueToSession(qSession, securityKeyName, ValueUtils.getValueAsString(optValue));
}
}
}
@ -697,7 +751,7 @@ public class Auth0AuthenticationModule implements QAuthenticationModuleInterface
{
for(String v : values.split(","))
{
qSession.withSecurityKeyValue(securityKeyName, v);
addSecurityKeyValueToSession(qSession, securityKeyName, v);
}
}
}
@ -815,6 +869,25 @@ public class Auth0AuthenticationModule implements QAuthenticationModuleInterface
**
*******************************************************************************/
private String getAccessTokenFromSessionUUID(Auth0AuthenticationMetaData metaData, String sessionUUID) throws QAuthenticationException
{
if(mayMemoize)
{
return getAccessTokenFromSessionUUIDMemoization.getResultThrowing(sessionUUID, (String x) ->
doGetAccessTokenFromSessionUUID(sessionUUID)
).orElse(null);
}
else
{
return (doGetAccessTokenFromSessionUUID(sessionUUID));
}
}
/*******************************************************************************
**
*******************************************************************************/
private String doGetAccessTokenFromSessionUUID(String sessionUUID) throws QAuthenticationException
{
String accessToken = null;
QSession beforeSession = QContext.getQSession();
@ -982,4 +1055,39 @@ public class Auth0AuthenticationModule implements QAuthenticationModuleInterface
}
}
/*******************************************************************************
**
*******************************************************************************/
private QAuthenticationModuleCustomizerInterface getCustomizer()
{
try
{
if(!customizerHasBeenRequested)
{
customizerHasBeenRequested = true;
if(this.metaData == null)
{
this.metaData = (Auth0AuthenticationMetaData) QContext.getQInstance().getAuthentication();
}
if(this.metaData.getCustomizer() != null)
{
this._customizer = QCodeLoader.getAdHoc(QAuthenticationModuleCustomizerInterface.class, this.metaData.getCustomizer());
}
}
return (this._customizer);
}
catch(Exception e)
{
////////////////////////
// should this throw? //
////////////////////////
LOG.warn("Error getting customizer.", e);
return (null);
}
}
}