mirror of
https://github.com/Kingsrook/qqq.git
synced 2025-07-20 22:18:43 +00:00
CE-937 Add concept of customizers to authentication modules; basic implementation within auth0 for customizing session/security keys
This commit is contained in:
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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 //
|
||||
//////////
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user