From 8479ef4b90602a5ecd7b351585849f2e2c7f9218 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Tue, 1 Aug 2023 08:58:47 -0500 Subject: [PATCH 1/7] Initial WIP Checkpoint of auth0 userSessions --- .../qqq/backend/core/context/QContext.java | 2 +- .../Auth0AuthenticationMetaData.java | 9 +- .../Auth0AuthenticationModule.java | 421 ++++++++++-------- .../metadata/UserSessionMetaDataProducer.java | 73 +++ .../implementations/model/UserSession.java | 262 +++++++++++ .../backend/core/utils/CollectionUtils.java | 15 + .../Auth0AuthenticationModuleTest.java | 12 +- .../qqq/api/javalin/QJavalinApiHandler.java | 105 ----- .../javalin/QJavalinImplementation.java | 54 ++- .../picocli/QPicoCliImplementation.java | 15 +- 10 files changed, 648 insertions(+), 320 deletions(-) create mode 100644 qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/metadata/UserSessionMetaDataProducer.java create mode 100644 qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/model/UserSession.java diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/context/QContext.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/context/QContext.java index 4c7bcbba..80515a5e 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/context/QContext.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/context/QContext.java @@ -84,7 +84,7 @@ public class QContext actionStackThreadLocal.get().add(actionInput); } - if(!qInstance.getHasBeenValidated()) + if(qInstance != null && !qInstance.getHasBeenValidated()) { try { diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/authentication/Auth0AuthenticationMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/authentication/Auth0AuthenticationMetaData.java index c0bc5e09..1e8f341d 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/authentication/Auth0AuthenticationMetaData.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/authentication/Auth0AuthenticationMetaData.java @@ -60,7 +60,6 @@ public class Auth0AuthenticationMetaData extends QAuthenticationMetaData private String auth0ClientSecretField; private Serializable qqqRecordIdField; - ///////////////////////////////////// // fields on the accessToken table // ///////////////////////////////////// @@ -70,6 +69,14 @@ public class Auth0AuthenticationMetaData extends QAuthenticationMetaData private String qqqApiKeyField; private String expiresInSecondsField; + ///////////////////////////////////////////////////////////////////////////////// + // table for storing user sessions, and field names we work with on that table // + ///////////////////////////////////////////////////////////////////////////////// + private String userSessionTableName; + private String userSessionUuidField; + private String userSessionUserIdField; + private String userSessionAccessTokenField; + /******************************************************************************* diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/Auth0AuthenticationModule.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/Auth0AuthenticationModule.java index 315cff73..e9f3b732 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/Auth0AuthenticationModule.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/Auth0AuthenticationModule.java @@ -33,7 +33,6 @@ import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; -import java.util.UUID; import com.auth0.client.auth.AuthAPI; import com.auth0.exception.Auth0Exception; import com.auth0.json.auth.TokenHolder; @@ -52,6 +51,7 @@ 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; import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction; +import com.kingsrook.qqq.backend.core.context.CapturedContext; import com.kingsrook.qqq.backend.core.context.QContext; import com.kingsrook.qqq.backend.core.exceptions.AccessTokenException; import com.kingsrook.qqq.backend.core.exceptions.QAuthenticationException; @@ -68,11 +68,11 @@ 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.Auth0AuthenticationMetaData; -import com.kingsrook.qqq.backend.core.model.metadata.authentication.QAuthenticationMetaData; 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.QAuthenticationModuleInterface; +import com.kingsrook.qqq.backend.core.modules.authentication.implementations.model.UserSession; import com.kingsrook.qqq.backend.core.state.InMemoryStateProvider; import com.kingsrook.qqq.backend.core.state.SimpleStateKey; import com.kingsrook.qqq.backend.core.state.StateProviderInterface; @@ -80,7 +80,6 @@ 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 org.apache.http.HttpStatus; import org.apache.http.client.entity.UrlEncodedFormEntity; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpPost; @@ -93,6 +92,18 @@ import org.json.JSONObject; /******************************************************************************* + ** QQQ AuthenticationModule for working with Auth0. + ** + ** createSession can be called with the following fields in its context: + ** + ** System-User session use-case: + ** 1: Takes in an "accessToken" (but doesn't store a userSession record). + ** + ** Web User session use-cases: + ** 2: creates a new session (userSession record) by taking an "accessToken" + ** 3: looks up an existing session (userSession record) by taking a "sessionUUID" + ** 4: takes an "apiKey" (looked up in metaData.AccessTokenTableName - refreshing accessToken with auth0 if needed). + ** 5: takes a "basicAuthString" (encoded username:password), which make a new accessToken in auth0 ** *******************************************************************************/ public class Auth0AuthenticationModule implements QAuthenticationModuleInterface @@ -104,14 +115,17 @@ public class Auth0AuthenticationModule implements QAuthenticationModuleInterface ///////////////////////////////////////////////////////////////////////////////////////////////////////////// public static final int ID_TOKEN_VALIDATION_INTERVAL_SECONDS = 1800; - public static final String AUTH0_ACCESS_TOKEN_KEY = "sessionId"; - public static final String API_KEY = "apiKey"; - public static final String BASIC_AUTH_KEY = "basicAuthString"; + public static final String ACCESS_TOKEN_KEY = "accessToken"; + public static final String API_KEY = "apiKey"; // todo - look for users of this, see if we can change to use this constant; maybe move constants up? + public static final String SESSION_UUID_KEY = "sessionUUID"; + public static final String BASIC_AUTH_KEY = "basicAuthString"; // todo - look for users of this, see if we can change to use this constant; maybe move constants up? - public static final String TOKEN_NOT_PROVIDED_ERROR = "Access Token was not provided"; - public static final String COULD_NOT_DECODE_ERROR = "Unable to decode access token"; - public static final String EXPIRED_TOKEN_ERROR = "Token has expired"; - public static final String INVALID_TOKEN_ERROR = "An invalid token was provided"; + public static final String DO_STORE_USER_SESSION_KEY = "doStoreUserSession"; + + static final String TOKEN_NOT_PROVIDED_ERROR = "Access Token was not provided"; + static final String COULD_NOT_DECODE_ERROR = "Unable to decode access token"; + static final String EXPIRED_TOKEN_ERROR = "Token has expired"; + static final String INVALID_TOKEN_ERROR = "An invalid token was provided"; //////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -149,94 +163,105 @@ public class Auth0AuthenticationModule implements QAuthenticationModuleInterface @Override public QSession createSession(QInstance qInstance, Map context) throws QAuthenticationException { - Auth0AuthenticationMetaData metaData = (Auth0AuthenticationMetaData) qInstance.getAuthentication(); - - /////////////////////////////////////////////////////////// - // check if we are processing a Basic Auth Session first // - /////////////////////////////////////////////////////////// - if(context.containsKey(BASIC_AUTH_KEY)) - { - AuthAPI auth = AuthAPI.newBuilder(metaData.getBaseUrl(), metaData.getClientId(), metaData.getClientSecret()).build(); - try - { - ///////////////////////////////////////////////// - // decode the credentials from the header auth // - ///////////////////////////////////////////////// - String base64Credentials = context.get(BASIC_AUTH_KEY).trim(); - String accessToken = getAccessTokenFromBase64BasicAuthCredentials(metaData, auth, base64Credentials); - context.put(AUTH0_ACCESS_TOKEN_KEY, accessToken); - } - catch(Auth0Exception e) - { - //////////////// - // ¯\_(ツ)_/¯ // - //////////////// - String message = "Error handling basic authentication: " + e.getMessage(); - LOG.error(message, e); - throw (new QAuthenticationException(message)); - } - } - - //////////////////////////////////////////////////////////////////// - // get the jwt id or qqq translated token from the context object // - //////////////////////////////////////////////////////////////////// - String accessToken = context.get(AUTH0_ACCESS_TOKEN_KEY); - - ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - // 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 access token is still null, look for an api key // - //////////////////////////////////////////////////////// - if(accessToken == null) - { - String apiKey = context.get(API_KEY); - if(apiKey != null) - { - accessToken = getAccessTokenFromApiKey(metaData, apiKey); - } - } - - if(accessToken == null) - { - LOG.warn(TOKEN_NOT_PROVIDED_ERROR); - throw (new QAuthenticationException(TOKEN_NOT_PROVIDED_ERROR)); - } - - ////////////////////////////////////////////////////////////////////////////////////// - // decode the token locally to make sure it is valid and to look at when it expires // - ////////////////////////////////////////////////////////////////////////////////////// try { + Auth0AuthenticationMetaData metaData = (Auth0AuthenticationMetaData) qInstance.getAuthentication(); + + ///////////////////////////////////////////////////////////////////////////////////////////// + // if the context contains an access token, then create a new session based on that token. // + ///////////////////////////////////////////////////////////////////////////////////////////// + String accessToken = null; + if(CollectionUtils.containsKeyWithNonNullValue(context, ACCESS_TOKEN_KEY)) + { + accessToken = context.get(ACCESS_TOKEN_KEY); + QSession qSession = buildAndValidateSession(qInstance, accessToken); + + //////////////////////////////////////////////////////////////// + // build & store userSession db record, if requested to do so // + //////////////////////////////////////////////////////////////// + if(CollectionUtils.containsKeyWithNonNullValue(context, DO_STORE_USER_SESSION_KEY)) + { + insertUserSession(qInstance, accessToken, qSession); + } + + return (qSession); + } + else if(CollectionUtils.containsKeyWithNonNullValue(context, BASIC_AUTH_KEY)) + { + ////////////////////////////////////////////////////////////////////////////////////// + // Process a basic auth (username:password) // + // by getting an access token from auth0 (re-using from state provider if possible) // + ////////////////////////////////////////////////////////////////////////////////////// + AuthAPI auth = AuthAPI.newBuilder(metaData.getBaseUrl(), metaData.getClientId(), metaData.getClientSecret()).build(); + try + { + ///////////////////////////////////////////////// + // decode the credentials from the header auth // + ///////////////////////////////////////////////// + String base64Credentials = context.get(BASIC_AUTH_KEY).trim(); + accessToken = getAccessTokenFromBase64BasicAuthCredentials(metaData, auth, base64Credentials); + } + catch(Auth0Exception e) + { + //////////////// + // ¯\_(ツ)_/¯ // + //////////////// + String message = "Error handling basic authentication: " + e.getMessage(); + LOG.error(message, e); + throw (new QAuthenticationException(message)); + } + } + else if(CollectionUtils.containsKeyWithNonNullValue(context, API_KEY)) + { + /////////////////////////////////////////////////////////////////////////////////////// + // process an api key - looks up client application token (creating token if needed) // + /////////////////////////////////////////////////////////////////////////////////////// + String apiKey = context.get(API_KEY); + if(apiKey != null) + { + accessToken = getAccessTokenFromApiKey(metaData, apiKey); + } + } + else if(CollectionUtils.containsKeyWithNonNullValue(context, SESSION_UUID_KEY)) + { + ///////////////////////////////////////////////////////////////////////////////////////// + // process a sessionUUID - looks up userSession record - cannot create token this way. // + ///////////////////////////////////////////////////////////////////////////////////////// + String sessionUUID = context.get(SESSION_UUID_KEY); + if(sessionUUID != null) + { + accessToken = getAccessTokenFromSessionUUID(metaData, sessionUUID); + } + } + + /* 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 // + /////////////////////////////////////////// + if(accessToken == null) + { + LOG.warn(TOKEN_NOT_PROVIDED_ERROR); + throw (new QAuthenticationException(TOKEN_NOT_PROVIDED_ERROR)); + } + ///////////////////////////////////////////////////// // try to build session to see if still valid // // then call method to check more session validity // ///////////////////////////////////////////////////// - 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 its been a long // - // enough duration so we need to re-verify the token // - /////////////////////////////////////////////////////////////////////////////////////// - qSession = revalidateTokenAndBuildSession(qInstance, accessToken); - - //////////////////////////////////////////////////////////////////// - // put now into state so we dont check until next interval passes // - /////////////////////////////////////////////////////////////////// - StateProviderInterface spi = getStateProvider(); - SimpleStateKey key = new SimpleStateKey<>(qSession.getIdReference()); - spi.put(key, Instant.now()); - - return (qSession); + return buildAndValidateSession(qInstance, accessToken); + } + catch(QAuthenticationException qae) + { + throw (qae); } catch(JWTDecodeException jde) { @@ -272,6 +297,61 @@ public class Auth0AuthenticationModule implements QAuthenticationModuleInterface + /******************************************************************************* + ** Insert a session as a new record into userSession table + *******************************************************************************/ + private void insertUserSession(QInstance qInstance, String accessToken, QSession qSession) throws QException + { + CapturedContext capturedContext = QContext.capture(); + try + { + QContext.init(qInstance, null); + QContext.setQSession(getChickenAndEggSession()); + + UserSession userSession = new UserSession() + .withUuid(qSession.getUuid()) + .withUserId(qSession.getUser().getIdReference()) + .withAccessToken(accessToken); + + new InsertAction().execute(new InsertInput(UserSession.TABLE_NAME).withRecordEntity(userSession)); + } + finally + { + QContext.init(capturedContext); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private QSession buildAndValidateSession(QInstance qInstance, String accessToken) throws JwkException + { + 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 key = new SimpleStateKey<>(qSession.getIdReference()); + spi.put(key, Instant.now()); + + return (qSession); + } + + + /******************************************************************************* ** *******************************************************************************/ @@ -299,7 +379,7 @@ public class Auth0AuthenticationModule implements QAuthenticationModuleInterface byte[] credDecoded = Base64.getDecoder().decode(base64Credentials); String credentials = new String(credDecoded, StandardCharsets.UTF_8); - String accessToken = getAccessTokenFromAuth0(metaData, auth, credentials); + String accessToken = getAccessTokenForUsernameAndPasswordFromAuth0(metaData, auth, credentials); stateProvider.put(accessTokenStateKey, accessToken); stateProvider.put(timestampStateKey, Instant.now()); return (accessToken); @@ -310,7 +390,7 @@ public class Auth0AuthenticationModule implements QAuthenticationModuleInterface /******************************************************************************* ** *******************************************************************************/ - protected String getAccessTokenFromAuth0(Auth0AuthenticationMetaData metaData, AuthAPI auth, String credentials) throws Auth0Exception + protected String getAccessTokenForUsernameAndPasswordFromAuth0(Auth0AuthenticationMetaData metaData, AuthAPI auth, String credentials) throws Auth0Exception { ///////////////////////////////////// // call auth0 with a login request // @@ -620,75 +700,11 @@ public class Auth0AuthenticationModule implements QAuthenticationModuleInterface /******************************************************************************* - ** create a new auth0 access token + ** make http request to Auth0 for a new access token for an application - e.g., + ** with a clientId and clientSecret as params ** *******************************************************************************/ - public String createAccessToken(QAuthenticationMetaData metaData, String clientId, String clientSecret) throws AccessTokenException - { - QSession sessionBefore = QContext.getQSession(); - Auth0AuthenticationMetaData auth0MetaData = (Auth0AuthenticationMetaData) metaData; - - try - { - QContext.setQSession(getChickenAndEggSession()); - - /////////////////////////////////////////////////////////////////////////////////////// - // fetch the application from database, will throw accesstokenexception if not found // - /////////////////////////////////////////////////////////////////////////////////////// - QRecord clientAuth0Application = getClientAuth0Application(auth0MetaData, clientId); - - ///////////////////////////////////////////////////////////////////////////////////////////////// - // request access token from auth0 if exception is not thrown, that means 200OK, we want to // - // store the actual access token in the database, and return a unique value // - // back to the user which will be what they use on subseqeunt requests (because token too big) // - ///////////////////////////////////////////////////////////////////////////////////////////////// - JSONObject accessTokenData = requestAccessTokenFromAuth0(auth0MetaData, clientId, clientSecret); - - Integer expiresInSeconds = accessTokenData.getInt("expires_in"); - String accessToken = accessTokenData.getString("access_token"); - String uuid = UUID.randomUUID().toString(); - - ///////////////////////////////// - // store the details in the db // - ///////////////////////////////// - QRecord accessTokenRecord = new QRecord() - .withValue(auth0MetaData.getClientAuth0ApplicationIdField(), clientAuth0Application.getValue("id")) - .withValue(auth0MetaData.getAuth0AccessTokenField(), accessToken) - .withValue(auth0MetaData.getQqqAccessTokenField(), uuid) - .withValue(auth0MetaData.getExpiresInSecondsField(), expiresInSeconds); - InsertInput input = new InsertInput(); - input.setTableName(auth0MetaData.getAccessTokenTableName()); - input.setRecords(List.of(accessTokenRecord)); - new InsertAction().execute(input); - - ////////////////////////////////// - // update and send the response // - ////////////////////////////////// - accessTokenData.put("access_token", uuid); - accessTokenData.remove("scope"); - return (accessTokenData.toString()); - } - catch(AccessTokenException ate) - { - throw (ate); - } - catch(Exception e) - { - throw (new AccessTokenException(e.getMessage(), e)); - } - finally - { - QContext.setQSession(sessionBefore); - } - } - - - - /******************************************************************************* - ** make http request to Auth0 for a new access token - ** - *******************************************************************************/ - public JSONObject requestAccessTokenFromAuth0(Auth0AuthenticationMetaData auth0MetaData, String clientId, String clientSecret) throws AccessTokenException + public JSONObject requestAccessTokenForClientIdAndSecretFromAuth0(Auth0AuthenticationMetaData auth0MetaData, String clientId, String clientSecret) throws AccessTokenException { /////////////////////////////////////////////////////////////////// // make a request to Auth0 using the client_id and client_secret // @@ -776,6 +792,63 @@ public class Auth0AuthenticationModule implements QAuthenticationModuleInterface + /******************************************************************************* + ** Look up access_token from session UUID + ** + *******************************************************************************/ + private String getAccessTokenFromSessionUUID(Auth0AuthenticationMetaData metaData, String sessionUUID) throws QAuthenticationException + { + String accessToken = null; + QSession beforeSession = QContext.getQSession(); + + try + { + QContext.setQSession(getChickenAndEggSession()); + + /////////////////////////////////////// + // query for the user session record // + /////////////////////////////////////// + QRecord userSessionRecord = new GetAction().executeForRecord(new GetInput(UserSession.TABLE_NAME) + .withUniqueKey(Map.of("uuid", sessionUUID)) + .withShouldMaskPasswords(false) + .withShouldOmitHiddenFields(false)); + + if(userSessionRecord != null) + { + accessToken = userSessionRecord.getValueString("accessToken"); + + //////////////////////////////////////////////////////////// + // decode the accessToken and make sure it is not expired // + //////////////////////////////////////////////////////////// + if(accessToken != null) + { + DecodedJWT jwt = JWT.decode(accessToken); + if(jwt.getExpiresAtAsInstant().isBefore(Instant.now())) + { + throw (new QAuthenticationException("accessToken is expired")); + } + } + } + } + catch(QAuthenticationException qae) + { + throw (qae); + } + catch(Exception e) + { + LOG.warn("Error looking up userSession by sessionUUID", e); + throw (new QAuthenticationException("Error looking up userSession by sessionUUID", e)); + } + finally + { + QContext.setQSession(beforeSession); + } + + return (accessToken); + } + + + /******************************************************************************* ** Look up access_token from api key ** @@ -841,7 +914,7 @@ public class Auth0AuthenticationModule implements QAuthenticationModuleInterface // store the actual access token in the database, and return a unique value // // back to the user which will be what they use on subsequent requests (because token too big) // ///////////////////////////////////////////////////////////////////////////////////////////////// - JSONObject accessTokenData = requestAccessTokenFromAuth0(metaData, clientId, clientSecret); + JSONObject accessTokenData = requestAccessTokenForClientIdAndSecretFromAuth0(metaData, clientId, clientSecret); Integer expiresInSeconds = accessTokenData.getInt("expires_in"); accessToken = accessTokenData.getString("access_token"); @@ -869,28 +942,4 @@ public class Auth0AuthenticationModule implements QAuthenticationModuleInterface return (accessToken); } - - - /******************************************************************************* - ** Look up client_auth0_application record, return if found. - ** - *******************************************************************************/ - QRecord getClientAuth0Application(Auth0AuthenticationMetaData metaData, String clientId) throws QException - { - ////////////////////////////////////////////////////////////////////////////////////// - // try to look up existing auth0 application from database, insert one if not found // - ////////////////////////////////////////////////////////////////////////////////////// - QueryInput queryInput = new QueryInput(); - queryInput.setTableName(metaData.getClientAuth0ApplicationTableName()); - queryInput.setFilter(new QQueryFilter(new QFilterCriteria(metaData.getAuth0ClientIdField(), QCriteriaOperator.EQUALS, clientId))); - QueryOutput queryOutput = new QueryAction().execute(queryInput); - - if(CollectionUtils.nullSafeHasContents(queryOutput.getRecords())) - { - return (queryOutput.getRecords().get(0)); - } - - throw (new AccessTokenException("This client has not been configured to use the API.", HttpStatus.SC_UNAUTHORIZED)); - } - } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/metadata/UserSessionMetaDataProducer.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/metadata/UserSessionMetaDataProducer.java new file mode 100644 index 00000000..c5dca6e2 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/metadata/UserSessionMetaDataProducer.java @@ -0,0 +1,73 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2023. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.modules.authentication.implementations.metadata; + + +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducer; +import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import com.kingsrook.qqq.backend.core.model.metadata.audits.AuditLevel; +import com.kingsrook.qqq.backend.core.model.metadata.audits.QAuditRules; +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.implementations.model.UserSession; + + +/******************************************************************************* + ** Meta Data Producer for UserSession + *******************************************************************************/ +public class UserSessionMetaDataProducer extends MetaDataProducer +{ + private final String backendName; + + + + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public UserSessionMetaDataProducer(String backendName) + { + this.backendName = backendName; + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public QTableMetaData produce(QInstance qInstance) throws QException + { + QTableMetaData tableMetaData = new QTableMetaData() + .withName(UserSession.TABLE_NAME) + .withBackendName(backendName) + .withRecordLabelFormat("%s") + .withRecordLabelFields("id") + .withPrimaryKeyField("id") + .withUniqueKey(new UniqueKey("uuid")) + .withFieldsFromEntity(UserSession.class) + .withAuditRules(new QAuditRules().withAuditLevel(AuditLevel.NONE)); + return tableMetaData; + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/model/UserSession.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/model/UserSession.java new file mode 100644 index 00000000..2e5b1a3b --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/model/UserSession.java @@ -0,0 +1,262 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2023. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.modules.authentication.implementations.model; + + +import java.time.Instant; +import com.kingsrook.qqq.backend.core.model.data.QField; +import com.kingsrook.qqq.backend.core.model.data.QRecord; +import com.kingsrook.qqq.backend.core.model.data.QRecordEntity; +import com.kingsrook.qqq.backend.core.model.metadata.fields.ValueTooLongBehavior; + + +/******************************************************************************* + ** QRecord Entity for UserSession table + *******************************************************************************/ +public class UserSession extends QRecordEntity +{ + public static final String TABLE_NAME = "userSession"; + + @QField(isEditable = false) + private Integer id; + + @QField(isEditable = false) + private Instant createDate; + + @QField(isEditable = false) + private Instant modifyDate; + + @QField(isEditable = false, isHidden = true, maxLength = 40, valueTooLongBehavior = ValueTooLongBehavior.ERROR) + private String uuid; + + @QField(isEditable = false, isHidden = true) + private String accessToken; + + @QField(isEditable = false, maxLength = 100, valueTooLongBehavior = ValueTooLongBehavior.TRUNCATE_ELLIPSIS) + private String userId; + + + + /******************************************************************************* + ** Default constructor + *******************************************************************************/ + public UserSession() + { + } + + + + /******************************************************************************* + ** Constructor that takes a QRecord + *******************************************************************************/ + public UserSession(QRecord record) + { + populateFromQRecord(record); + } + + + + /******************************************************************************* + ** Getter for id + *******************************************************************************/ + public Integer getId() + { + return (this.id); + } + + + + /******************************************************************************* + ** Setter for id + *******************************************************************************/ + public void setId(Integer id) + { + this.id = id; + } + + + + /******************************************************************************* + ** Fluent setter for id + *******************************************************************************/ + public UserSession withId(Integer id) + { + this.id = id; + return (this); + } + + + + /******************************************************************************* + ** Getter for createDate + *******************************************************************************/ + public Instant getCreateDate() + { + return (this.createDate); + } + + + + /******************************************************************************* + ** Setter for createDate + *******************************************************************************/ + public void setCreateDate(Instant createDate) + { + this.createDate = createDate; + } + + + + /******************************************************************************* + ** Fluent setter for createDate + *******************************************************************************/ + public UserSession withCreateDate(Instant createDate) + { + this.createDate = createDate; + return (this); + } + + + + /******************************************************************************* + ** Getter for modifyDate + *******************************************************************************/ + public Instant getModifyDate() + { + return (this.modifyDate); + } + + + + /******************************************************************************* + ** Setter for modifyDate + *******************************************************************************/ + public void setModifyDate(Instant modifyDate) + { + this.modifyDate = modifyDate; + } + + + + /******************************************************************************* + ** Fluent setter for modifyDate + *******************************************************************************/ + public UserSession withModifyDate(Instant modifyDate) + { + this.modifyDate = modifyDate; + return (this); + } + + + + /******************************************************************************* + ** Getter for uuid + *******************************************************************************/ + public String getUuid() + { + return (this.uuid); + } + + + + /******************************************************************************* + ** Setter for uuid + *******************************************************************************/ + public void setUuid(String uuid) + { + this.uuid = uuid; + } + + + + /******************************************************************************* + ** Fluent setter for uuid + *******************************************************************************/ + public UserSession withUuid(String uuid) + { + this.uuid = uuid; + return (this); + } + + + + /******************************************************************************* + ** Getter for accessToken + *******************************************************************************/ + public String getAccessToken() + { + return (this.accessToken); + } + + + + /******************************************************************************* + ** Setter for accessToken + *******************************************************************************/ + public void setAccessToken(String accessToken) + { + this.accessToken = accessToken; + } + + + + /******************************************************************************* + ** Fluent setter for accessToken + *******************************************************************************/ + public UserSession withAccessToken(String accessToken) + { + this.accessToken = accessToken; + return (this); + } + + + + /******************************************************************************* + ** Getter for userId + *******************************************************************************/ + public String getUserId() + { + return (this.userId); + } + + + + /******************************************************************************* + ** Setter for userId + *******************************************************************************/ + public void setUserId(String userId) + { + this.userId = userId; + } + + + + /******************************************************************************* + ** Fluent setter for userId + *******************************************************************************/ + public UserSession withUserId(String userId) + { + this.userId = userId; + return (this); + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/CollectionUtils.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/CollectionUtils.java index ecb5aca5..95b8d091 100755 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/CollectionUtils.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/CollectionUtils.java @@ -663,4 +663,19 @@ public class CollectionUtils return (output); } + + + /******************************************************************************* + ** + *******************************************************************************/ + public static boolean containsKeyWithNonNullValue(Map map, K key) + { + if(map == null) + { + return (false); + } + + return (map.containsKey(key) && map.get(key) != null); + } + } diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/Auth0AuthenticationModuleTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/Auth0AuthenticationModuleTest.java index 9a5d906e..2c791804 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/Auth0AuthenticationModuleTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/Auth0AuthenticationModuleTest.java @@ -42,7 +42,7 @@ import com.kingsrook.qqq.backend.core.state.SimpleStateKey; import com.kingsrook.qqq.backend.core.utils.CollectionUtils; import org.json.JSONObject; import org.junit.jupiter.api.Test; -import static com.kingsrook.qqq.backend.core.modules.authentication.implementations.Auth0AuthenticationModule.AUTH0_ACCESS_TOKEN_KEY; +import static com.kingsrook.qqq.backend.core.modules.authentication.implementations.Auth0AuthenticationModule.ACCESS_TOKEN_KEY; import static com.kingsrook.qqq.backend.core.modules.authentication.implementations.Auth0AuthenticationModule.BASIC_AUTH_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; @@ -143,7 +143,7 @@ public class Auth0AuthenticationModuleTest extends BaseTest public void testInvalidToken() { Map context = new HashMap<>(); - context.put(AUTH0_ACCESS_TOKEN_KEY, INVALID_TOKEN); + context.put(ACCESS_TOKEN_KEY, INVALID_TOKEN); try { @@ -167,7 +167,7 @@ public class Auth0AuthenticationModuleTest extends BaseTest public void testUndecodableToken() { Map context = new HashMap<>(); - context.put(AUTH0_ACCESS_TOKEN_KEY, UNDECODABLE_TOKEN); + context.put(ACCESS_TOKEN_KEY, UNDECODABLE_TOKEN); try { @@ -191,7 +191,7 @@ public class Auth0AuthenticationModuleTest extends BaseTest public void testProperlyFormattedButExpiredToken() { Map context = new HashMap<>(); - context.put(AUTH0_ACCESS_TOKEN_KEY, EXPIRED_TOKEN); + context.put(ACCESS_TOKEN_KEY, EXPIRED_TOKEN); try { @@ -236,7 +236,7 @@ public class Auth0AuthenticationModuleTest extends BaseTest public void testNullToken() { Map context = new HashMap<>(); - context.put(AUTH0_ACCESS_TOKEN_KEY, null); + context.put(ACCESS_TOKEN_KEY, null); try { @@ -267,7 +267,7 @@ public class Auth0AuthenticationModuleTest extends BaseTest auth0Spy.createSession(qInstance, context); auth0Spy.createSession(qInstance, context); auth0Spy.createSession(qInstance, context); - verify(auth0Spy, times(1)).getAccessTokenFromAuth0(any(), any(), any()); + verify(auth0Spy, times(1)).getAccessTokenForUsernameAndPasswordFromAuth0(any(), any(), any()); } diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandler.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandler.java index 380979d0..cd99af77 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandler.java +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandler.java @@ -27,7 +27,6 @@ import java.io.Serializable; import java.nio.charset.StandardCharsets; import java.time.Instant; import java.util.ArrayList; -import java.util.Base64; import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashMap; @@ -58,7 +57,6 @@ import com.kingsrook.qqq.api.model.openapi.HttpMethod; import com.kingsrook.qqq.backend.core.actions.tables.GetAction; import com.kingsrook.qqq.backend.core.actions.tables.InsertAction; import com.kingsrook.qqq.backend.core.context.QContext; -import com.kingsrook.qqq.backend.core.exceptions.AccessTokenException; import com.kingsrook.qqq.backend.core.exceptions.QAuthenticationException; import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.exceptions.QModuleDispatchException; @@ -75,15 +73,12 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertOutput; 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.QInstance; -import com.kingsrook.qqq.backend.core.model.metadata.authentication.Auth0AuthenticationMetaData; import com.kingsrook.qqq.backend.core.model.metadata.branding.QBrandingMetaData; 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.model.session.QUser; -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; import com.kingsrook.qqq.backend.core.utils.ExceptionUtils; import com.kingsrook.qqq.backend.core.utils.JsonUtils; @@ -137,11 +132,6 @@ public class QJavalinApiHandler { return (() -> { - ///////////////////////////// - // authentication endpoint // - ///////////////////////////// - ApiBuilder.post("/api/oauth/token", QJavalinApiHandler::handleAuthorization); - /////////////////////////////////////////////// // static endpoints to support rapidoc pages // /////////////////////////////////////////////// @@ -583,101 +573,6 @@ public class QJavalinApiHandler - /******************************************************************************* - ** - *******************************************************************************/ - private static void handleAuthorization(Context context) - { - try - { - //////////////////////////////////////////////////////////////////////////////////////////////////////// - // clientId & clientSecret may either be provided as formParams, or in an Authorization: Basic header // - //////////////////////////////////////////////////////////////////////////////////////////////////////// - String clientId; - String clientSecret; - String authorizationHeader = context.header("Authorization"); - if(authorizationHeader != null && authorizationHeader.startsWith("Basic ")) - { - try - { - byte[] credDecoded = Base64.getDecoder().decode(authorizationHeader.replace("Basic ", "")); - String credentials = new String(credDecoded, StandardCharsets.UTF_8); - String[] parts = credentials.split(":", 2); - clientId = parts[0]; - clientSecret = parts[1]; - } - catch(Exception e) - { - context.status(HttpStatus.BAD_REQUEST_400); - context.result("Could not parse client_id and client_secret from Basic Authorization header."); - return; - } - } - else - { - clientId = context.formParam("client_id"); - if(clientId == null) - { - context.status(HttpStatus.BAD_REQUEST_400); - context.result("'client_id' must be provided."); - return; - } - clientSecret = context.formParam("client_secret"); - if(clientSecret == null) - { - context.status(HttpStatus.BAD_REQUEST_400); - context.result("'client_secret' must be provided."); - return; - } - } - - //////////////////////////////////////////////////////// - // get the auth0 authentication module from qInstance // - //////////////////////////////////////////////////////// - Auth0AuthenticationMetaData metaData = (Auth0AuthenticationMetaData) qInstance.getAuthentication(); - QAuthenticationModuleDispatcher qAuthenticationModuleDispatcher = new QAuthenticationModuleDispatcher(); - QAuthenticationModuleInterface authenticationModule = qAuthenticationModuleDispatcher.getQModule(qInstance.getAuthentication()); - - try - { - ////////////////////////////////////////////////////////////////////////////////////////// - // make call to get access token data, if no exception thrown, assume 200 OK and return // - ////////////////////////////////////////////////////////////////////////////////////////// - QContext.init(qInstance, null); // hmm... - String accessToken = authenticationModule.createAccessToken(metaData, clientId, clientSecret); - context.status(HttpStatus.Code.OK.getCode()); - context.result(accessToken); - QJavalinAccessLogger.logEndSuccess(); - } - catch(AccessTokenException aae) - { - LOG.info("Error getting api access token", aae, logPair("clientId", clientId)); - - /////////////////////////////////////////////////////////////////////////// - // if the exception has a status code, then return that code and message // - /////////////////////////////////////////////////////////////////////////// - if(aae.getStatusCode() != null) - { - context.status(aae.getStatusCode()); - context.result(aae.getMessage()); - QJavalinAccessLogger.logEndSuccess(); - } - - //////////////////////////////////////////////////////// - // if no code, throw and handle like other exceptions // - //////////////////////////////////////////////////////// - throw (aae); - } - } - catch(Exception e) - { - handleException(context, e); - QJavalinAccessLogger.logEndFail(e); - } - } - - - /******************************************************************************* ** *******************************************************************************/ diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java index 172d9544..4799b12a 100644 --- a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java @@ -113,6 +113,7 @@ import com.kingsrook.qqq.backend.core.model.session.QSession; import com.kingsrook.qqq.backend.core.model.statusmessages.QStatusMessage; 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.CollectionUtils; import com.kingsrook.qqq.backend.core.utils.ExceptionUtils; import com.kingsrook.qqq.backend.core.utils.JsonUtils; @@ -148,10 +149,10 @@ public class QJavalinImplementation { private static final QLogger LOG = QLogger.getLogger(QJavalinImplementation.class); - public static final int SESSION_COOKIE_AGE = 60 * 60 * 24; - public static final String SESSION_ID_COOKIE_NAME = "sessionId"; - public static final String BASIC_AUTH_NAME = "basicAuthString"; - public static final String API_KEY_NAME = "apiKey"; + public static final int SESSION_COOKIE_AGE = 60 * 60 * 24; + public static final String SESSION_ID_COOKIE_NAME = "sessionId"; + public static final String SESSION_UUID_COOKIE_NAME = "sessionUUID"; + public static final String API_KEY_NAME = "apiKey"; static QInstance qInstance; static QJavalinMetaData javalinMetaData; @@ -329,6 +330,8 @@ public class QJavalinImplementation { return (() -> { + post("/manageSession", QJavalinImplementation::manageSession); + ///////////////////// // metadata routes // ///////////////////// @@ -400,6 +403,36 @@ public class QJavalinImplementation + /******************************************************************************* + ** + *******************************************************************************/ + private static void manageSession(Context context) + { + try + { + Map map = context.bodyAsClass(Map.class); + + QAuthenticationModuleDispatcher qAuthenticationModuleDispatcher = new QAuthenticationModuleDispatcher(); + QAuthenticationModuleInterface authenticationModule = qAuthenticationModuleDispatcher.getQModule(qInstance.getAuthentication()); + + Map authContext = new HashMap<>(); + //? authContext.put("uuid", ValueUtils.getValueAsString(map.get("uuid"))); + authContext.put(Auth0AuthenticationModule.ACCESS_TOKEN_KEY, ValueUtils.getValueAsString(map.get("accessToken"))); + authContext.put(Auth0AuthenticationModule.DO_STORE_USER_SESSION_KEY, "true"); + + QSession session = authenticationModule.createSession(qInstance, authContext); + + context.cookie(SESSION_UUID_COOKIE_NAME, session.getUuid(), SESSION_COOKIE_AGE); + context.result(JsonUtils.toJson(MapBuilder.of("uuid", session.getUuid()))); + } + catch(Exception e) + { + handleException(context, e); + } + } + + + /******************************************************************************* ** *******************************************************************************/ @@ -442,10 +475,12 @@ public class QJavalinImplementation { Map authenticationContext = new HashMap<>(); - String sessionIdCookieValue = context.cookie(SESSION_ID_COOKIE_NAME); + // todo delete String sessionIdCookieValue = context.cookie(SESSION_ID_COOKIE_NAME); + String sessionUuidCookieValue = context.cookie(Auth0AuthenticationModule.SESSION_UUID_KEY); String authorizationHeaderValue = context.header("Authorization"); String apiKeyHeaderValue = context.header("x-api-key"); + /* todo - change to sessionUUID. if(StringUtils.hasContent(sessionIdCookieValue)) { //////////////////////////////////////// @@ -453,6 +488,11 @@ public class QJavalinImplementation //////////////////////////////////////// authenticationContext.put(SESSION_ID_COOKIE_NAME, sessionIdCookieValue); } + else*/ + if(StringUtils.hasContent(sessionUuidCookieValue)) + { + authenticationContext.put(Auth0AuthenticationModule.SESSION_UUID_KEY, sessionUuidCookieValue); + } else if(apiKeyHeaderValue != null) { ///////////////////////////////////////////////////////////////// @@ -533,12 +573,12 @@ public class QJavalinImplementation if(authorizationHeaderValue.startsWith(basicPrefix)) { authorizationHeaderValue = authorizationHeaderValue.replaceFirst(basicPrefix, ""); - authenticationContext.put(BASIC_AUTH_NAME, authorizationHeaderValue); + authenticationContext.put(Auth0AuthenticationModule.BASIC_AUTH_KEY, authorizationHeaderValue); } else if(authorizationHeaderValue.startsWith(bearerPrefix)) { authorizationHeaderValue = authorizationHeaderValue.replaceFirst(bearerPrefix, ""); - authenticationContext.put(SESSION_ID_COOKIE_NAME, authorizationHeaderValue); + authenticationContext.put(Auth0AuthenticationModule.ACCESS_TOKEN_KEY, authorizationHeaderValue); } else { diff --git a/qqq-middleware-picocli/src/main/java/com/kingsrook/qqq/frontend/picocli/QPicoCliImplementation.java b/qqq-middleware-picocli/src/main/java/com/kingsrook/qqq/frontend/picocli/QPicoCliImplementation.java index bc47eeb9..620433ab 100644 --- a/qqq-middleware-picocli/src/main/java/com/kingsrook/qqq/frontend/picocli/QPicoCliImplementation.java +++ b/qqq-middleware-picocli/src/main/java/com/kingsrook/qqq/frontend/picocli/QPicoCliImplementation.java @@ -90,8 +90,6 @@ import com.kingsrook.qqq.backend.core.utils.StringUtils; import io.github.cdimascio.dotenv.Dotenv; import org.apache.commons.io.FileUtils; import org.apache.logging.log4j.core.config.Configurator; -import org.jline.reader.LineReader; -import org.jline.reader.LineReaderBuilder; import org.jline.utils.Log; import picocli.CommandLine; import picocli.CommandLine.Model.CommandSpec; @@ -292,18 +290,7 @@ public class QPicoCliImplementation } Map authenticationContext = new HashMap<>(); - if(sessionId == null && authenticationModule instanceof Auth0AuthenticationModule) - { - LineReader lr = LineReaderBuilder.builder().build(); - String tokenId = lr.readLine("Create a .env file with the contents of the Auth0 JWT Id Token in the variable 'SESSION_ID': \nPress enter once complete..."); - dotenv = loadDotEnv(); - if(dotenv.isPresent()) - { - sessionId = dotenv.get().get("SESSION_ID"); - } - } - - authenticationContext.put("sessionId", sessionId); + authenticationContext.put(Auth0AuthenticationModule.ACCESS_TOKEN_KEY, sessionId); // todo - does this need some per-provider logic actually? mmm... session = authenticationModule.createSession(qInstance, authenticationContext); From dbaad85ec73de2246db845dd7864d9b6e11d6cf0 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Wed, 9 Aug 2023 09:55:59 -0500 Subject: [PATCH 2/7] CE-609 Restore usage of sessionId cookie/auth-key (used by a test on table-based auth) --- .../backend/javalin/QJavalinImplementation.java | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java index 4799b12a..5f3f3d9d 100644 --- a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java @@ -475,22 +475,23 @@ public class QJavalinImplementation { Map authenticationContext = new HashMap<>(); - // todo delete String sessionIdCookieValue = context.cookie(SESSION_ID_COOKIE_NAME); + String sessionIdCookieValue = context.cookie(SESSION_ID_COOKIE_NAME); String sessionUuidCookieValue = context.cookie(Auth0AuthenticationModule.SESSION_UUID_KEY); String authorizationHeaderValue = context.header("Authorization"); String apiKeyHeaderValue = context.header("x-api-key"); - /* todo - change to sessionUUID. if(StringUtils.hasContent(sessionIdCookieValue)) { - //////////////////////////////////////// - // first, look for a sessionId cookie // - //////////////////////////////////////// + /////////////////////////////////////////////////////// + // sessionId - maybe used by table-based auth module // + /////////////////////////////////////////////////////// authenticationContext.put(SESSION_ID_COOKIE_NAME, sessionIdCookieValue); } - else*/ - if(StringUtils.hasContent(sessionUuidCookieValue)) + else if(StringUtils.hasContent(sessionUuidCookieValue)) { + /////////////////////////////////////////////////////////////////////////// + // session UUID - known to be used by auth0 module (in aug. 2023 update) // + /////////////////////////////////////////////////////////////////////////// authenticationContext.put(Auth0AuthenticationModule.SESSION_UUID_KEY, sessionUuidCookieValue); } else if(apiKeyHeaderValue != null) From 366639c88254899b51a4ea5c2b7c221c0adf46bb Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Wed, 9 Aug 2023 10:31:59 -0500 Subject: [PATCH 3/7] CE-609 Increase javalin test coverage (manageSessions and hotSwap) --- .../javalin/QJavalinImplementation.java | 14 +- .../javalin/QJavalinImplementationTest.java | 126 +++++++++++++++++- 2 files changed, 137 insertions(+), 3 deletions(-) diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java index 5f3f3d9d..16733f00 100644 --- a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java @@ -160,8 +160,8 @@ public class QJavalinImplementation private static Supplier qInstanceHotSwapSupplier; private static long lastQInstanceHotSwapMillis; - private static final long MILLIS_BETWEEN_HOT_SWAPS = 2500; - public static final long SLOW_LOG_THRESHOLD_MS = 1000; + private static long MILLIS_BETWEEN_HOT_SWAPS = 2500; + public static final long SLOW_LOG_THRESHOLD_MS = 1000; private static final Integer DEFAULT_COUNT_TIMEOUT_SECONDS = 60; private static final Integer DEFAULT_QUERY_TIMEOUT_SECONDS = 60; @@ -1857,4 +1857,14 @@ public class QJavalinImplementation return StringUtils.joinWithCommasAndAnd(errors.stream().map(QStatusMessage::getMessage).toList()); } + + + /******************************************************************************* + ** + *******************************************************************************/ + public static void setMillisBetweenHotSwaps(long millisBetweenHotSwaps) + { + MILLIS_BETWEEN_HOT_SWAPS = millisBetweenHotSwaps; + } + } diff --git a/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementationTest.java b/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementationTest.java index 9e15f2fb..64f0558d 100644 --- a/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementationTest.java +++ b/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementationTest.java @@ -29,9 +29,17 @@ import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportFormat; import com.kingsrook.qqq.backend.core.model.dashboard.widgets.WidgetType; +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.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.utils.JsonUtils; +import com.kingsrook.qqq.backend.core.utils.SleepUtils; import kong.unirest.HttpResponse; import kong.unirest.Unirest; import org.eclipse.jetty.http.HttpStatus; @@ -43,6 +51,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -635,7 +644,8 @@ class QJavalinImplementationTest extends QJavalinTestBase JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody()); assertNotNull(jsonObject); assertEquals(1, jsonObject.getInt("deletedRecordCount")); - TestUtils.runTestSql("SELECT id FROM person", (rs -> { + TestUtils.runTestSql("SELECT id FROM person", (rs -> + { int rowsFound = 0; while(rs.next()) { @@ -832,4 +842,118 @@ class QJavalinImplementationTest extends QJavalinTestBase assertTrue(jsonObject.has("type")); } + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testManageSession() + { + String body = """ + { + "accessToken": "abcd", + "doStoreUserSession": true + } + """; + HttpResponse response = Unirest.post(BASE_URL + "/manageSession") + .header("Content-Type", "application/json") + .body(body) + .asString(); + + assertEquals(200, response.getStatus()); + JSONObject jsonObject = JsonUtils.toJSONObject(response.getBody()); + assertNotNull(jsonObject); + assertTrue(jsonObject.has("uuid")); + response.getHeaders().get("Set-Cookie").stream().anyMatch(s -> s.contains("sessionUUID")); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testHotSwap() + { + Function makeNewInstanceWithBackendName = (backendName) -> + { + QInstance newInstance = new QInstance(); + newInstance.addBackend(new QBackendMetaData().withName(backendName).withBackendType("mock")); + + if(!"invalid".equals(backendName)) + { + newInstance.addTable(new QTableMetaData() + .withName("newTable") + .withBackendName(backendName) + .withField(new QFieldMetaData("newField", QFieldType.INTEGER)) + .withPrimaryKeyField("newField") + ); + } + + return (newInstance); + }; + + QJavalinImplementation.setQInstanceHotSwapSupplier(() -> makeNewInstanceWithBackendName.apply("newBackend")); + + ///////////////////////////////////////////////////////////////////////////////// + // make sure before a hot-swap, that the instance doesn't have our new backend // + ///////////////////////////////////////////////////////////////////////////////// + assertNull(QJavalinImplementation.qInstance.getBackend("newBackend")); + + /////////////////////////////////////////////////////// + // do a hot-swap, make sure the new backend is there // + /////////////////////////////////////////////////////// + QJavalinImplementation.hotSwapQInstance(null); + assertNotNull(QJavalinImplementation.qInstance.getBackend("newBackend")); + + //////////////////////////////////////////////////////////////////////////////////////////////////////////// + // now change to make a different backend - try to swap again - but the newer backend shouldn't be there, // + // because the millis-between-hot-swaps won't have passed // + //////////////////////////////////////////////////////////////////////////////////////////////////////////// + QJavalinImplementation.setQInstanceHotSwapSupplier(() -> makeNewInstanceWithBackendName.apply("newerBackend")); + QJavalinImplementation.hotSwapQInstance(null); + assertNull(QJavalinImplementation.qInstance.getBackend("newerBackend")); + + //////////////////////////////////////////////////////////////////////////////////////////// + // set the sleep threshold to 1 milli, sleep for 2, and then assert that we do swap again // + //////////////////////////////////////////////////////////////////////////////////////////// + QJavalinImplementation.setMillisBetweenHotSwaps(1); + SleepUtils.sleep(2, TimeUnit.MILLISECONDS); + + QJavalinImplementation.setQInstanceHotSwapSupplier(() -> makeNewInstanceWithBackendName.apply("newerBackend")); + QJavalinImplementation.hotSwapQInstance(null); + assertNotNull(QJavalinImplementation.qInstance.getBackend("newerBackend")); + + //////////////////////////////////////////////////////////// + // assert that an invalid instance doesn't get swapped in // + // e.g., "newerBackend" still exists // + //////////////////////////////////////////////////////////// + SleepUtils.sleep(2, TimeUnit.MILLISECONDS); + QJavalinImplementation.setQInstanceHotSwapSupplier(() -> makeNewInstanceWithBackendName.apply("invalid")); + QJavalinImplementation.hotSwapQInstance(null); + assertNotNull(QJavalinImplementation.qInstance.getBackend("newerBackend")); + + /////////////////////////////////////////////////////// + // assert that if the supplier throws, we don't swap // + // e.g., "newerBackend" still exists // + /////////////////////////////////////////////////////// + SleepUtils.sleep(2, TimeUnit.MILLISECONDS); + QJavalinImplementation.setQInstanceHotSwapSupplier(() -> + { + throw new RuntimeException("oops"); + }); + QJavalinImplementation.hotSwapQInstance(null); + assertNotNull(QJavalinImplementation.qInstance.getBackend("newerBackend")); + + ///////////////////////////////////////////////////////////// + // assert that if the supplier returns null, we don't swap // + // e.g., "newerBackend" still exists // + ///////////////////////////////////////////////////////////// + SleepUtils.sleep(2, TimeUnit.MILLISECONDS); + QJavalinImplementation.setQInstanceHotSwapSupplier(() -> null); + QJavalinImplementation.hotSwapQInstance(null); + assertNotNull(QJavalinImplementation.qInstance.getBackend("newerBackend")); + } + } From db0b434e52bb934176db3a66b790b4be3fbdb59d Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Tue, 15 Aug 2023 11:27:51 -0500 Subject: [PATCH 4/7] CE-609 - Support for staged rollout: Check sessionUUID before any other value; add logging. --- .../Auth0AuthenticationModule.java | 70 +++++++++++++++---- .../Auth0AuthenticationModuleTest.java | 24 +++++++ 2 files changed, 79 insertions(+), 15 deletions(-) diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/Auth0AuthenticationModule.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/Auth0AuthenticationModule.java index e9f3b732..70fe607a 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/Auth0AuthenticationModule.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/Auth0AuthenticationModule.java @@ -89,6 +89,7 @@ import org.apache.http.message.BasicNameValuePair; import org.apache.http.util.EntityUtils; import org.json.JSONArray; import org.json.JSONObject; +import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair; /******************************************************************************* @@ -98,6 +99,7 @@ import org.json.JSONObject; ** ** System-User session use-case: ** 1: Takes in an "accessToken" (but doesn't store a userSession record). + ** 1b: legacy frontend use-case does the same as system-user! ** ** Web User session use-cases: ** 2: creates a new session (userSession record) by taking an "accessToken" @@ -167,12 +169,25 @@ public class Auth0AuthenticationModule implements QAuthenticationModuleInterface { Auth0AuthenticationMetaData metaData = (Auth0AuthenticationMetaData) qInstance.getAuthentication(); - ///////////////////////////////////////////////////////////////////////////////////////////// - // if the context contains an access token, then create a new session based on that token. // - ///////////////////////////////////////////////////////////////////////////////////////////// String accessToken = null; - if(CollectionUtils.containsKeyWithNonNullValue(context, ACCESS_TOKEN_KEY)) + if(CollectionUtils.containsKeyWithNonNullValue(context, SESSION_UUID_KEY)) { + ///////////////////////////////////////////////////////////////////////////////////////// + // process a sessionUUID - looks up userSession record - cannot create token this way. // + ///////////////////////////////////////////////////////////////////////////////////////// + String sessionUUID = context.get(SESSION_UUID_KEY); + LOG.info("Creating session from sessionUUID (userSession)", logPair("sessionUUID", maskForLog(sessionUUID))); + if(sessionUUID != null) + { + accessToken = getAccessTokenFromSessionUUID(metaData, sessionUUID); + } + } + else if(CollectionUtils.containsKeyWithNonNullValue(context, ACCESS_TOKEN_KEY)) + { + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // if the context contains an access token, then create a new session based on that token. // + // todo#authHeader - this else/if should maybe be first, but while we have frontend passing both, we want it second // + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// accessToken = context.get(ACCESS_TOKEN_KEY); QSession qSession = buildAndValidateSession(qInstance, accessToken); @@ -182,6 +197,18 @@ public class Auth0AuthenticationModule implements QAuthenticationModuleInterface if(CollectionUtils.containsKeyWithNonNullValue(context, DO_STORE_USER_SESSION_KEY)) { insertUserSession(qInstance, accessToken, qSession); + LOG.info("Creating session based on input accessToken and creating a userSession", logPair("userId", qSession.getUser().getIdReference())); + } + else + { + /////////////////////////////////////////////// + // todo#authHeader - remove all this logging // + /////////////////////////////////////////////// + String userName = qSession.getUser() != null ? qSession.getUser().getFullName() : null; + if(userName != null && !userName.contains("System User")) + { + LOG.info("Creating session based on input accessToken but not creating a userSession", logPair("userName", qSession.getUser().getFullName())); + } } return (qSession); @@ -199,6 +226,7 @@ public class Auth0AuthenticationModule implements QAuthenticationModuleInterface // decode the credentials from the header auth // ///////////////////////////////////////////////// String base64Credentials = context.get(BASIC_AUTH_KEY).trim(); + LOG.info("Creating session from basicAuthentication", logPair("base64Credentials", maskForLog(base64Credentials))); accessToken = getAccessTokenFromBase64BasicAuthCredentials(metaData, auth, base64Credentials); } catch(Auth0Exception e) @@ -217,22 +245,12 @@ public class Auth0AuthenticationModule implements QAuthenticationModuleInterface // process an api key - looks up client application token (creating token if needed) // /////////////////////////////////////////////////////////////////////////////////////// String apiKey = context.get(API_KEY); + LOG.info("Creating session from apiKey (accessTokenTable)", logPair("apiKey", maskForLog(apiKey))); if(apiKey != null) { accessToken = getAccessTokenFromApiKey(metaData, apiKey); } } - else if(CollectionUtils.containsKeyWithNonNullValue(context, SESSION_UUID_KEY)) - { - ///////////////////////////////////////////////////////////////////////////////////////// - // process a sessionUUID - looks up userSession record - cannot create token this way. // - ///////////////////////////////////////////////////////////////////////////////////////// - String sessionUUID = context.get(SESSION_UUID_KEY); - if(sessionUUID != null) - { - accessToken = getAccessTokenFromSessionUUID(metaData, sessionUUID); - } - } /* todo confirm this is deprecated ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -942,4 +960,26 @@ public class Auth0AuthenticationModule implements QAuthenticationModuleInterface return (accessToken); } + + + /******************************************************************************* + ** + *******************************************************************************/ + static String maskForLog(String input) + { + if(input == null) + { + return (null); + } + + if(input.length() < 8) + { + return ("******"); + } + else + { + return (input.substring(0, 6) + "******"); + } + } + } diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/Auth0AuthenticationModuleTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/Auth0AuthenticationModuleTest.java index 2c791804..e056889e 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/Auth0AuthenticationModuleTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/Auth0AuthenticationModuleTest.java @@ -48,9 +48,11 @@ import static com.kingsrook.qqq.backend.core.modules.authentication.implementati 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 com.kingsrook.qqq.backend.core.modules.authentication.implementations.Auth0AuthenticationModule.maskForLog; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; import static org.mockito.ArgumentMatchers.any; @@ -467,4 +469,26 @@ public class Auth0AuthenticationModuleTest extends BaseTest return (encoder.encodeToString(originalString.getBytes())); } + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testMask() + { + assertNull(maskForLog(null)); + assertEquals("******", maskForLog("1")); + assertEquals("******", maskForLog("12")); + assertEquals("******", maskForLog("123")); + assertEquals("******", maskForLog("1234")); + assertEquals("******", maskForLog("12345")); + assertEquals("******", maskForLog("12345")); + assertEquals("******", maskForLog("123456")); + assertEquals("******", maskForLog("1234567")); + assertEquals("123456******", maskForLog("12345678")); + assertEquals("123456******", maskForLog("123456789")); + assertEquals("123456******", maskForLog("1234567890")); + } + } From 2577bbeb37f84a874582cbd097d0cc2dee0f2d6e Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Tue, 15 Aug 2023 11:38:46 -0500 Subject: [PATCH 5/7] Restore QJavalinImplementation to original state after testHotSwap --- .../javalin/QJavalinImplementationTest.java | 147 ++++++++++-------- 1 file changed, 80 insertions(+), 67 deletions(-) diff --git a/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementationTest.java b/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementationTest.java index 64f0558d..a352e558 100644 --- a/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementationTest.java +++ b/qqq-middleware-javalin/src/test/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementationTest.java @@ -31,6 +31,7 @@ import java.util.HashMap; import java.util.Map; import java.util.concurrent.TimeUnit; import java.util.function.Function; +import com.kingsrook.qqq.backend.core.exceptions.QInstanceValidationException; import com.kingsrook.qqq.backend.core.model.actions.reporting.ReportFormat; import com.kingsrook.qqq.backend.core.model.dashboard.widgets.WidgetType; import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData; @@ -874,86 +875,98 @@ class QJavalinImplementationTest extends QJavalinTestBase ** *******************************************************************************/ @Test - void testHotSwap() + void testHotSwap() throws QInstanceValidationException { - Function makeNewInstanceWithBackendName = (backendName) -> + try { - QInstance newInstance = new QInstance(); - newInstance.addBackend(new QBackendMetaData().withName(backendName).withBackendType("mock")); - - if(!"invalid".equals(backendName)) + Function makeNewInstanceWithBackendName = (backendName) -> { - newInstance.addTable(new QTableMetaData() - .withName("newTable") - .withBackendName(backendName) - .withField(new QFieldMetaData("newField", QFieldType.INTEGER)) - .withPrimaryKeyField("newField") - ); - } + QInstance newInstance = new QInstance(); + newInstance.addBackend(new QBackendMetaData().withName(backendName).withBackendType("mock")); - return (newInstance); - }; + if(!"invalid".equals(backendName)) + { + newInstance.addTable(new QTableMetaData() + .withName("newTable") + .withBackendName(backendName) + .withField(new QFieldMetaData("newField", QFieldType.INTEGER)) + .withPrimaryKeyField("newField") + ); + } - QJavalinImplementation.setQInstanceHotSwapSupplier(() -> makeNewInstanceWithBackendName.apply("newBackend")); + return (newInstance); + }; - ///////////////////////////////////////////////////////////////////////////////// - // make sure before a hot-swap, that the instance doesn't have our new backend // - ///////////////////////////////////////////////////////////////////////////////// - assertNull(QJavalinImplementation.qInstance.getBackend("newBackend")); + QJavalinImplementation.setQInstanceHotSwapSupplier(() -> makeNewInstanceWithBackendName.apply("newBackend")); - /////////////////////////////////////////////////////// - // do a hot-swap, make sure the new backend is there // - /////////////////////////////////////////////////////// - QJavalinImplementation.hotSwapQInstance(null); - assertNotNull(QJavalinImplementation.qInstance.getBackend("newBackend")); + ///////////////////////////////////////////////////////////////////////////////// + // make sure before a hot-swap, that the instance doesn't have our new backend // + ///////////////////////////////////////////////////////////////////////////////// + assertNull(QJavalinImplementation.qInstance.getBackend("newBackend")); - //////////////////////////////////////////////////////////////////////////////////////////////////////////// - // now change to make a different backend - try to swap again - but the newer backend shouldn't be there, // - // because the millis-between-hot-swaps won't have passed // - //////////////////////////////////////////////////////////////////////////////////////////////////////////// - QJavalinImplementation.setQInstanceHotSwapSupplier(() -> makeNewInstanceWithBackendName.apply("newerBackend")); - QJavalinImplementation.hotSwapQInstance(null); - assertNull(QJavalinImplementation.qInstance.getBackend("newerBackend")); + /////////////////////////////////////////////////////// + // do a hot-swap, make sure the new backend is there // + /////////////////////////////////////////////////////// + QJavalinImplementation.hotSwapQInstance(null); + assertNotNull(QJavalinImplementation.qInstance.getBackend("newBackend")); - //////////////////////////////////////////////////////////////////////////////////////////// - // set the sleep threshold to 1 milli, sleep for 2, and then assert that we do swap again // - //////////////////////////////////////////////////////////////////////////////////////////// - QJavalinImplementation.setMillisBetweenHotSwaps(1); - SleepUtils.sleep(2, TimeUnit.MILLISECONDS); + //////////////////////////////////////////////////////////////////////////////////////////////////////////// + // now change to make a different backend - try to swap again - but the newer backend shouldn't be there, // + // because the millis-between-hot-swaps won't have passed // + //////////////////////////////////////////////////////////////////////////////////////////////////////////// + QJavalinImplementation.setQInstanceHotSwapSupplier(() -> makeNewInstanceWithBackendName.apply("newerBackend")); + QJavalinImplementation.hotSwapQInstance(null); + assertNull(QJavalinImplementation.qInstance.getBackend("newerBackend")); - QJavalinImplementation.setQInstanceHotSwapSupplier(() -> makeNewInstanceWithBackendName.apply("newerBackend")); - QJavalinImplementation.hotSwapQInstance(null); - assertNotNull(QJavalinImplementation.qInstance.getBackend("newerBackend")); + //////////////////////////////////////////////////////////////////////////////////////////// + // set the sleep threshold to 1 milli, sleep for 2, and then assert that we do swap again // + //////////////////////////////////////////////////////////////////////////////////////////// + QJavalinImplementation.setMillisBetweenHotSwaps(1); + SleepUtils.sleep(2, TimeUnit.MILLISECONDS); - //////////////////////////////////////////////////////////// - // assert that an invalid instance doesn't get swapped in // - // e.g., "newerBackend" still exists // - //////////////////////////////////////////////////////////// - SleepUtils.sleep(2, TimeUnit.MILLISECONDS); - QJavalinImplementation.setQInstanceHotSwapSupplier(() -> makeNewInstanceWithBackendName.apply("invalid")); - QJavalinImplementation.hotSwapQInstance(null); - assertNotNull(QJavalinImplementation.qInstance.getBackend("newerBackend")); + QJavalinImplementation.setQInstanceHotSwapSupplier(() -> makeNewInstanceWithBackendName.apply("newerBackend")); + QJavalinImplementation.hotSwapQInstance(null); + assertNotNull(QJavalinImplementation.qInstance.getBackend("newerBackend")); - /////////////////////////////////////////////////////// - // assert that if the supplier throws, we don't swap // - // e.g., "newerBackend" still exists // - /////////////////////////////////////////////////////// - SleepUtils.sleep(2, TimeUnit.MILLISECONDS); - QJavalinImplementation.setQInstanceHotSwapSupplier(() -> + //////////////////////////////////////////////////////////// + // assert that an invalid instance doesn't get swapped in // + // e.g., "newerBackend" still exists // + //////////////////////////////////////////////////////////// + SleepUtils.sleep(2, TimeUnit.MILLISECONDS); + QJavalinImplementation.setQInstanceHotSwapSupplier(() -> makeNewInstanceWithBackendName.apply("invalid")); + QJavalinImplementation.hotSwapQInstance(null); + assertNotNull(QJavalinImplementation.qInstance.getBackend("newerBackend")); + + /////////////////////////////////////////////////////// + // assert that if the supplier throws, we don't swap // + // e.g., "newerBackend" still exists // + /////////////////////////////////////////////////////// + SleepUtils.sleep(2, TimeUnit.MILLISECONDS); + QJavalinImplementation.setQInstanceHotSwapSupplier(() -> + { + throw new RuntimeException("oops"); + }); + QJavalinImplementation.hotSwapQInstance(null); + assertNotNull(QJavalinImplementation.qInstance.getBackend("newerBackend")); + + ///////////////////////////////////////////////////////////// + // assert that if the supplier returns null, we don't swap // + // e.g., "newerBackend" still exists // + ///////////////////////////////////////////////////////////// + SleepUtils.sleep(2, TimeUnit.MILLISECONDS); + QJavalinImplementation.setQInstanceHotSwapSupplier(() -> null); + QJavalinImplementation.hotSwapQInstance(null); + assertNotNull(QJavalinImplementation.qInstance.getBackend("newerBackend")); + } + finally { - throw new RuntimeException("oops"); - }); - QJavalinImplementation.hotSwapQInstance(null); - assertNotNull(QJavalinImplementation.qInstance.getBackend("newerBackend")); - - ///////////////////////////////////////////////////////////// - // assert that if the supplier returns null, we don't swap // - // e.g., "newerBackend" still exists // - ///////////////////////////////////////////////////////////// - SleepUtils.sleep(2, TimeUnit.MILLISECONDS); - QJavalinImplementation.setQInstanceHotSwapSupplier(() -> null); - QJavalinImplementation.hotSwapQInstance(null); - assertNotNull(QJavalinImplementation.qInstance.getBackend("newerBackend")); + //////////////////////////////////////////////////////////// + // restore things to how they used to be, for other tests // + //////////////////////////////////////////////////////////// + QInstance qInstance = TestUtils.defineInstance(); + QJavalinImplementation.setQInstanceHotSwapSupplier(null); + restartServerWithInstance(qInstance); + } } } From 7da34d70da8f6aa46b7637550192b1badc04eadc Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Tue, 15 Aug 2023 18:48:57 -0500 Subject: [PATCH 6/7] CE-609 Remove tests for now-removed /api/oauth/token paths --- .../api/javalin/QJavalinApiHandlerTest.java | 51 ------------------- 1 file changed, 51 deletions(-) diff --git a/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandlerTest.java b/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandlerTest.java index 8d195e62..ac6b1e9b 100644 --- a/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandlerTest.java +++ b/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandlerTest.java @@ -51,7 +51,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.fields.QFieldType; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; -import com.kingsrook.qqq.backend.core.modules.authentication.implementations.FullyAnonymousAuthenticationModule; import com.kingsrook.qqq.backend.core.utils.SleepUtils; import com.kingsrook.qqq.backend.core.utils.StringUtils; import com.kingsrook.qqq.backend.javalin.QJavalinImplementation; @@ -1386,56 +1385,6 @@ class QJavalinApiHandlerTest extends BaseTest - /******************************************************************************* - ** - *******************************************************************************/ - @Test - void testAuthorizeNoParams() - { - /////////////// - // no params // - /////////////// - HttpResponse response = Unirest.post(BASE_URL + "/api/oauth/token").asString(); - assertEquals(HttpStatus.BAD_REQUEST_400, response.getStatus()); - assertThat(response.getBody()).contains("client_id"); - } - - - - /******************************************************************************* - ** - *******************************************************************************/ - @Test - void testAuthorizeOneParam() - { - /////////////// - // no params // - /////////////// - HttpResponse response = Unirest.post(BASE_URL + "/api/oauth/token") - .body("client_id=XXXXXXXXXX").asString(); - assertEquals(HttpStatus.BAD_REQUEST_400, response.getStatus()); - assertThat(response.getBody()).contains("client_secret"); - } - - - - /******************************************************************************* - ** - *******************************************************************************/ - @Test - void testAuthorizeAllParams() - { - /////////////// - // no params // - /////////////// - HttpResponse response = Unirest.post(BASE_URL + "/api/oauth/token") - .body("client_id=XXXXXXXXXX&client_secret=YYYYYYYYYYYY").asString(); - assertEquals(HttpStatus.OK_200, response.getStatus()); - assertThat(response.getBody()).isEqualTo(FullyAnonymousAuthenticationModule.TEST_ACCESS_TOKEN); - } - - - /******************************************************************************* ** *******************************************************************************/ From 994ab1565211b122d1c99cf9b79e58866d7a6afb Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Mon, 25 Sep 2023 14:09:55 -0500 Subject: [PATCH 7/7] Remove unused fields --- .../authentication/Auth0AuthenticationMetaData.java | 8 -------- 1 file changed, 8 deletions(-) diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/authentication/Auth0AuthenticationMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/authentication/Auth0AuthenticationMetaData.java index 1e8f341d..bc254a58 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/authentication/Auth0AuthenticationMetaData.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/authentication/Auth0AuthenticationMetaData.java @@ -69,14 +69,6 @@ public class Auth0AuthenticationMetaData extends QAuthenticationMetaData private String qqqApiKeyField; private String expiresInSecondsField; - ///////////////////////////////////////////////////////////////////////////////// - // table for storing user sessions, and field names we work with on that table // - ///////////////////////////////////////////////////////////////////////////////// - private String userSessionTableName; - private String userSessionUuidField; - private String userSessionUserIdField; - private String userSessionAccessTokenField; - /*******************************************************************************