diff --git a/qqq-backend-core/pom.xml b/qqq-backend-core/pom.xml
index 87a88d69..5e795411 100644
--- a/qqq-backend-core/pom.xml
+++ b/qqq-backend-core/pom.xml
@@ -167,6 +167,13 @@
test
+
+ org.mockito
+ mockito-core
+ 4.8.1
+ test
+
+
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 f5264730..69ef1fe6 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
@@ -26,6 +26,7 @@ import java.nio.charset.StandardCharsets;
import java.security.interfaces.RSAPublicKey;
import java.time.Duration;
import java.time.Instant;
+import java.time.temporal.ChronoUnit;
import java.util.Base64;
import java.util.Map;
import java.util.Optional;
@@ -49,8 +50,8 @@ import com.kingsrook.qqq.backend.core.model.metadata.authentication.Auth0Authent
import com.kingsrook.qqq.backend.core.model.session.QSession;
import com.kingsrook.qqq.backend.core.model.session.QUser;
import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModuleInterface;
-import com.kingsrook.qqq.backend.core.state.AbstractStateKey;
import com.kingsrook.qqq.backend.core.state.InMemoryStateProvider;
+import com.kingsrook.qqq.backend.core.state.SimpleStateKey;
import com.kingsrook.qqq.backend.core.state.StateProviderInterface;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
@@ -101,17 +102,8 @@ public class Auth0AuthenticationModule implements QAuthenticationModuleInterface
// decode the credentials from the header auth //
/////////////////////////////////////////////////
String base64Credentials = context.get(BASIC_AUTH_KEY).trim();
- byte[] credDecoded = Base64.getDecoder().decode(base64Credentials);
- String credentials = new String(credDecoded, StandardCharsets.UTF_8);
-
- /////////////////////////////////////
- // call auth0 with a login request //
- /////////////////////////////////////
- TokenHolder result = auth.login(credentials.split(":")[0], credentials.split(":")[1].toCharArray())
- .setScope("openid email nickname")
- .execute();
-
- context.put(AUTH0_ID_TOKEN_KEY, result.getIdToken());
+ String idToken = getIdTokenFromBase64BasicAuthCredentials(auth, base64Credentials);
+ context.put(AUTH0_ID_TOKEN_KEY, idToken);
}
catch(Auth0Exception e)
{
@@ -159,7 +151,7 @@ public class Auth0AuthenticationModule implements QAuthenticationModuleInterface
// put now into state so we dont check until next interval passes //
///////////////////////////////////////////////////////////////////
StateProviderInterface spi = getStateProvider();
- Auth0StateKey key = new Auth0StateKey(qSession.getIdReference());
+ SimpleStateKey key = new SimpleStateKey<>(qSession.getIdReference());
spi.put(key, Instant.now());
return (qSession);
@@ -198,6 +190,58 @@ public class Auth0AuthenticationModule implements QAuthenticationModuleInterface
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private String getIdTokenFromBase64BasicAuthCredentials(AuthAPI auth, String base64Credentials) throws Auth0Exception
+ {
+ ////////////////////////////////////////////////////////////////////////////////
+ // look for a fresh idToken in the state provider for this set of credentials //
+ ////////////////////////////////////////////////////////////////////////////////
+ SimpleStateKey idTokenStateKey = new SimpleStateKey<>(base64Credentials + ":idToken");
+ SimpleStateKey timestampStateKey = new SimpleStateKey<>(base64Credentials + ":timestamp");
+ StateProviderInterface stateProvider = getStateProvider();
+ Optional cachedIdToken = stateProvider.get(String.class, idTokenStateKey);
+ Optional cachedTimestamp = stateProvider.get(Instant.class, timestampStateKey);
+ if(cachedIdToken.isPresent() && cachedTimestamp.isPresent())
+ {
+ if(cachedTimestamp.get().isAfter(Instant.now().minus(1, ChronoUnit.MINUTES)))
+ {
+ return cachedIdToken.get();
+ }
+ }
+
+ //////////////////////////////////////////////////////////////////////////////
+ // not found in cache, make request to auth0 and cache the returned idToken //
+ //////////////////////////////////////////////////////////////////////////////
+ byte[] credDecoded = Base64.getDecoder().decode(base64Credentials);
+ String credentials = new String(credDecoded, StandardCharsets.UTF_8);
+
+ String idToken = getIdTokenFromAuth0(auth, credentials);
+ stateProvider.put(idTokenStateKey, idToken);
+ stateProvider.put(timestampStateKey, Instant.now());
+ return (idToken);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ protected String getIdTokenFromAuth0(AuthAPI auth, String credentials) throws Auth0Exception
+ {
+ /////////////////////////////////////
+ // call auth0 with a login request //
+ /////////////////////////////////////
+ TokenHolder result = auth.login(credentials.split(":")[0], credentials.split(":")[1].toCharArray())
+ .setScope("openid email nickname")
+ .execute();
+
+ return (result.getIdToken());
+ }
+
+
+
/*******************************************************************************
**
*******************************************************************************/
@@ -215,7 +259,7 @@ public class Auth0AuthenticationModule implements QAuthenticationModuleInterface
}
StateProviderInterface spi = getStateProvider();
- Auth0StateKey key = new Auth0StateKey(session.getIdReference());
+ SimpleStateKey key = new SimpleStateKey<>(session.getIdReference());
Optional lastTimeCheckedOptional = spi.get(Instant.class, key);
if(lastTimeCheckedOptional.isPresent())
{
@@ -336,78 +380,4 @@ public class Auth0AuthenticationModule implements QAuthenticationModuleInterface
return (InMemoryStateProvider.getInstance());
}
-
-
- /*******************************************************************************
- **
- *******************************************************************************/
- public static class Auth0StateKey extends AbstractStateKey
- {
- private final String key;
-
-
-
- /*******************************************************************************
- ** Constructor.
- **
- *******************************************************************************/
- Auth0StateKey(String key)
- {
- this.key = key;
- }
-
-
-
- /*******************************************************************************
- **
- *******************************************************************************/
- @Override
- public String toString()
- {
- return (this.key);
- }
-
-
-
- /*******************************************************************************
- ** Make the key give a unique string to identify itself.
- *
- *******************************************************************************/
- @Override
- public String getUniqueIdentifier()
- {
- return (this.key);
- }
-
-
-
- /*******************************************************************************
- **
- *******************************************************************************/
- @Override
- public boolean equals(Object o)
- {
- if(this == o)
- {
- return true;
- }
- if(o == null || getClass() != o.getClass())
- {
- return false;
- }
- Auth0StateKey that = (Auth0StateKey) o;
- return key.equals(that.key);
- }
-
-
-
- /*******************************************************************************
- **
- *******************************************************************************/
- @Override
- public int hashCode()
- {
- return key.hashCode();
- }
- }
}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/state/SimpleStateKey.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/state/SimpleStateKey.java
new file mode 100644
index 00000000..61f19fdb
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/state/SimpleStateKey.java
@@ -0,0 +1,96 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2022. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.core.state;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public class SimpleStateKey extends AbstractStateKey
+{
+ private final T key;
+
+
+
+ /*******************************************************************************
+ ** Constructor.
+ **
+ *******************************************************************************/
+ public SimpleStateKey(T key)
+ {
+ this.key = key;
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ public String toString()
+ {
+ return (String.valueOf(this.key));
+ }
+
+
+
+ /*******************************************************************************
+ ** Make the key give a unique string to identify itself.
+ *
+ *******************************************************************************/
+ @Override
+ public String getUniqueIdentifier()
+ {
+ return (String.valueOf(this.key));
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ public boolean equals(Object o)
+ {
+ if(this == o)
+ {
+ return true;
+ }
+ if(o == null || getClass() != o.getClass())
+ {
+ return false;
+ }
+ SimpleStateKey> that = (SimpleStateKey>) o;
+ return key.equals(that.key);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ public int hashCode()
+ {
+ return key.hashCode();
+ }
+}
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 b2992cbe..918b57a2 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
@@ -24,17 +24,22 @@ package com.kingsrook.qqq.backend.core.modules.authentication.implementations;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
+import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
+import com.auth0.exception.Auth0Exception;
import com.kingsrook.qqq.backend.core.exceptions.QAuthenticationException;
+import com.kingsrook.qqq.backend.core.instances.QMetaDataVariableInterpreter;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.authentication.Auth0AuthenticationMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.authentication.QAuthenticationMetaData;
import com.kingsrook.qqq.backend.core.model.session.QSession;
import com.kingsrook.qqq.backend.core.state.InMemoryStateProvider;
+import com.kingsrook.qqq.backend.core.state.SimpleStateKey;
import com.kingsrook.qqq.backend.core.utils.TestUtils;
import org.junit.jupiter.api.Test;
import static com.kingsrook.qqq.backend.core.modules.authentication.implementations.Auth0AuthenticationModule.AUTH0_ID_TOKEN_KEY;
+import static com.kingsrook.qqq.backend.core.modules.authentication.implementations.Auth0AuthenticationModule.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;
import static com.kingsrook.qqq.backend.core.modules.authentication.implementations.Auth0AuthenticationModule.INVALID_TOKEN_ERROR;
@@ -43,6 +48,10 @@ import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
/*******************************************************************************
@@ -55,8 +64,6 @@ public class Auth0AuthenticationModuleTest
private static final String EXPIRED_TOKEN = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IllrY2FkWTA0Q3RFVUFxQUdLNTk3ayJ9.eyJnaXZlbl9uYW1lIjoiVGltIiwiZmFtaWx5X25hbWUiOiJDaGFtYmVybGFpbiIsIm5pY2tuYW1lIjoidGltLmNoYW1iZXJsYWluIiwibmFtZSI6IlRpbSBDaGFtYmVybGFpbiIsInBpY3R1cmUiOiJodHRwczovL2xoMy5nb29nbGV1c2VyY29udGVudC5jb20vYS0vQUZkWnVjcXVSaUFvTzk1RG9URklnbUtseVA1akVBVnZmWXFnS0lHTkVubzE9czk2LWMiLCJsb2NhbGUiOiJlbiIsInVwZGF0ZWRfYXQiOiIyMDIyLTA3LTE4VDIxOjM4OjE1LjM4NloiLCJlbWFpbCI6InRpbS5jaGFtYmVybGFpbkBraW5nc3Jvb2suY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsImlzcyI6Imh0dHBzOi8va2luZ3Nyb29rLnVzLmF1dGgwLmNvbS8iLCJzdWIiOiJnb29nbGUtb2F1dGgyfDEwODk2NDEyNjE3MjY1NzAzNDg2NyIsImF1ZCI6InNwQ1NtczAzcHpVZGRYN1BocHN4ZDlUd2FLMDlZZmNxIiwiaWF0IjoxNjU4MTgwNDc3LCJleHAiOjE2NTgyMTY0NzcsIm5vbmNlIjoiVkZkQlYzWmplR2hvY1cwMk9WZEtabHBLU0c1K1ZXbElhMEV3VkZaeFpVdEJVMDErZUZaT1RtMTNiZz09In0.fU7EwUgNrupOPz_PX_aQKON2xG1-LWD85xVo1Bn41WNEek-iMyJoch8l6NUihi7Bou14BoOfeWIG_sMqsLHqI2Pk7el7l1kigsjURx0wpiXadBt8piMxdIlxdToZEMuZCBzg7eJvXh4sM8tlV5cm0gPa6FT9Ih3VGJajNlXi5BcYS_JRpIvFvHn8-Bxj4KiAlZ5XPPkopjnDgP8kFfc4cMn_nxDkqWYlhj-5TaGW2xCLC9Qr_9UNxX0fm-CkKjYs3Z5ezbiXNkc-bxrCYvxeBeDPf8-T3EqrxCRVqCZSJ85BHdOc_E7UZC_g8bNj0umoplGwlCbzO4XIuOO-KlIaOg";
private static final String UNDECODABLE_TOKEN = "UNDECODABLE";
- public static final String AUTH0_BASE_URL = "https://kingsrook.us.auth0.com/";
-
/*******************************************************************************
@@ -109,7 +116,7 @@ public class Auth0AuthenticationModuleTest
/////////////////////////////////////////////////////////////
// put the input last-time-checked into the state provider //
/////////////////////////////////////////////////////////////
- Auth0AuthenticationModule.Auth0StateKey key = new Auth0AuthenticationModule.Auth0StateKey(token);
+ SimpleStateKey key = new SimpleStateKey<>(token);
InMemoryStateProvider.getInstance().put(key, lastTimeChecked);
//////////////////////
@@ -241,14 +248,38 @@ public class Auth0AuthenticationModuleTest
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testBasicAuthSuccess() throws QAuthenticationException, Auth0Exception
+ {
+ Map context = new HashMap<>();
+ context.put(BASIC_AUTH_KEY, encodeBasicAuth("darin.kelkhoff@gmail.com", "6-EQ!XzBJ!F*LRVDK6VZY__92!"));
+
+ Auth0AuthenticationModule auth0Spy = spy(Auth0AuthenticationModule.class);
+ auth0Spy.createSession(getQInstance(), context);
+ auth0Spy.createSession(getQInstance(), context);
+ auth0Spy.createSession(getQInstance(), context);
+ verify(auth0Spy, times(1)).getIdTokenFromAuth0(any(), any());
+ }
+
+
+
/*******************************************************************************
** utility method to prime a qInstance for auth0 tests
**
*******************************************************************************/
private QInstance getQInstance()
{
+ String auth0BaseUrl = new QMetaDataVariableInterpreter().interpret("${env.AUTH0_BASE_URL}");
+ String auth0ClientId = new QMetaDataVariableInterpreter().interpret("${env.AUTH0_CLIENT_ID}");
+ String auth0ClientSecret = new QMetaDataVariableInterpreter().interpret("${env.AUTH0_CLIENT_SECRET}");
+
QAuthenticationMetaData authenticationMetaData = new Auth0AuthenticationMetaData()
- .withBaseUrl(AUTH0_BASE_URL)
+ .withBaseUrl(auth0BaseUrl)
+ .withClientId(auth0ClientId)
+ .withClientSecret(auth0ClientSecret)
.withName("auth0");
QInstance qInstance = TestUtils.defineInstance();
@@ -256,4 +287,16 @@ public class Auth0AuthenticationModuleTest
return (qInstance);
}
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private String encodeBasicAuth(String username, String password)
+ {
+ Base64.Encoder encoder = Base64.getEncoder();
+ String originalString = username + ":" + password;
+ return (encoder.encodeToString(originalString.getBytes()));
+ }
+
}
diff --git a/qqq-dev-tools/bin/resolve-pom-conflicts.sh b/qqq-dev-tools/bin/resolve-pom-conflicts.sh
new file mode 100755
index 00000000..dd9e47b0
--- /dev/null
+++ b/qqq-dev-tools/bin/resolve-pom-conflicts.sh
@@ -0,0 +1,8 @@
+#!/bin/bash
+
+############################################################################
+## resolve-pom-conflicts.sh
+## Tries to automatically resove pom conflicts by putting SNAPSHOT back
+############################################################################
+gsed "/Updated upstream/,/=======/d" pom.xml | gsed "/Stashed/d" > /tmp/temp-pom.xml
+mv /tmp/temp-pom.xml pom.xml