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