From 661caaf38c00f8db3194d9738051a7acf6af5fd0 Mon Sep 17 00:00:00 2001 From: Tim Chamberlain Date: Tue, 19 Jul 2022 13:16:13 -0500 Subject: [PATCH 1/5] QQQ-27: updates to allow Auth0 to be an authentication model in QQQ --- pom.xml | 5 + .../exceptions/QAuthenticationException.java | 51 +++ .../model/actions/AbstractActionInput.java | 2 +- .../model/metadata/QAuthenticationType.java | 59 ++++ .../core/model/metadata/QInstance.java | 1 + .../backend/core/model/session/QSession.java | 3 +- .../Auth0AuthenticationModule.java | 333 ++++++++++++++++++ .../FullyAnonymousAuthenticationModule.java | 10 +- .../MockAuthenticationModule.java | 3 +- .../QAuthenticationModuleDispatcher.java | 14 +- .../QAuthenticationModuleInterface.java | 4 +- .../metadata/Auth0AuthenticationMetaData.java | 79 +++++ .../metadata/QAuthenticationMetaData.java | 11 +- .../Auth0AuthenticationModuleTest.java | 223 ++++++++++++ ...ullyAnonymousAuthenticationModuleTest.java | 8 +- .../QAuthenticationModuleDispatcherTest.java | 4 +- .../qqq/backend/core/utils/TestUtils.java | 7 +- 17 files changed, 785 insertions(+), 32 deletions(-) create mode 100644 src/main/java/com/kingsrook/qqq/backend/core/exceptions/QAuthenticationException.java create mode 100644 src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QAuthenticationType.java create mode 100644 src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/Auth0AuthenticationModule.java create mode 100644 src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/metadata/Auth0AuthenticationMetaData.java rename src/main/java/com/kingsrook/qqq/backend/core/{model => modules/authentication}/metadata/QAuthenticationMetaData.java (93%) create mode 100644 src/test/java/com/kingsrook/qqq/backend/core/modules/authentication/Auth0AuthenticationModuleTest.java diff --git a/pom.xml b/pom.xml index 077ba675..b7fd2a16 100644 --- a/pom.xml +++ b/pom.xml @@ -53,6 +53,11 @@ + + com.auth0 + mvc-auth-commons + [1.0, 2.0) + com.fasterxml.jackson.core jackson-databind diff --git a/src/main/java/com/kingsrook/qqq/backend/core/exceptions/QAuthenticationException.java b/src/main/java/com/kingsrook/qqq/backend/core/exceptions/QAuthenticationException.java new file mode 100644 index 00000000..58478273 --- /dev/null +++ b/src/main/java/com/kingsrook/qqq/backend/core/exceptions/QAuthenticationException.java @@ -0,0 +1,51 @@ +/* + * 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.exceptions; + + +/******************************************************************************* + * Exception thrown while doing module-dispatch + * + *******************************************************************************/ +public class QAuthenticationException extends QException +{ + + /******************************************************************************* + ** Constructor of message + ** + *******************************************************************************/ + public QAuthenticationException(String message) + { + super(message); + } + + + + /******************************************************************************* + ** Constructor of message & cause + ** + *******************************************************************************/ + public QAuthenticationException(String message, Throwable cause) + { + super(message, cause); + } +} diff --git a/src/main/java/com/kingsrook/qqq/backend/core/model/actions/AbstractActionInput.java b/src/main/java/com/kingsrook/qqq/backend/core/model/actions/AbstractActionInput.java index c8cc23b7..9907c9ba 100644 --- a/src/main/java/com/kingsrook/qqq/backend/core/model/actions/AbstractActionInput.java +++ b/src/main/java/com/kingsrook/qqq/backend/core/model/actions/AbstractActionInput.java @@ -24,7 +24,7 @@ package com.kingsrook.qqq.backend.core.model.actions; import com.kingsrook.qqq.backend.core.exceptions.QInstanceValidationException; import com.kingsrook.qqq.backend.core.instances.QInstanceValidator; -import com.kingsrook.qqq.backend.core.model.metadata.QAuthenticationMetaData; +import com.kingsrook.qqq.backend.core.modules.authentication.metadata.QAuthenticationMetaData; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.session.QSession; import org.apache.logging.log4j.LogManager; diff --git a/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QAuthenticationType.java b/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QAuthenticationType.java new file mode 100644 index 00000000..afc24aaa --- /dev/null +++ b/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QAuthenticationType.java @@ -0,0 +1,59 @@ +/* + * 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.model.metadata; + + +/******************************************************************************* + ** Enum to define the possible authentication types + ** + *******************************************************************************/ +@SuppressWarnings("rawtypes") +public enum QAuthenticationType +{ + AUTH_0("auth0"), + FULLY_ANONYMOUS("fullyAnonymous"), + MOCK("mock"); + + private final String name; + + + + /******************************************************************************* + ** enum constructor + *******************************************************************************/ + QAuthenticationType(String name) + { + this.name = name; + } + + + + /******************************************************************************* + ** Getter for name + ** + *******************************************************************************/ + public String getName() + { + return this.name; + } + +} diff --git a/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QInstance.java b/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QInstance.java index af087399..620f9f65 100644 --- a/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QInstance.java +++ b/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QInstance.java @@ -32,6 +32,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleVal import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData; import com.kingsrook.qqq.backend.core.model.metadata.processes.QStepMetaData; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; +import com.kingsrook.qqq.backend.core.modules.authentication.metadata.QAuthenticationMetaData; /******************************************************************************* diff --git a/src/main/java/com/kingsrook/qqq/backend/core/model/session/QSession.java b/src/main/java/com/kingsrook/qqq/backend/core/model/session/QSession.java index 9a37780c..f0ba6a22 100644 --- a/src/main/java/com/kingsrook/qqq/backend/core/model/session/QSession.java +++ b/src/main/java/com/kingsrook/qqq/backend/core/model/session/QSession.java @@ -22,6 +22,7 @@ package com.kingsrook.qqq.backend.core.model.session; +import java.io.Serializable; import java.util.HashMap; import java.util.Map; @@ -29,7 +30,7 @@ import java.util.Map; /******************************************************************************* ** *******************************************************************************/ -public class QSession +public class QSession implements Serializable { private String idReference; private QUser user; diff --git a/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/Auth0AuthenticationModule.java b/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/Auth0AuthenticationModule.java new file mode 100644 index 00000000..2962fabd --- /dev/null +++ b/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/Auth0AuthenticationModule.java @@ -0,0 +1,333 @@ +/* + * 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.modules.authentication; + + +import java.security.interfaces.RSAPublicKey; +import java.time.Duration; +import java.time.Instant; +import java.util.Base64; +import java.util.Map; +import com.auth0.jwk.Jwk; +import com.auth0.jwk.JwkException; +import com.auth0.jwk.JwkProvider; +import com.auth0.jwk.UrlJwkProvider; +import com.auth0.jwt.JWT; +import com.auth0.jwt.algorithms.Algorithm; +import com.auth0.jwt.exceptions.JWTDecodeException; +import com.auth0.jwt.exceptions.JWTVerificationException; +import com.auth0.jwt.exceptions.TokenExpiredException; +import com.auth0.jwt.interfaces.DecodedJWT; +import com.auth0.jwt.interfaces.JWTVerifier; +import com.kingsrook.qqq.backend.core.exceptions.QAuthenticationException; +import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +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.metadata.Auth0AuthenticationMetaData; +import com.kingsrook.qqq.backend.core.state.AbstractStateKey; +import com.kingsrook.qqq.backend.core.state.InMemoryStateProvider; +import com.kingsrook.qqq.backend.core.state.StateProviderInterface; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.json.JSONObject; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class Auth0AuthenticationModule implements QAuthenticationModuleInterface +{ + private static final Logger logger = LogManager.getLogger(Auth0AuthenticationModule.class); + + private static final int ID_TOKEN_VALIDATION_INTERVAL_SECONDS = 300; + + public static final String AUTH0_ID_TOKEN_KEY = "qqq.idToken"; + + public static final String TOKEN_NOT_PROVIDED_ERROR = "Id Token was not provided"; + public static final String COULD_NOT_DECODE_ERROR = "Unable to decode id token"; + public static final String EXPIRED_TOKEN_ERROR = "Token has expired"; + public static final String INVALID_TOKEN_ERROR = "An invalid token was provided"; + + + private Instant now; + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public QSession createSession(QInstance qInstance, Map context) throws QAuthenticationException + { + ////////////////////////////////////////////////// + // get the jwt id token from the context object // + ////////////////////////////////////////////////// + String idToken = context.get(AUTH0_ID_TOKEN_KEY); + if(idToken == null) + { + //////////////////////////////// + // could not decode the token // + //////////////////////////////// + logger.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 + { + ///////////////////////////////////////////////////// + // try to build session to see if still valid // + // then call method to check more session validity // + ///////////////////////////////////////////////////// + QSession qSession = buildQSessionFromToken(idToken); + if(isSessionValid(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 = revalidateToken(qInstance, idToken); + + //////////////////////////////////////////////////////////////////// + // put now into state so we dont check until next interval passes // + /////////////////////////////////////////////////////////////////// + StateProviderInterface spi = getStateProvider(); + Auth0StateKey key = new Auth0StateKey(qSession.getIdReference()); + spi.put(key, getNow()); + + return (qSession); + } + catch(JWTDecodeException jde) + { + //////////////////////////////// + // could not decode the token // + //////////////////////////////// + logger.warn(COULD_NOT_DECODE_ERROR, jde); + throw (new QAuthenticationException(COULD_NOT_DECODE_ERROR)); + } + catch(TokenExpiredException tee) + { + logger.info(EXPIRED_TOKEN_ERROR, tee); + throw (new QAuthenticationException(EXPIRED_TOKEN_ERROR)); + } + catch(JWTVerificationException | JwkException jve) + { + /////////////////////////////////////////// + // token had invalid signature or claims // + /////////////////////////////////////////// + logger.warn(INVALID_TOKEN_ERROR, jve); + throw (new QAuthenticationException(INVALID_TOKEN_ERROR)); + } + catch(Exception e) + { + //////////////// + // ¯\_(ツ)_/¯ // + //////////////// + String message = "An unknown error occurred"; + logger.error(message, e); + throw (new QAuthenticationException(message)); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public boolean isSessionValid(QSession session) + { + if(session == null) + { + return (false); + } + + StateProviderInterface spi = getStateProvider(); + Auth0StateKey key = new Auth0StateKey(session.getIdReference()); + if(spi.get(Instant.class, key).isPresent()) + { + Instant lastTimeChecked = spi.get(Instant.class, key).get(); + + /////////////////////////////////////////////////////////////////////////////////////////////////// + // returns negative int if less than compared duration, 0 if equal, positive int if greater than // + // - so this is basically saying, if the time between the last time we checked the token and // + // right now is more than ID_TOKEN_VALIDATION_INTERVAL_SECTIONS, then session needs revalidated // + /////////////////////////////////////////////////////////////////////////////////////////////////// + return (Duration.between(lastTimeChecked, getNow()).compareTo(Duration.ofSeconds(ID_TOKEN_VALIDATION_INTERVAL_SECONDS)) < 0); + } + + return (false); + } + + + + /******************************************************************************* + ** public method so that 'now' can be used for testing purposes + ** - defaults to real 'now' + *******************************************************************************/ + public Instant getNow() + { + if(now == null) + { + now = Instant.now(); + } + + return (now); + } + + + + /******************************************************************************* + ** public method so that 'now' can be set for testing purposes + *******************************************************************************/ + public void setNow(Instant now) + { + this.now = now; + } + + + + /******************************************************************************* + ** makes request to check if a token is still valid and build new qSession if it is + ** + *******************************************************************************/ + private QSession revalidateToken(QInstance qInstance, String idToken) throws JwkException + { + Auth0AuthenticationMetaData metaData = (Auth0AuthenticationMetaData) qInstance.getAuthentication(); + + DecodedJWT jwt = JWT.decode(idToken); + JwkProvider provider = new UrlJwkProvider(metaData.getBaseUrl()); + Jwk jwk = provider.get(jwt.getKeyId()); + Algorithm algorithm = Algorithm.RSA256((RSAPublicKey) jwk.getPublicKey(), null); + JWTVerifier verifier = JWT.require(algorithm) + .withIssuer(metaData.getBaseUrl()) + .build(); + + /////////////////////////////////// + // make call to verify the token // + /////////////////////////////////// + verifier.verify(idToken); + + return (buildQSessionFromToken(idToken)); + } + + + + /******************************************************************************* + ** extracts info from token creating a QSession + ** + *******************************************************************************/ + private QSession buildQSessionFromToken(String idToken) throws JwkException + { + //////////////////////////////////// + // decode and extract the payload // + //////////////////////////////////// + DecodedJWT jwt = JWT.decode(idToken); + Base64.Decoder decoder = Base64.getUrlDecoder(); + String payloadString = new String(decoder.decode(jwt.getPayload())); + JSONObject payload = new JSONObject(payloadString); + + QUser qUser = new QUser(); + qUser.setIdReference(payload.getString("email")); + qUser.setFullName(payload.getString("name")); + + QSession qSession = new QSession(); + qSession.setIdReference(idToken); + qSession.setUser(qUser); + + return (qSession); + } + + + + /******************************************************************************* + ** Load an instance of the appropriate state provider + ** + *******************************************************************************/ + public static StateProviderInterface getStateProvider() + { + // TODO - read this from somewhere in meta data eh? + return (InMemoryStateProvider.getInstance()); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static class Auth0StateKey extends AbstractStateKey + { + private final String key; + + + + /******************************************************************************* + ** Constructor. + ** + *******************************************************************************/ + Auth0StateKey(String key) + { + this.key = key; + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public String toString() + { + 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/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/FullyAnonymousAuthenticationModule.java b/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/FullyAnonymousAuthenticationModule.java index ebc573c4..442941b5 100644 --- a/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/FullyAnonymousAuthenticationModule.java +++ b/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/FullyAnonymousAuthenticationModule.java @@ -24,9 +24,9 @@ package com.kingsrook.qqq.backend.core.modules.authentication; import java.util.Map; import java.util.UUID; +import com.kingsrook.qqq.backend.core.model.metadata.QInstance; 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; /******************************************************************************* @@ -40,7 +40,7 @@ public class FullyAnonymousAuthenticationModule implements QAuthenticationModule ** *******************************************************************************/ @Override - public QSession createSession(Map context) + public QSession createSession(QInstance qInstance, Map context) { QUser qUser = new QUser(); qUser.setIdReference("anonymous"); @@ -68,10 +68,6 @@ public class FullyAnonymousAuthenticationModule implements QAuthenticationModule @Override public boolean isSessionValid(QSession session) { - if(session == null) - { - return (false); - } - return (true); + return session != null; } } diff --git a/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/MockAuthenticationModule.java b/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/MockAuthenticationModule.java index 1bd72ae2..21b25c2a 100644 --- a/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/MockAuthenticationModule.java +++ b/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/MockAuthenticationModule.java @@ -24,6 +24,7 @@ package com.kingsrook.qqq.backend.core.modules.authentication; import java.util.Map; import java.util.UUID; +import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.session.QSession; import com.kingsrook.qqq.backend.core.model.session.QUser; import org.apache.logging.log4j.LogManager; @@ -43,7 +44,7 @@ public class MockAuthenticationModule implements QAuthenticationModuleInterface ** *******************************************************************************/ @Override - public QSession createSession(Map context) + public QSession createSession(QInstance qInstance, Map context) { QUser qUser = new QUser(); qUser.setIdReference("User:" + (System.currentTimeMillis() % USER_ID_MODULO)); diff --git a/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/QAuthenticationModuleDispatcher.java b/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/QAuthenticationModuleDispatcher.java index 0e32a1d7..258509f2 100644 --- a/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/QAuthenticationModuleDispatcher.java +++ b/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/QAuthenticationModuleDispatcher.java @@ -25,8 +25,8 @@ package com.kingsrook.qqq.backend.core.modules.authentication; import java.util.HashMap; import java.util.Map; import com.kingsrook.qqq.backend.core.exceptions.QModuleDispatchException; -import com.kingsrook.qqq.backend.core.model.metadata.QAuthenticationMetaData; -import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModuleInterface; +import com.kingsrook.qqq.backend.core.model.metadata.QAuthenticationType; +import com.kingsrook.qqq.backend.core.modules.authentication.metadata.QAuthenticationMetaData; /******************************************************************************* @@ -38,7 +38,7 @@ import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModu *******************************************************************************/ public class QAuthenticationModuleDispatcher { - private Map authenticationTypeToModuleClassNameMap; + private final Map authenticationTypeToModuleClassNameMap; @@ -48,9 +48,9 @@ public class QAuthenticationModuleDispatcher public QAuthenticationModuleDispatcher() { authenticationTypeToModuleClassNameMap = new HashMap<>(); - authenticationTypeToModuleClassNameMap.put("mock", "com.kingsrook.qqq.backend.core.modules.authentication.MockAuthenticationModule"); - authenticationTypeToModuleClassNameMap.put("fullyAnonymous", "com.kingsrook.qqq.backend.core.modules.authentication.FullyAnonymousAuthenticationModule"); - authenticationTypeToModuleClassNameMap.put("TODO:google", "com.kingsrook.qqq.authentication.module.google.GoogleAuthenticationModule"); + authenticationTypeToModuleClassNameMap.put(QAuthenticationType.MOCK.getName(), "com.kingsrook.qqq.backend.core.modules.authentication.MockAuthenticationModule"); + authenticationTypeToModuleClassNameMap.put(QAuthenticationType.FULLY_ANONYMOUS.getName(), "com.kingsrook.qqq.backend.core.modules.authentication.FullyAnonymousAuthenticationModule"); + authenticationTypeToModuleClassNameMap.put(QAuthenticationType.AUTH_0.getName(), "com.kingsrook.qqq.backend.core.modules.authentication.Auth0AuthenticationModule"); // todo - let user define custom type -> classes } @@ -66,7 +66,7 @@ public class QAuthenticationModuleDispatcher throw (new QModuleDispatchException("No authentication meta data defined.")); } - return getQModule(authenticationMetaData.getType()); + return getQModule(authenticationMetaData.getType().getName()); } diff --git a/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/QAuthenticationModuleInterface.java b/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/QAuthenticationModuleInterface.java index 8ab7b07c..ec5db589 100644 --- a/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/QAuthenticationModuleInterface.java +++ b/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/QAuthenticationModuleInterface.java @@ -23,6 +23,8 @@ package com.kingsrook.qqq.backend.core.modules.authentication; import java.util.Map; +import com.kingsrook.qqq.backend.core.exceptions.QAuthenticationException; +import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.session.QSession; @@ -35,7 +37,7 @@ public interface QAuthenticationModuleInterface /******************************************************************************* ** *******************************************************************************/ - QSession createSession(Map context); + QSession createSession(QInstance qInstance, Map context) throws QAuthenticationException; /******************************************************************************* diff --git a/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/metadata/Auth0AuthenticationMetaData.java b/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/metadata/Auth0AuthenticationMetaData.java new file mode 100644 index 00000000..ad0b9dd6 --- /dev/null +++ b/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/metadata/Auth0AuthenticationMetaData.java @@ -0,0 +1,79 @@ +/* + * 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.modules.authentication.metadata; + + +import com.kingsrook.qqq.backend.core.model.metadata.QAuthenticationType; + + +/******************************************************************************* + ** Meta-data to provide details of an RDBMS backend (e.g., connection params) + *******************************************************************************/ +public class Auth0AuthenticationMetaData extends QAuthenticationMetaData +{ + private String baseUrl; + + + + /******************************************************************************* + ** Default Constructor. + *******************************************************************************/ + public Auth0AuthenticationMetaData() + { + super(); + setType(QAuthenticationType.AUTH_0); + } + + + + /******************************************************************************* + ** Fluent setter, override to help fluent flows + *******************************************************************************/ + public Auth0AuthenticationMetaData withBaseUrl(String baseUrl) + { + setBaseUrl(baseUrl); + return this; + } + + + + /******************************************************************************* + ** Getter for baseUrl + ** + *******************************************************************************/ + public String getBaseUrl() + { + return baseUrl; + } + + + + /******************************************************************************* + ** Setter for baseUrl + ** + *******************************************************************************/ + public void setBaseUrl(String baseUrl) + { + this.baseUrl = baseUrl; + } + +} diff --git a/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QAuthenticationMetaData.java b/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/metadata/QAuthenticationMetaData.java similarity index 93% rename from src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QAuthenticationMetaData.java rename to src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/metadata/QAuthenticationMetaData.java index a14e58ac..7e259de5 100644 --- a/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QAuthenticationMetaData.java +++ b/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/metadata/QAuthenticationMetaData.java @@ -19,12 +19,13 @@ * along with this program. If not, see . */ -package com.kingsrook.qqq.backend.core.model.metadata; +package com.kingsrook.qqq.backend.core.modules.authentication.metadata; import java.util.HashMap; import java.util.Map; import com.fasterxml.jackson.annotation.JsonFilter; +import com.kingsrook.qqq.backend.core.model.metadata.QAuthenticationType; /******************************************************************************* @@ -35,7 +36,7 @@ import com.fasterxml.jackson.annotation.JsonFilter; public class QAuthenticationMetaData { private String name; - private String type; + private QAuthenticationType type; @JsonFilter("secretsFilter") private Map values; @@ -120,7 +121,7 @@ public class QAuthenticationMetaData ** Getter for type ** *******************************************************************************/ - public String getType() + public QAuthenticationType getType() { return type; } @@ -131,7 +132,7 @@ public class QAuthenticationMetaData ** Setter for type ** *******************************************************************************/ - public void setType(String type) + public void setType(QAuthenticationType type) { this.type = type; } @@ -141,7 +142,7 @@ public class QAuthenticationMetaData /******************************************************************************* ** *******************************************************************************/ - public QAuthenticationMetaData withType(String type) + public QAuthenticationMetaData withType(QAuthenticationType type) { this.type = type; return (this); diff --git a/src/test/java/com/kingsrook/qqq/backend/core/modules/authentication/Auth0AuthenticationModuleTest.java b/src/test/java/com/kingsrook/qqq/backend/core/modules/authentication/Auth0AuthenticationModuleTest.java new file mode 100644 index 00000000..219eba62 --- /dev/null +++ b/src/test/java/com/kingsrook/qqq/backend/core/modules/authentication/Auth0AuthenticationModuleTest.java @@ -0,0 +1,223 @@ +/* + * 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.modules.authentication; + + +import java.time.Instant; +import java.util.HashMap; +import java.util.Map; +import com.kingsrook.qqq.backend.core.exceptions.QAuthenticationException; +import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import com.kingsrook.qqq.backend.core.model.session.QSession; +import com.kingsrook.qqq.backend.core.modules.authentication.metadata.Auth0AuthenticationMetaData; +import com.kingsrook.qqq.backend.core.modules.authentication.metadata.QAuthenticationMetaData; +import com.kingsrook.qqq.backend.core.utils.TestUtils; +import org.junit.jupiter.api.Test; +import static com.kingsrook.qqq.backend.core.modules.authentication.Auth0AuthenticationModule.AUTH0_ID_TOKEN_KEY; +import static com.kingsrook.qqq.backend.core.modules.authentication.Auth0AuthenticationModule.COULD_NOT_DECODE_ERROR; +import static com.kingsrook.qqq.backend.core.modules.authentication.Auth0AuthenticationModule.EXPIRED_TOKEN_ERROR; +import static com.kingsrook.qqq.backend.core.modules.authentication.Auth0AuthenticationModule.INVALID_TOKEN_ERROR; +import static com.kingsrook.qqq.backend.core.modules.authentication.Auth0AuthenticationModule.TOKEN_NOT_PROVIDED_ERROR; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + + +/******************************************************************************* + ** Unit test for the FullyAnonymousAuthenticationModule + *******************************************************************************/ +public class Auth0AuthenticationModuleTest +{ + private static final String VALID_TOKEN = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IllrY2FkWTA0Q3RFVUFxQUdLNTk3ayJ9.eyJnaXZlbl9uYW1lIjoiVGltIiwiZmFtaWx5X25hbWUiOiJDaGFtYmVybGFpbiIsIm5pY2tuYW1lIjoidGltLmNoYW1iZXJsYWluIiwibmFtZSI6IlRpbSBDaGFtYmVybGFpbiIsInBpY3R1cmUiOiJodHRwczovL2xoMy5nb29nbGV1c2VyY29udGVudC5jb20vYS0vQUZkWnVjcXVSaUFvTzk1RG9URklnbUtseVA1akVBVnZmWXFnS0lHTkVubzE9czk2LWMiLCJsb2NhbGUiOiJlbiIsInVwZGF0ZWRfYXQiOiIyMDIyLTA3LTE5VDE2OjI0OjQ1LjgyMloiLCJlbWFpbCI6InRpbS5jaGFtYmVybGFpbkBraW5nc3Jvb2suY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsImlzcyI6Imh0dHBzOi8va2luZ3Nyb29rLnVzLmF1dGgwLmNvbS8iLCJzdWIiOiJnb29nbGUtb2F1dGgyfDEwODk2NDEyNjE3MjY1NzAzNDg2NyIsImF1ZCI6InNwQ1NtczAzcHpVZGRYN1BocHN4ZDlUd2FLMDlZZmNxIiwiaWF0IjoxNjU4MjQ3OTAyLCJleHAiOjE2NTgyODM5MDIsIm5vbmNlIjoiZUhOdFMxbEtUR2N5ZG5KS1VVY3RkRTFVT0ZKNmJFNUxVVkEwZEdsRGVXOXZkVkl4UW41eVRrUlJlZz09In0.hib7JR8NDU2kx8Fj1bnzo3IUuabE6Hb-Z7HHZAJPQuF_Zdg3L1KDypn6SY7HAd_dsz2N8RkXfvQto-Y2g2ukuz7FxzNFgcVL99cyEO3YqmyCa6JTOTCrxdeaIE8QZpCEKvC28oeJBv0wO1Dwc--OVJMsK2vSzyxj1WNok64YYjWKLL4c0dFf-nj0KWFr1IU-tMiyWLDDiJw2Sa8M4YxXZYqdlkgNmrBPExgcm9l9SiT2l3Ts3Sgc_IyMVyMrnV8XX50EWdsm6vuCOSUcqf0XhjDQ7urZveoVwVLnYq3GcLhVBcy1Hr9RL8zPdPynOzsbX6uCww2Esrv6iwWrgQ5zBA"; + private static final String INVALID_TOKEN = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IllrY2FkWTA0Q3RFVUFxQUdLNTk3ayJ9.eyJnaXZlbl9uYW1lIjoiVGltIiwiZmFtaWx5X25hbWUiOiJDaGFtYmVybGFpbiIsIm5pY2tuYW1lIjoidGltLmNoYW1iZXJsYWluIiwibmFtZSI6IlRpbSBDaGFtYmVybGFpbiIsInBpY3R1cmUiOiJodHRwczovL2xoMy5nb29nbGV1c2VyY29udGVudC5jb20vYS0vQUZkWnVjcXVSaUFvTzk1RG9URklnbUtseVA1akVBVnZmWXFnS0lHTkVubzE9czk2LWMiLCJsb2NhbGUiOiJlbiIsInVwZGF0ZWRfYXQiOiIyMDIyLTA3LTE5VDE2OjI0OjQ1LjgyMloiLCJlbWFpbCI6InRpbS5jaGFtYmVybGFpbkBraW5nc3Jvb2suY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsImlzcyI6Imh0dHBzOi8va2luZ3Nyb29rLnVzLmF1dGgwLmNvbS8iLCJzdWIiOiJnb29nbGUtb2F1dGgyfDEwODk2NDEyNjE3MjY1NzAzNDg2NyIsImF1ZCI6InNwQ1NtczAzcHpVZGRYN1BocHN4ZDlUd2FLMDlZZmNxIiwiaWF0IjoxNjU4MjQ3OTAyLCJleHAiOjE2NTgyODM5MDIsIm5vbmNlIjoiZUhOdFMxbEtUR2N5ZG5KS1VVY3RkRTFVT0ZKNmJFNUxVVkEwZEdsRGVXOXZkVkl4UW41eVRrUlJlZz09In0.hib7JR8NDU2kx8Fj1bnzo3IUuabE6Hb-Z7HHZAJPQuF_Zdg3L1KDypn6SY7HAd_dsz2N8RkXfvQto-Y2g2ukuz7FxzNFgcVL99cyEO3YqmyCa6JTOTCrxdeaIE8QZpCEKvC28oeJBv0wO1Dwc--OVJMsK2vSzyxj1WNok64YYjWKLL4c0dFf-nj0KWFr1IU-tMiyWLDDiJw2Sa8M4YxXZYqdlkgNmrBPExgcm9l9SiT2l3Ts3Sgc_IyMVyMrnV8XX50EWdsm6vuCOSUcqf0XhjDQ7urZveoVwVLnYq3GcLhVBcy1Hr9RL8zPdPynOzsbX6uCww2Esrv6iwWrgQ5zBA-thismakesinvalid"; + 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/"; + + + + /******************************************************************************* + ** Test a valid token where 'now' is set to a time that would be valid for it + ** + *******************************************************************************/ + @Test + public void testValidToken() throws QAuthenticationException + { + Map context = new HashMap<>(); + context.put(AUTH0_ID_TOKEN_KEY, VALID_TOKEN); + + ////////////////////////////////////////////////////////// + // Tuesday, July 19, 2022 12:40:27.299 PM GMT-05:00 DST // + ////////////////////////////////////////////////////////// + Instant now = Instant.ofEpochMilli(1658252427299L); + + Auth0AuthenticationModule auth0AuthenticationModule = new Auth0AuthenticationModule(); + auth0AuthenticationModule.setNow(now); + QSession session = auth0AuthenticationModule.createSession(getQInstance(), context); + assertTrue(session.getUser().getIdReference().equals("tim.chamberlain@kingsrook.com")); + assertTrue(session.getUser().getFullName().equals("Tim Chamberlain")); + } + + + + /******************************************************************************* + ** Test failure case, token is invalid + ** + *******************************************************************************/ + @Test + public void testInvalidToken() + { + Map context = new HashMap<>(); + context.put(AUTH0_ID_TOKEN_KEY, INVALID_TOKEN); + + try + { + Auth0AuthenticationModule auth0AuthenticationModule = new Auth0AuthenticationModule(); + auth0AuthenticationModule.createSession(getQInstance(), context); + } + catch(QAuthenticationException qae) + { + assertTrue(qae.getMessage().contains(INVALID_TOKEN_ERROR)); + return; + } + + fail("Should never get here"); + } + + + + /******************************************************************************* + ** Test failure case, token cant be decoded + ** + *******************************************************************************/ + @Test + public void testUndecodableToken() + { + Map context = new HashMap<>(); + context.put(AUTH0_ID_TOKEN_KEY, UNDECODABLE_TOKEN); + + try + { + Auth0AuthenticationModule auth0AuthenticationModule = new Auth0AuthenticationModule(); + auth0AuthenticationModule.createSession(getQInstance(), context); + } + catch(QAuthenticationException qae) + { + assertTrue(qae.getMessage().contains(COULD_NOT_DECODE_ERROR)); + return; + } + + fail("Should never get here"); + } + + + + /******************************************************************************* + ** Test failure case, token is expired + ** + *******************************************************************************/ + @Test + public void testProperlyFormattedButExpiredToken() + { + Map context = new HashMap<>(); + context.put(AUTH0_ID_TOKEN_KEY, EXPIRED_TOKEN); + + try + { + Auth0AuthenticationModule auth0AuthenticationModule = new Auth0AuthenticationModule(); + auth0AuthenticationModule.createSession(getQInstance(), context); + } + catch(QAuthenticationException qae) + { + assertTrue(qae.getMessage().contains(EXPIRED_TOKEN_ERROR)); + return; + } + + fail("Should never get here"); + } + + + + /******************************************************************************* + ** Test failure case, empty context + ** + *******************************************************************************/ + @Test + public void testEmptyContext() + { + try + { + Auth0AuthenticationModule auth0AuthenticationModule = new Auth0AuthenticationModule(); + auth0AuthenticationModule.createSession(getQInstance(), new HashMap<>()); + } + catch(QAuthenticationException qae) + { + assertTrue(qae.getMessage().contains(TOKEN_NOT_PROVIDED_ERROR)); + return; + } + + fail("Should never get here"); + } + + + + /******************************************************************************* + ** Test failure case, null token + ** + *******************************************************************************/ + @Test + public void testNullToken() + { + Map context = new HashMap<>(); + context.put(AUTH0_ID_TOKEN_KEY, null); + + try + { + Auth0AuthenticationModule auth0AuthenticationModule = new Auth0AuthenticationModule(); + auth0AuthenticationModule.createSession(getQInstance(), context); + } + catch(QAuthenticationException qae) + { + assertTrue(qae.getMessage().contains(TOKEN_NOT_PROVIDED_ERROR)); + return; + } + + fail("Should never get here"); + } + + + + /******************************************************************************* + ** utility method to prime a qInstance for auth0 tests + ** + *******************************************************************************/ + private QInstance getQInstance() + { + QAuthenticationMetaData authenticationMetaData = new Auth0AuthenticationMetaData() + .withBaseUrl(AUTH0_BASE_URL) + .withName("auth0"); + + QInstance qInstance = TestUtils.defineInstance(); + qInstance.setAuthentication(authenticationMetaData); + return (qInstance); + } +} diff --git a/src/test/java/com/kingsrook/qqq/backend/core/modules/authentication/FullyAnonymousAuthenticationModuleTest.java b/src/test/java/com/kingsrook/qqq/backend/core/modules/authentication/FullyAnonymousAuthenticationModuleTest.java index 7f46b9f7..68c1e7d8 100644 --- a/src/test/java/com/kingsrook/qqq/backend/core/modules/authentication/FullyAnonymousAuthenticationModuleTest.java +++ b/src/test/java/com/kingsrook/qqq/backend/core/modules/authentication/FullyAnonymousAuthenticationModuleTest.java @@ -23,7 +23,6 @@ package com.kingsrook.qqq.backend.core.modules.authentication; import com.kingsrook.qqq.backend.core.model.session.QSession; -import com.kingsrook.qqq.backend.core.modules.authentication.FullyAnonymousAuthenticationModule; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -36,12 +35,15 @@ import static org.junit.jupiter.api.Assertions.assertTrue; public class FullyAnonymousAuthenticationModuleTest { + /******************************************************************************* + ** + *******************************************************************************/ @Test public void test() { FullyAnonymousAuthenticationModule fullyAnonymousAuthenticationModule = new FullyAnonymousAuthenticationModule(); - QSession session = fullyAnonymousAuthenticationModule.createSession(null); + QSession session = fullyAnonymousAuthenticationModule.createSession(null, null); assertNotNull(session, "Session should not be null"); assertNotNull(session.getIdReference(), "Session id ref should not be null"); @@ -51,4 +53,4 @@ public class FullyAnonymousAuthenticationModuleTest assertFalse(fullyAnonymousAuthenticationModule.isSessionValid(null), "null should be not valid"); } -} \ No newline at end of file +} diff --git a/src/test/java/com/kingsrook/qqq/backend/core/modules/authentication/QAuthenticationModuleDispatcherTest.java b/src/test/java/com/kingsrook/qqq/backend/core/modules/authentication/QAuthenticationModuleDispatcherTest.java index 9b366a3c..44a0ae3b 100644 --- a/src/test/java/com/kingsrook/qqq/backend/core/modules/authentication/QAuthenticationModuleDispatcherTest.java +++ b/src/test/java/com/kingsrook/qqq/backend/core/modules/authentication/QAuthenticationModuleDispatcherTest.java @@ -23,9 +23,7 @@ package com.kingsrook.qqq.backend.core.modules.authentication; import com.kingsrook.qqq.backend.core.exceptions.QModuleDispatchException; -import com.kingsrook.qqq.backend.core.model.metadata.QAuthenticationMetaData; -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.metadata.QAuthenticationMetaData; import com.kingsrook.qqq.backend.core.utils.TestUtils; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertNotNull; diff --git a/src/test/java/com/kingsrook/qqq/backend/core/utils/TestUtils.java b/src/test/java/com/kingsrook/qqq/backend/core/utils/TestUtils.java index 02828726..814a1aac 100644 --- a/src/test/java/com/kingsrook/qqq/backend/core/utils/TestUtils.java +++ b/src/test/java/com/kingsrook/qqq/backend/core/utils/TestUtils.java @@ -26,8 +26,9 @@ import java.util.List; import com.kingsrook.qqq.backend.core.actions.processes.person.addtopeoplesage.AddAge; import com.kingsrook.qqq.backend.core.actions.processes.person.addtopeoplesage.GetAgeStatistics; import com.kingsrook.qqq.backend.core.adapters.QInstanceAdapter; +import com.kingsrook.qqq.backend.core.model.metadata.QAuthenticationType; import com.kingsrook.qqq.backend.core.processes.implementations.mock.MockBackendStep; -import com.kingsrook.qqq.backend.core.model.metadata.QAuthenticationMetaData; +import com.kingsrook.qqq.backend.core.modules.authentication.metadata.QAuthenticationMetaData; import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData; import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeType; @@ -109,7 +110,7 @@ public class TestUtils { return new QAuthenticationMetaData() .withName("mock") - .withType("mock"); + .withType(QAuthenticationType.MOCK); } @@ -304,6 +305,6 @@ public class TestUtils public static QSession getMockSession() { MockAuthenticationModule mockAuthenticationModule = new MockAuthenticationModule(); - return (mockAuthenticationModule.createSession(null)); + return (mockAuthenticationModule.createSession(null, null)); } } From afbba77afe3b2d53483b62d8e6c7a001626dcdc3 Mon Sep 17 00:00:00 2001 From: Tim Chamberlain Date: Tue, 19 Jul 2022 13:24:20 -0500 Subject: [PATCH 2/5] QQQ-27: fixed checkstyle violations --- .../qqq/backend/core/model/actions/AbstractActionInput.java | 2 +- .../modules/authentication/Auth0AuthenticationModule.java | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/kingsrook/qqq/backend/core/model/actions/AbstractActionInput.java b/src/main/java/com/kingsrook/qqq/backend/core/model/actions/AbstractActionInput.java index 9907c9ba..95e3aecd 100644 --- a/src/main/java/com/kingsrook/qqq/backend/core/model/actions/AbstractActionInput.java +++ b/src/main/java/com/kingsrook/qqq/backend/core/model/actions/AbstractActionInput.java @@ -24,9 +24,9 @@ package com.kingsrook.qqq.backend.core.model.actions; import com.kingsrook.qqq.backend.core.exceptions.QInstanceValidationException; import com.kingsrook.qqq.backend.core.instances.QInstanceValidator; -import com.kingsrook.qqq.backend.core.modules.authentication.metadata.QAuthenticationMetaData; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.session.QSession; +import com.kingsrook.qqq.backend.core.modules.authentication.metadata.QAuthenticationMetaData; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; diff --git a/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/Auth0AuthenticationModule.java b/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/Auth0AuthenticationModule.java index 2962fabd..9078a80b 100644 --- a/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/Auth0AuthenticationModule.java +++ b/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/Auth0AuthenticationModule.java @@ -312,9 +312,13 @@ public class Auth0AuthenticationModule implements QAuthenticationModuleInterface 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); } From 7a9a83a348b3e6d3761ddf9a62cd981cbbd59b84 Mon Sep 17 00:00:00 2001 From: Tim Chamberlain Date: Tue, 19 Jul 2022 18:17:00 -0500 Subject: [PATCH 3/5] QQQ-27: couple minor changes and updates from code review feedback --- pom.xml | 2 +- .../backend/core/actions/ActionHelper.java | 3 +- .../exceptions/QAuthenticationException.java | 2 +- .../Auth0AuthenticationModule.java | 20 ++++++---- .../Auth0AuthenticationModuleTest.java | 37 +++++++------------ 5 files changed, 31 insertions(+), 33 deletions(-) diff --git a/pom.xml b/pom.xml index b7fd2a16..2a36c546 100644 --- a/pom.xml +++ b/pom.xml @@ -56,7 +56,7 @@ com.auth0 mvc-auth-commons - [1.0, 2.0) + 1.9.2 com.fasterxml.jackson.core diff --git a/src/main/java/com/kingsrook/qqq/backend/core/actions/ActionHelper.java b/src/main/java/com/kingsrook/qqq/backend/core/actions/ActionHelper.java index ae4e8f8a..d5ab7318 100644 --- a/src/main/java/com/kingsrook/qqq/backend/core/actions/ActionHelper.java +++ b/src/main/java/com/kingsrook/qqq/backend/core/actions/ActionHelper.java @@ -22,6 +22,7 @@ package com.kingsrook.qqq.backend.core.actions; +import com.kingsrook.qqq.backend.core.exceptions.QAuthenticationException; import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput; import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModuleDispatcher; @@ -43,7 +44,7 @@ public class ActionHelper QAuthenticationModuleInterface authenticationModule = qAuthenticationModuleDispatcher.getQModule(request.getAuthenticationMetaData()); if(!authenticationModule.isSessionValid(request.getSession())) { - throw new QException("Invalid session in request"); + throw new QAuthenticationException("Invalid session in request"); } } diff --git a/src/main/java/com/kingsrook/qqq/backend/core/exceptions/QAuthenticationException.java b/src/main/java/com/kingsrook/qqq/backend/core/exceptions/QAuthenticationException.java index 58478273..c2237629 100644 --- a/src/main/java/com/kingsrook/qqq/backend/core/exceptions/QAuthenticationException.java +++ b/src/main/java/com/kingsrook/qqq/backend/core/exceptions/QAuthenticationException.java @@ -23,7 +23,7 @@ package com.kingsrook.qqq.backend.core.exceptions; /******************************************************************************* - * Exception thrown while doing module-dispatch + * Exception thrown doing authentication * *******************************************************************************/ public class QAuthenticationException extends QException diff --git a/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/Auth0AuthenticationModule.java b/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/Auth0AuthenticationModule.java index 9078a80b..db644312 100644 --- a/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/Auth0AuthenticationModule.java +++ b/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/Auth0AuthenticationModule.java @@ -27,6 +27,7 @@ import java.time.Duration; import java.time.Instant; import java.util.Base64; import java.util.Map; +import java.util.Optional; import com.auth0.jwk.Jwk; import com.auth0.jwk.JwkException; import com.auth0.jwk.JwkProvider; @@ -60,7 +61,7 @@ public class Auth0AuthenticationModule implements QAuthenticationModuleInterface private static final int ID_TOKEN_VALIDATION_INTERVAL_SECONDS = 300; - public static final String AUTH0_ID_TOKEN_KEY = "qqq.idToken"; + public static final String AUTH0_ID_TOKEN_KEY = "sessionId"; public static final String TOKEN_NOT_PROVIDED_ERROR = "Id Token was not provided"; public static final String COULD_NOT_DECODE_ERROR = "Unable to decode id token"; @@ -82,9 +83,6 @@ public class Auth0AuthenticationModule implements QAuthenticationModuleInterface String idToken = context.get(AUTH0_ID_TOKEN_KEY); if(idToken == null) { - //////////////////////////////// - // could not decode the token // - //////////////////////////////// logger.warn(TOKEN_NOT_PROVIDED_ERROR); throw (new QAuthenticationException(TOKEN_NOT_PROVIDED_ERROR)); } @@ -166,9 +164,10 @@ public class Auth0AuthenticationModule implements QAuthenticationModuleInterface StateProviderInterface spi = getStateProvider(); Auth0StateKey key = new Auth0StateKey(session.getIdReference()); - if(spi.get(Instant.class, key).isPresent()) + Optional lastTimeCheckedOptional = spi.get(Instant.class, key); + if(lastTimeCheckedOptional.isPresent()) { - Instant lastTimeChecked = spi.get(Instant.class, key).get(); + Instant lastTimeChecked = lastTimeCheckedOptional.get(); /////////////////////////////////////////////////////////////////////////////////////////////////// // returns negative int if less than compared duration, 0 if equal, positive int if greater than // @@ -250,8 +249,15 @@ public class Auth0AuthenticationModule implements QAuthenticationModuleInterface JSONObject payload = new JSONObject(payloadString); QUser qUser = new QUser(); - qUser.setIdReference(payload.getString("email")); qUser.setFullName(payload.getString("name")); + if(payload.has("email")) + { + qUser.setIdReference(payload.getString("email")); + } + else + { + qUser.setIdReference(payload.getString("nickname")); + } QSession qSession = new QSession(); qSession.setIdReference(idToken); diff --git a/src/test/java/com/kingsrook/qqq/backend/core/modules/authentication/Auth0AuthenticationModuleTest.java b/src/test/java/com/kingsrook/qqq/backend/core/modules/authentication/Auth0AuthenticationModuleTest.java index 219eba62..80e10109 100644 --- a/src/test/java/com/kingsrook/qqq/backend/core/modules/authentication/Auth0AuthenticationModuleTest.java +++ b/src/test/java/com/kingsrook/qqq/backend/core/modules/authentication/Auth0AuthenticationModuleTest.java @@ -37,7 +37,8 @@ import static com.kingsrook.qqq.backend.core.modules.authentication.Auth0Authent import static com.kingsrook.qqq.backend.core.modules.authentication.Auth0AuthenticationModule.EXPIRED_TOKEN_ERROR; import static com.kingsrook.qqq.backend.core.modules.authentication.Auth0AuthenticationModule.INVALID_TOKEN_ERROR; import static com.kingsrook.qqq.backend.core.modules.authentication.Auth0AuthenticationModule.TOKEN_NOT_PROVIDED_ERROR; -import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.fail; @@ -73,8 +74,8 @@ public class Auth0AuthenticationModuleTest Auth0AuthenticationModule auth0AuthenticationModule = new Auth0AuthenticationModule(); auth0AuthenticationModule.setNow(now); QSession session = auth0AuthenticationModule.createSession(getQInstance(), context); - assertTrue(session.getUser().getIdReference().equals("tim.chamberlain@kingsrook.com")); - assertTrue(session.getUser().getFullName().equals("Tim Chamberlain")); + assertEquals("tim.chamberlain@kingsrook.com", session.getUser().getIdReference(), "Id should be Tim's email."); + assertEquals("Tim Chamberlain", session.getUser().getFullName(), "Full name should be Tim's full name (well without the middle name)."); } @@ -93,14 +94,12 @@ public class Auth0AuthenticationModuleTest { Auth0AuthenticationModule auth0AuthenticationModule = new Auth0AuthenticationModule(); auth0AuthenticationModule.createSession(getQInstance(), context); + fail("Should never get here"); } catch(QAuthenticationException qae) { - assertTrue(qae.getMessage().contains(INVALID_TOKEN_ERROR)); - return; + assertThat(qae.getMessage()).contains(INVALID_TOKEN_ERROR); } - - fail("Should never get here"); } @@ -119,14 +118,12 @@ public class Auth0AuthenticationModuleTest { Auth0AuthenticationModule auth0AuthenticationModule = new Auth0AuthenticationModule(); auth0AuthenticationModule.createSession(getQInstance(), context); + fail("Should never get here"); } catch(QAuthenticationException qae) { - assertTrue(qae.getMessage().contains(COULD_NOT_DECODE_ERROR)); - return; + assertThat(qae.getMessage()).contains(COULD_NOT_DECODE_ERROR); } - - fail("Should never get here"); } @@ -145,14 +142,12 @@ public class Auth0AuthenticationModuleTest { Auth0AuthenticationModule auth0AuthenticationModule = new Auth0AuthenticationModule(); auth0AuthenticationModule.createSession(getQInstance(), context); + fail("Should never get here"); } catch(QAuthenticationException qae) { - assertTrue(qae.getMessage().contains(EXPIRED_TOKEN_ERROR)); - return; + assertThat(qae.getMessage()).contains(EXPIRED_TOKEN_ERROR); } - - fail("Should never get here"); } @@ -168,14 +163,12 @@ public class Auth0AuthenticationModuleTest { Auth0AuthenticationModule auth0AuthenticationModule = new Auth0AuthenticationModule(); auth0AuthenticationModule.createSession(getQInstance(), new HashMap<>()); + fail("Should never get here"); } catch(QAuthenticationException qae) { - assertTrue(qae.getMessage().contains(TOKEN_NOT_PROVIDED_ERROR)); - return; + assertThat(qae.getMessage()).contains(TOKEN_NOT_PROVIDED_ERROR); } - - fail("Should never get here"); } @@ -194,14 +187,12 @@ public class Auth0AuthenticationModuleTest { Auth0AuthenticationModule auth0AuthenticationModule = new Auth0AuthenticationModule(); auth0AuthenticationModule.createSession(getQInstance(), context); + fail("Should never get here"); } catch(QAuthenticationException qae) { - assertTrue(qae.getMessage().contains(TOKEN_NOT_PROVIDED_ERROR)); - return; + assertThat(qae.getMessage()).contains(TOKEN_NOT_PROVIDED_ERROR); } - - fail("Should never get here"); } From 960b316d993b66625b48a2cb32f440f3da817b33 Mon Sep 17 00:00:00 2001 From: Tim Chamberlain Date: Thu, 21 Jul 2022 11:24:08 -0500 Subject: [PATCH 4/5] QQQ-27: fixed fragile test --- .../Auth0AuthenticationModule.java | 32 ++----------------- .../Auth0AuthenticationModuleTest.java | 27 ++++++++++------ 2 files changed, 21 insertions(+), 38 deletions(-) diff --git a/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/Auth0AuthenticationModule.java b/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/Auth0AuthenticationModule.java index db644312..0bf8d5f8 100644 --- a/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/Auth0AuthenticationModule.java +++ b/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/Auth0AuthenticationModule.java @@ -113,7 +113,7 @@ public class Auth0AuthenticationModule implements QAuthenticationModuleInterface /////////////////////////////////////////////////////////////////// StateProviderInterface spi = getStateProvider(); Auth0StateKey key = new Auth0StateKey(qSession.getIdReference()); - spi.put(key, getNow()); + spi.put(key, Instant.now()); return (qSession); } @@ -174,7 +174,7 @@ public class Auth0AuthenticationModule implements QAuthenticationModuleInterface // - so this is basically saying, if the time between the last time we checked the token and // // right now is more than ID_TOKEN_VALIDATION_INTERVAL_SECTIONS, then session needs revalidated // /////////////////////////////////////////////////////////////////////////////////////////////////// - return (Duration.between(lastTimeChecked, getNow()).compareTo(Duration.ofSeconds(ID_TOKEN_VALIDATION_INTERVAL_SECONDS)) < 0); + return (Duration.between(lastTimeChecked, Instant.now()).compareTo(Duration.ofSeconds(ID_TOKEN_VALIDATION_INTERVAL_SECONDS)) < 0); } return (false); @@ -182,32 +182,6 @@ public class Auth0AuthenticationModule implements QAuthenticationModuleInterface - /******************************************************************************* - ** public method so that 'now' can be used for testing purposes - ** - defaults to real 'now' - *******************************************************************************/ - public Instant getNow() - { - if(now == null) - { - now = Instant.now(); - } - - return (now); - } - - - - /******************************************************************************* - ** public method so that 'now' can be set for testing purposes - *******************************************************************************/ - public void setNow(Instant now) - { - this.now = now; - } - - - /******************************************************************************* ** makes request to check if a token is still valid and build new qSession if it is ** @@ -283,7 +257,7 @@ public class Auth0AuthenticationModule implements QAuthenticationModuleInterface /******************************************************************************* ** *******************************************************************************/ - private static class Auth0StateKey extends AbstractStateKey + public static class Auth0StateKey extends AbstractStateKey { private final String key; diff --git a/src/test/java/com/kingsrook/qqq/backend/core/modules/authentication/Auth0AuthenticationModuleTest.java b/src/test/java/com/kingsrook/qqq/backend/core/modules/authentication/Auth0AuthenticationModuleTest.java index 80e10109..99cbb6b6 100644 --- a/src/test/java/com/kingsrook/qqq/backend/core/modules/authentication/Auth0AuthenticationModuleTest.java +++ b/src/test/java/com/kingsrook/qqq/backend/core/modules/authentication/Auth0AuthenticationModuleTest.java @@ -30,6 +30,8 @@ import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.session.QSession; import com.kingsrook.qqq.backend.core.modules.authentication.metadata.Auth0AuthenticationMetaData; import com.kingsrook.qqq.backend.core.modules.authentication.metadata.QAuthenticationMetaData; +import com.kingsrook.qqq.backend.core.state.InMemoryStateProvider; +import com.kingsrook.qqq.backend.core.state.StateProviderInterface; import com.kingsrook.qqq.backend.core.utils.TestUtils; import org.junit.jupiter.api.Test; import static com.kingsrook.qqq.backend.core.modules.authentication.Auth0AuthenticationModule.AUTH0_ID_TOKEN_KEY; @@ -61,21 +63,28 @@ public class Auth0AuthenticationModuleTest ** *******************************************************************************/ @Test - public void testValidToken() throws QAuthenticationException + public void testLastTimeChecked() throws QAuthenticationException { - Map context = new HashMap<>(); - context.put(AUTH0_ID_TOKEN_KEY, VALID_TOKEN); - ////////////////////////////////////////////////////////// // Tuesday, July 19, 2022 12:40:27.299 PM GMT-05:00 DST // ////////////////////////////////////////////////////////// - Instant now = Instant.ofEpochMilli(1658252427299L); + Instant now = Instant.now(); + + ///////////////////////////////////////////////////////// + // put the 'now' from the past into the state provider // + ///////////////////////////////////////////////////////// + StateProviderInterface spi = InMemoryStateProvider.getInstance(); + Auth0AuthenticationModule.Auth0StateKey key = new Auth0AuthenticationModule.Auth0StateKey(VALID_TOKEN); + spi.put(key, now); + + ////////////////////// + // build up session // + ////////////////////// + QSession session = new QSession(); + session.setIdReference(VALID_TOKEN); Auth0AuthenticationModule auth0AuthenticationModule = new Auth0AuthenticationModule(); - auth0AuthenticationModule.setNow(now); - QSession session = auth0AuthenticationModule.createSession(getQInstance(), context); - assertEquals("tim.chamberlain@kingsrook.com", session.getUser().getIdReference(), "Id should be Tim's email."); - assertEquals("Tim Chamberlain", session.getUser().getFullName(), "Full name should be Tim's full name (well without the middle name)."); + assertEquals(true, auth0AuthenticationModule.isSessionValid(session), "Session should return as still valid."); } From c7b5ae103ac1beafa0ca6a67c0fcee9ab509054c Mon Sep 17 00:00:00 2001 From: Tim Chamberlain Date: Thu, 21 Jul 2022 11:45:22 -0500 Subject: [PATCH 5/5] QQQ-27: upped qqq-backend-core version --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 2a36c546..5a029945 100644 --- a/pom.xml +++ b/pom.xml @@ -25,7 +25,7 @@ com.kingsrook.qqq qqq-backend-core - 0.2.0-SNAPSHOT + 0.2.0-20220721.162748-8 scm:git:git@github.com:Kingsrook/qqq-backend-core.git