From f49be5ff63996c853056e4e92126c8611735700c Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Tue, 4 Mar 2025 10:51:17 -0600 Subject: [PATCH 1/4] Switch accessToken check from != null to StringUtils.hasContent --- .../implementations/Auth0AuthenticationModule.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 8b871ac1..25923209 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 @@ -1020,7 +1020,7 @@ public class Auth0AuthenticationModule implements QAuthenticationModuleInterface // decode the accessToken and make sure it is not expired // //////////////////////////////////////////////////////////// boolean needNewToken = true; - if(accessToken != null) + if(StringUtils.hasContent(accessToken)) { DecodedJWT jwt = JWT.decode(accessToken); String payload = jwt.getPayload(); From 23e87cd9ce18e60f696c1b43fed412d9825cce40 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Fri, 7 Mar 2025 20:36:20 -0600 Subject: [PATCH 2/4] Initial implementation of 0Auth2 authentication module --- .../model/metadata/QAuthenticationType.java | 1 + .../OAuth2AuthenticationMetaData.java | 192 ++++++++++ .../qqq/backend/core/model/session/QUser.java | 5 +- .../QAuthenticationModuleInterface.java | 12 - .../FullyAnonymousAuthenticationModule.java | 13 - .../OAuth2AuthenticationModule.java | 340 ++++++++++++++++++ .../javalin/QJavalinImplementation.java | 15 +- .../metadata/OAuth2MetaDataProvider.java | 63 ++++ .../metadata/SampleMetaDataProvider.java | 21 +- 9 files changed, 633 insertions(+), 29 deletions(-) create mode 100644 qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/authentication/OAuth2AuthenticationMetaData.java create mode 100644 qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/OAuth2AuthenticationModule.java create mode 100644 qqq-sample-project/src/main/java/com/kingsrook/sampleapp/metadata/OAuth2MetaDataProvider.java diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QAuthenticationType.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QAuthenticationType.java index 9ae00fbd..e8ecedf8 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QAuthenticationType.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QAuthenticationType.java @@ -28,6 +28,7 @@ package com.kingsrook.qqq.backend.core.model.metadata; *******************************************************************************/ public enum QAuthenticationType { + OAUTH2("OAuth2"), AUTH_0("auth0"), TABLE_BASED("tableBased"), FULLY_ANONYMOUS("fullyAnonymous"), diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/authentication/OAuth2AuthenticationMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/authentication/OAuth2AuthenticationMetaData.java new file mode 100644 index 00000000..0c5f585c --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/authentication/OAuth2AuthenticationMetaData.java @@ -0,0 +1,192 @@ +/* + * 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.authentication; + + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.kingsrook.qqq.backend.core.model.metadata.QAuthenticationType; +import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModuleDispatcher; +import com.kingsrook.qqq.backend.core.modules.authentication.implementations.OAuth2AuthenticationModule; + + +/******************************************************************************* + ** Meta-data to provide details of an OAuth2 Authentication module + *******************************************************************************/ +public class OAuth2AuthenticationMetaData extends QAuthenticationMetaData +{ + private String baseUrl; + private String tokenUrl; + private String clientId; + + //////////////////////////////////////////////////////////////////////////////////////// + // keep this secret, on the server - don't let it be serialized and sent to a client! // + //////////////////////////////////////////////////////////////////////////////////////// + @JsonIgnore + private String clientSecret; + + + + /******************************************************************************* + ** Default Constructor. + *******************************************************************************/ + public OAuth2AuthenticationMetaData() + { + super(); + setType(QAuthenticationType.OAUTH2); + + ////////////////////////////////////////////////////////// + // ensure this module is registered with the dispatcher // + ////////////////////////////////////////////////////////// + QAuthenticationModuleDispatcher.registerModule(QAuthenticationType.OAUTH2.getName(), OAuth2AuthenticationModule.class.getName()); + } + + + + /******************************************************************************* + ** Fluent setter, override to help fluent flows + *******************************************************************************/ + public OAuth2AuthenticationMetaData 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; + } + + + + /******************************************************************************* + ** Fluent setter, override to help fluent flows + *******************************************************************************/ + public OAuth2AuthenticationMetaData withClientId(String clientId) + { + setClientId(clientId); + return this; + } + + + + /******************************************************************************* + ** Getter for clientId + ** + *******************************************************************************/ + public String getClientId() + { + return clientId; + } + + + + /******************************************************************************* + ** Setter for clientId + ** + *******************************************************************************/ + public void setClientId(String clientId) + { + this.clientId = clientId; + } + + + + /******************************************************************************* + ** Fluent setter, override to help fluent flows + *******************************************************************************/ + public OAuth2AuthenticationMetaData withClientSecret(String clientSecret) + { + setClientSecret(clientSecret); + return this; + } + + + + /******************************************************************************* + ** Getter for clientSecret + ** + *******************************************************************************/ + public String getClientSecret() + { + return clientSecret; + } + + + + /******************************************************************************* + ** Setter for clientSecret + ** + *******************************************************************************/ + public void setClientSecret(String clientSecret) + { + this.clientSecret = clientSecret; + } + + + + /******************************************************************************* + ** Getter for tokenUrl + *******************************************************************************/ + public String getTokenUrl() + { + return (this.tokenUrl); + } + + + + /******************************************************************************* + ** Setter for tokenUrl + *******************************************************************************/ + public void setTokenUrl(String tokenUrl) + { + this.tokenUrl = tokenUrl; + } + + + + /******************************************************************************* + ** Fluent setter for tokenUrl + *******************************************************************************/ + public OAuth2AuthenticationMetaData withTokenUrl(String tokenUrl) + { + this.tokenUrl = tokenUrl; + return (this); + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/session/QUser.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/session/QUser.java index 1adcc3a4..787da174 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/session/QUser.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/session/QUser.java @@ -22,10 +22,13 @@ package com.kingsrook.qqq.backend.core.model.session; +import java.io.Serializable; + + /******************************************************************************* ** *******************************************************************************/ -public class QUser implements Cloneable +public class QUser implements Cloneable, Serializable { private String idReference; private String fullName; diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/QAuthenticationModuleInterface.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/QAuthenticationModuleInterface.java index 28db59d6..b296d348 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/QAuthenticationModuleInterface.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/QAuthenticationModuleInterface.java @@ -25,13 +25,10 @@ package com.kingsrook.qqq.backend.core.modules.authentication; import java.io.Serializable; import java.util.Map; import com.kingsrook.qqq.backend.core.context.QContext; -import com.kingsrook.qqq.backend.core.exceptions.AccessTokenException; import com.kingsrook.qqq.backend.core.exceptions.QAuthenticationException; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; -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.utils.ValueUtils; -import org.apache.commons.lang.NotImplementedException; /******************************************************************************* @@ -81,13 +78,4 @@ public interface QAuthenticationModuleInterface return (false); } - - /******************************************************************************* - ** - *******************************************************************************/ - default String createAccessToken(QAuthenticationMetaData metaData, String clientId, String clientSecret) throws AccessTokenException - { - throw (new NotImplementedException("The method createAccessToken() is not implemented in the class: " + this.getClass().getSimpleName())); - } - } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/FullyAnonymousAuthenticationModule.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/FullyAnonymousAuthenticationModule.java index 457f6bbb..519b609f 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/FullyAnonymousAuthenticationModule.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/FullyAnonymousAuthenticationModule.java @@ -24,9 +24,7 @@ package com.kingsrook.qqq.backend.core.modules.authentication.implementations; import java.util.Map; import java.util.UUID; -import com.kingsrook.qqq.backend.core.exceptions.AccessTokenException; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; -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.model.session.QUser; import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModuleInterface; @@ -77,15 +75,4 @@ public class FullyAnonymousAuthenticationModule implements QAuthenticationModule return session != null; } - - - /******************************************************************************* - ** Load an instance of the appropriate state provider - ** - *******************************************************************************/ - public String createAccessToken(QAuthenticationMetaData metaData, String clientId, String clientSecret) throws AccessTokenException - { - return (TEST_ACCESS_TOKEN); - } - } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/OAuth2AuthenticationModule.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/OAuth2AuthenticationModule.java new file mode 100644 index 00000000..893a47ef --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/OAuth2AuthenticationModule.java @@ -0,0 +1,340 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2025. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.modules.authentication.implementations; + + +import java.io.Serializable; +import java.net.URI; +import java.time.Duration; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Base64; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import com.auth0.jwt.JWT; +import com.auth0.jwt.interfaces.DecodedJWT; +import com.kingsrook.qqq.backend.core.actions.tables.GetAction; +import com.kingsrook.qqq.backend.core.actions.tables.InsertAction; +import com.kingsrook.qqq.backend.core.context.CapturedContext; +import com.kingsrook.qqq.backend.core.context.QContext; +import com.kingsrook.qqq.backend.core.exceptions.QAuthenticationException; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.logging.QLogger; +import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput; +import com.kingsrook.qqq.backend.core.model.data.QRecord; +import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import com.kingsrook.qqq.backend.core.model.metadata.authentication.OAuth2AuthenticationMetaData; +import com.kingsrook.qqq.backend.core.model.session.QSession; +import com.kingsrook.qqq.backend.core.model.session.QSystemUserSession; +import com.kingsrook.qqq.backend.core.model.session.QUser; +import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModuleInterface; +import com.kingsrook.qqq.backend.core.modules.authentication.implementations.model.UserSession; +import com.kingsrook.qqq.backend.core.utils.memoization.Memoization; +import com.nimbusds.oauth2.sdk.AuthorizationCode; +import com.nimbusds.oauth2.sdk.AuthorizationCodeGrant; +import com.nimbusds.oauth2.sdk.AuthorizationGrant; +import com.nimbusds.oauth2.sdk.ErrorObject; +import com.nimbusds.oauth2.sdk.Scope; +import com.nimbusds.oauth2.sdk.TokenRequest; +import com.nimbusds.oauth2.sdk.TokenResponse; +import com.nimbusds.oauth2.sdk.auth.ClientAuthentication; +import com.nimbusds.oauth2.sdk.auth.ClientSecretBasic; +import com.nimbusds.oauth2.sdk.auth.Secret; +import com.nimbusds.oauth2.sdk.id.ClientID; +import com.nimbusds.oauth2.sdk.pkce.CodeVerifier; +import com.nimbusds.oauth2.sdk.token.AccessToken; +import org.json.JSONObject; +import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair; + + +/******************************************************************************* + ** Implementation of OAuth2 authentication. + *******************************************************************************/ +public class OAuth2AuthenticationModule implements QAuthenticationModuleInterface +{ + private static final QLogger LOG = QLogger.getLogger(OAuth2AuthenticationModule.class); + + private static boolean mayMemoize = true; + + private static final Memoization getAccessTokenFromSessionUUIDMemoization = new Memoization() + .withTimeout(Duration.of(1, ChronoUnit.MINUTES)) + .withMaxSize(1000); + + + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public QSession createSession(QInstance qInstance, Map context) throws QAuthenticationException + { + try + { + OAuth2AuthenticationMetaData oauth2MetaData = (OAuth2AuthenticationMetaData) qInstance.getAuthentication(); + + if(context.containsKey("code") && context.containsKey("redirectUri") && context.containsKey("codeVerifier")) + { + AuthorizationCode code = new AuthorizationCode(context.get("code")); + URI callback = new URI(context.get("redirectUri")); + CodeVerifier codeVerifier = new CodeVerifier(context.get("codeVerifier")); + AuthorizationGrant codeGrant = new AuthorizationCodeGrant(code, callback, codeVerifier); + + ClientID clientID = new ClientID(oauth2MetaData.getClientId()); + Secret clientSecret = new Secret(oauth2MetaData.getClientSecret()); + ClientAuthentication clientAuth = new ClientSecretBasic(clientID, clientSecret); + + URI tokenEndpoint = new URI(oauth2MetaData.getTokenUrl()); + Scope scope = new Scope("openid profile email offline_access"); + TokenRequest tokenRequest = new TokenRequest(tokenEndpoint, clientAuth, codeGrant, scope); + + TokenResponse tokenResponse = TokenResponse.parse(tokenRequest.toHTTPRequest().send()); + + if(tokenResponse.indicatesSuccess()) + { + AccessToken accessToken = tokenResponse.toSuccessResponse().getTokens().getAccessToken(); + // todo - what?? RefreshToken refreshToken = tokenResponse.toSuccessResponse().getTokens().getRefreshToken(); + + QSession session = createSessionFromToken(accessToken.getValue()); + insertUserSession(accessToken.getValue(), session); + return (session); + } + else + { + ErrorObject errorObject = tokenResponse.toErrorResponse().getErrorObject(); + LOG.info("Token request failed", logPair("code", errorObject.getCode()), logPair("description", errorObject.getDescription())); + throw (new QAuthenticationException(errorObject.getDescription())); + } + } + else if(context.containsKey("sessionUUID") || context.containsKey("uuid")) + { + String uuid = Objects.requireNonNullElseGet(context.get("sessionUUID"), () -> context.get("uuid")); + String accessToken = getAccessTokenFromSessionUUID(uuid); + QSession session = createSessionFromToken(accessToken); + session.setUuid(uuid); + // todo - validate its age or against provider?? + return (session); + } + else + { + String message = "Did not receive recognized values in context for creating session"; + LOG.warn(message, logPair("contextKeys", context.keySet())); + throw (new QAuthenticationException(message)); + } + } + catch(QAuthenticationException qae) + { + throw (qae); + } + catch(Exception e) + { + throw (new QAuthenticationException("Failed to create session (token)", e)); + } + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public boolean isSessionValid(QInstance instance, QSession session) + { + if(session instanceof QSystemUserSession) + { + return (true); + } + + try + { + String accessToken = getAccessTokenFromSessionUUID(session.getUuid()); + DecodedJWT jwt = JWT.decode(accessToken); + if(jwt.getExpiresAtAsInstant().isBefore(Instant.now())) + { + LOG.debug("accessToken is expired", logPair("sessionUUID", session.getUuid())); + return (false); + } + + return true; + } + catch(QAuthenticationException e) + { + return (false); + } + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + private QSession createSessionFromToken(String accessToken) throws QException + { + DecodedJWT jwt = JWT.decode(accessToken); + Base64.Decoder decoder = Base64.getUrlDecoder(); + String payloadString = new String(decoder.decode(jwt.getPayload())); + JSONObject payload = new JSONObject(payloadString); + + QSession session = new QSession(); + QUser user = new QUser(); + session.setUser(user); + + user.setFullName("Unknown"); + String email = Objects.requireNonNullElseGet(payload.optString("email", null), () -> payload.optString("sub", null)); + String name = payload.optString("name", email); + + user.setIdReference(email); + user.setFullName(name); + + //////////////////////////////////////////////////////////// + // todo - this needs to be much better standardized w/ fe // + //////////////////////////////////////////////////////////// + session.withValueForFrontend("user", new HashMap<>(Map.of("name", name, "email", email))); + + return session; + } + + + + /******************************************************************************* + ** Insert a session as a new record into userSession table + *******************************************************************************/ + private void insertUserSession(String accessToken, QSession qSession) throws QException + { + CapturedContext capturedContext = QContext.capture(); + try + { + QContext.init(capturedContext.qInstance(), new QSystemUserSession()); + + UserSession userSession = new UserSession() + .withUuid(qSession.getUuid()) + .withUserId(qSession.getUser().getIdReference()) + .withAccessToken(accessToken); + + new InsertAction().execute(new InsertInput(UserSession.TABLE_NAME).withRecordEntity(userSession)); + } + finally + { + QContext.init(capturedContext); + } + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public QSession createAutomatedSessionForUser(QInstance qInstance, Serializable userId) throws QAuthenticationException + { + return QAuthenticationModuleInterface.super.createAutomatedSessionForUser(qInstance, userId); + } + + + + /******************************************************************************* + ** Look up access_token from session UUID + ** + *******************************************************************************/ + private String getAccessTokenFromSessionUUID(String sessionUUID) throws QAuthenticationException + { + if(mayMemoize) + { + return getAccessTokenFromSessionUUIDMemoization.getResultThrowing(sessionUUID, (String x) -> + doGetAccessTokenFromSessionUUID(sessionUUID) + ).orElse(null); + } + else + { + return (doGetAccessTokenFromSessionUUID(sessionUUID)); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private String doGetAccessTokenFromSessionUUID(String sessionUUID) throws QAuthenticationException + { + String accessToken = null; + QSession beforeSession = QContext.getQSession(); + + try + { + QContext.setQSession(new QSystemUserSession()); + + /////////////////////////////////////// + // query for the user session record // + /////////////////////////////////////// + QRecord userSessionRecord = new GetAction().executeForRecord(new GetInput(UserSession.TABLE_NAME) + .withUniqueKey(Map.of("uuid", sessionUUID)) + .withShouldMaskPasswords(false) + .withShouldOmitHiddenFields(false)); + + if(userSessionRecord != null) + { + accessToken = userSessionRecord.getValueString("accessToken"); + + //////////////////////////////////////////////////////////// + // decode the accessToken and make sure it is not expired // + //////////////////////////////////////////////////////////// + if(accessToken != null) + { + DecodedJWT jwt = JWT.decode(accessToken); + if(jwt.getExpiresAtAsInstant().isBefore(Instant.now())) + { + throw (new QAuthenticationException("accessToken is expired")); + } + } + } + } + catch(QAuthenticationException qae) + { + throw (qae); + } + catch(Exception e) + { + LOG.warn("Error looking up userSession by sessionUUID", e); + throw (new QAuthenticationException("Error looking up userSession by sessionUUID", e)); + } + finally + { + QContext.setQSession(beforeSession); + } + + return (accessToken); + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public boolean usesSessionIdCookie() + { + return (false); + } + +} diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java index 80f227d3..349e4d49 100644 --- a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java @@ -453,10 +453,21 @@ public class QJavalinImplementation QAuthenticationModuleInterface authenticationModule = qAuthenticationModuleDispatcher.getQModule(qInstance.getAuthentication()); Map authContext = new HashMap<>(); - //? authContext.put("uuid", ValueUtils.getValueAsString(map.get("uuid"))); - authContext.put(Auth0AuthenticationModule.ACCESS_TOKEN_KEY, ValueUtils.getValueAsString(map.get("accessToken"))); authContext.put(Auth0AuthenticationModule.DO_STORE_USER_SESSION_KEY, "true"); + ////////////////////////////////////////////////////////////////////////////////////////////////////////// + // before this code iterated the map, it had zombied uuid line, and only actually used ACCESS_TOKEN_KEY // + ////////////////////////////////////////////////////////////////////////////////////////////////////////// + //? authContext.put("uuid", ValueUtils.getValueAsString(map.get("uuid"))); + // authContext.put(Auth0AuthenticationModule.ACCESS_TOKEN_KEY, ValueUtils.getValueAsString(map.get("accessToken"))); + ///////////////////////////////////////////////////////////////// + // todo - have the auth module declare what values it expects? // + ///////////////////////////////////////////////////////////////// + for(Map.Entry entry : map.entrySet()) + { + authContext.put(ValueUtils.getValueAsString(entry.getKey()), ValueUtils.getValueAsString(entry.getValue())); + } + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // put the qInstance into context - but no session yet (since, the whole point of this call is to manage the session!) // ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/metadata/OAuth2MetaDataProvider.java b/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/metadata/OAuth2MetaDataProvider.java new file mode 100644 index 00000000..ce6310dd --- /dev/null +++ b/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/metadata/OAuth2MetaDataProvider.java @@ -0,0 +1,63 @@ +/* + * 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.sampleapp.metadata; + + +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.instances.QMetaDataVariableInterpreter; +import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerInterface; +import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import com.kingsrook.qqq.backend.core.model.metadata.authentication.OAuth2AuthenticationMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.authentication.QAuthenticationMetaData; + + +/******************************************************************************* + ** Provides all OAuth2 authentication related metadata to the QQQ engine + * + *******************************************************************************/ +public class OAuth2MetaDataProvider implements MetaDataProducerInterface +{ + public static final String NAME = "OAuth2"; + + + + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public QAuthenticationMetaData produce(QInstance qInstance) throws QException + { + QMetaDataVariableInterpreter qMetaDataVariableInterpreter = new QMetaDataVariableInterpreter(); + + String oauth2BaseUrl = qMetaDataVariableInterpreter.interpret("${env.OAUTH2_BASE_URL}"); + String oauth2TokenUrl = qMetaDataVariableInterpreter.interpret("${env.OAUTH2_TOKEN_URL}"); + String oauth2ClientId = qMetaDataVariableInterpreter.interpret("${env.OAUTH2_CLIENT_ID}"); + String oauth2ClientSecret = qMetaDataVariableInterpreter.interpret("${env.OAUTH2_CLIENT_SECRET}"); + + return (new OAuth2AuthenticationMetaData() + .withBaseUrl(oauth2BaseUrl) + .withTokenUrl(oauth2TokenUrl) + .withClientId(oauth2ClientId) + .withClientSecret(oauth2ClientSecret) + .withName(NAME)); + } +} diff --git a/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/metadata/SampleMetaDataProvider.java b/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/metadata/SampleMetaDataProvider.java index bc40d1a1..c3194006 100644 --- a/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/metadata/SampleMetaDataProvider.java +++ b/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/metadata/SampleMetaDataProvider.java @@ -41,6 +41,7 @@ import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInpu import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput; import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerHelper; import com.kingsrook.qqq.backend.core.model.metadata.QAuthenticationType; +import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.metadata.authentication.QAuthenticationMetaData; import com.kingsrook.qqq.backend.core.model.metadata.branding.QBrandingMetaData; @@ -71,6 +72,8 @@ import com.kingsrook.qqq.backend.core.model.metadata.tables.Association; import com.kingsrook.qqq.backend.core.model.metadata.tables.QFieldSection; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import com.kingsrook.qqq.backend.core.model.metadata.tables.Tier; +import com.kingsrook.qqq.backend.core.modules.authentication.implementations.metadata.UserSessionMetaDataProducer; +import com.kingsrook.qqq.backend.core.modules.backend.implementations.memory.MemoryBackendModule; import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.ExtractViaQueryStep; import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.LoadViaInsertStep; import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.StreamedETLWithFrontendProcess; @@ -97,6 +100,7 @@ public class SampleMetaDataProvider extends AbstractQQQApplication public static final String RDBMS_BACKEND_NAME = "rdbms"; public static final String FILESYSTEM_BACKEND_NAME = "filesystem"; + public static final String MEMORY_BACKEND_NAME = "memory"; public static final String APP_NAME_GREETINGS = "greetingsApp"; public static final String APP_NAME_PEOPLE = "peopleApp"; @@ -140,8 +144,9 @@ public class SampleMetaDataProvider extends AbstractQQQApplication { QInstance qInstance = new QInstance(); - qInstance.setAuthentication(defineAuthentication()); + // qInstance.setAuthentication(defineAuthentication()); qInstance.addBackend(defineRdbmsBackend()); + qInstance.addBackend(defineMemoryBackend()); qInstance.addBackend(defineFilesystemBackend()); qInstance.addTable(defineTableCarrier()); qInstance.addTable(defineTablePerson()); @@ -157,6 +162,8 @@ public class SampleMetaDataProvider extends AbstractQQQApplication qInstance.addProcess(defineProcessScreenThenSleep()); qInstance.addProcess(defineProcessSimpleThrow()); + qInstance.addTable(new UserSessionMetaDataProducer(MEMORY_BACKEND_NAME).produce(qInstance)); + MetaDataProducerHelper.processAllMetaDataProducersInPackage(qInstance, SampleMetaDataProvider.class.getPackageName()); defineWidgets(qInstance); @@ -168,6 +175,18 @@ public class SampleMetaDataProvider extends AbstractQQQApplication + /*************************************************************************** + ** + ***************************************************************************/ + private static QBackendMetaData defineMemoryBackend() + { + return new QBackendMetaData() + .withName(MEMORY_BACKEND_NAME) + .withBackendType(MemoryBackendModule.class); + } + + + /******************************************************************************* ** *******************************************************************************/ From 2c976e59f486f9f449bf25af64181cb95ce47da0 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Sat, 8 Mar 2025 20:02:00 -0600 Subject: [PATCH 3/4] Add oauth2-oidc-sdk; update auth0, jwks-rsa, and dotenv-java deps (for securtiy warnings) --- qqq-backend-core/pom.xml | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/qqq-backend-core/pom.xml b/qqq-backend-core/pom.xml index 21723b83..9d0c44e7 100644 --- a/qqq-backend-core/pom.xml +++ b/qqq-backend-core/pom.xml @@ -125,10 +125,16 @@ 2.16.0 + + com.nimbusds + oauth2-oidc-sdk + 11.23.1 + + com.auth0 auth0 - 2.1.0 + 2.18.0 com.auth0 @@ -138,12 +144,12 @@ com.auth0 jwks-rsa - 0.22.0 + 0.22.1 io.github.cdimascio - java-dotenv - 5.2.2 + dotenv-java + 3.2.0 org.apache.velocity From a2b36a10e7c6701afd003ef06c41d6a689bb7418 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Sat, 8 Mar 2025 20:20:11 -0600 Subject: [PATCH 4/4] Switch tests (back) to use mock authentication --- .../com/kingsrook/sampleapp/SampleCli.java | 20 ++++++++++++++++++- .../metadata/SampleMetaDataProvider.java | 13 +++++++++++- .../kingsrook/sampleapp/SampleCliTest.java | 11 ++++++---- .../sampleapp/SampleMetaDataProviderTest.java | 6 +++--- .../widgets/RenderAllWidgetsTest.java | 2 +- .../ClonePeopleTransformStepTest.java | 2 +- 6 files changed, 43 insertions(+), 11 deletions(-) diff --git a/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/SampleCli.java b/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/SampleCli.java index cd828001..fd1b468b 100644 --- a/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/SampleCli.java +++ b/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/SampleCli.java @@ -50,7 +50,25 @@ public class SampleCli { try { - QInstance qInstance = SampleMetaDataProvider.defineInstance(); + QInstance qInstance = SampleMetaDataProvider.defineInstance(); + return (run(qInstance, args)); + } + catch(Exception e) + { + e.printStackTrace(); + return (-1); + } + } + + + + /*************************************************************************** + ** + ***************************************************************************/ + int run(QInstance qInstance, String[] args) + { + try + { QPicoCliImplementation qPicoCliImplementation = new QPicoCliImplementation(qInstance); return (qPicoCliImplementation.runCli("my-sample-cli", args)); diff --git a/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/metadata/SampleMetaDataProvider.java b/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/metadata/SampleMetaDataProvider.java index c3194006..2a866f00 100644 --- a/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/metadata/SampleMetaDataProvider.java +++ b/qqq-sample-project/src/main/java/com/kingsrook/sampleapp/metadata/SampleMetaDataProvider.java @@ -144,7 +144,6 @@ public class SampleMetaDataProvider extends AbstractQQQApplication { QInstance qInstance = new QInstance(); - // qInstance.setAuthentication(defineAuthentication()); qInstance.addBackend(defineRdbmsBackend()); qInstance.addBackend(defineMemoryBackend()); qInstance.addBackend(defineFilesystemBackend()); @@ -175,6 +174,18 @@ public class SampleMetaDataProvider extends AbstractQQQApplication + /*************************************************************************** + ** for tests, define the same instance as above, but use mock authentication. + ***************************************************************************/ + public static QInstance defineTestInstance() throws QException + { + QInstance qInstance = defineInstance(); + qInstance.setAuthentication(defineAuthentication()); + return qInstance; + } + + + /*************************************************************************** ** ***************************************************************************/ diff --git a/qqq-sample-project/src/test/java/com/kingsrook/sampleapp/SampleCliTest.java b/qqq-sample-project/src/test/java/com/kingsrook/sampleapp/SampleCliTest.java index e342f586..e3cd2762 100644 --- a/qqq-sample-project/src/test/java/com/kingsrook/sampleapp/SampleCliTest.java +++ b/qqq-sample-project/src/test/java/com/kingsrook/sampleapp/SampleCliTest.java @@ -24,6 +24,7 @@ package com.kingsrook.sampleapp; import com.kingsrook.qqq.backend.core.context.QContext; import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.session.QSession; import com.kingsrook.sampleapp.metadata.SampleMetaDataProvider; import org.junit.jupiter.api.Test; @@ -43,8 +44,9 @@ class SampleCliTest @Test void testExitSuccess() throws QException { - QContext.init(SampleMetaDataProvider.defineInstance(), new QSession()); - int exitCode = new SampleCli().run(new String[] { "--meta-data" }); + QInstance qInstance = SampleMetaDataProvider.defineTestInstance(); + QContext.init(qInstance, new QSession()); + int exitCode = new SampleCli().run(qInstance, new String[] { "--meta-data" }); assertEquals(0, exitCode); } @@ -56,8 +58,9 @@ class SampleCliTest @Test void testNotExitSuccess() throws QException { - QContext.init(SampleMetaDataProvider.defineInstance(), new QSession()); - int exitCode = new SampleCli().run(new String[] { "asdfasdf" }); + QInstance qInstance = SampleMetaDataProvider.defineTestInstance(); + QContext.init(qInstance, new QSession()); + int exitCode = new SampleCli().run(qInstance, new String[] { "asdfasdf" }); assertNotEquals(0, exitCode); } diff --git a/qqq-sample-project/src/test/java/com/kingsrook/sampleapp/SampleMetaDataProviderTest.java b/qqq-sample-project/src/test/java/com/kingsrook/sampleapp/SampleMetaDataProviderTest.java index e2b488ca..8f584ee9 100644 --- a/qqq-sample-project/src/test/java/com/kingsrook/sampleapp/SampleMetaDataProviderTest.java +++ b/qqq-sample-project/src/test/java/com/kingsrook/sampleapp/SampleMetaDataProviderTest.java @@ -75,7 +75,7 @@ public class SampleMetaDataProviderTest void beforeEach() throws Exception { primeTestDatabase("prime-test-database.sql"); - QContext.init(SampleMetaDataProvider.defineInstance(), new QSession()); + QContext.init(SampleMetaDataProvider.defineTestInstance(), new QSession()); } @@ -190,7 +190,7 @@ public class SampleMetaDataProviderTest @Test public void testGreetProcess() throws Exception { - QInstance qInstance = SampleMetaDataProvider.defineInstance(); + QInstance qInstance = SampleMetaDataProvider.defineTestInstance(); QTableMetaData personTable = SampleMetaDataProvider.defineTablePerson(); RunProcessInput request = new RunProcessInput(); request.setProcessName(SampleMetaDataProvider.PROCESS_NAME_GREET); @@ -216,7 +216,7 @@ public class SampleMetaDataProviderTest @Test public void testThrowProcess() throws Exception { - QInstance qInstance = SampleMetaDataProvider.defineInstance(); + QInstance qInstance = SampleMetaDataProvider.defineTestInstance(); RunProcessInput request = new RunProcessInput(); request.setProcessName(SampleMetaDataProvider.PROCESS_NAME_SIMPLE_THROW); request.addValue(SampleMetaDataProvider.ThrowerStep.FIELD_SLEEP_MILLIS, 10); diff --git a/qqq-sample-project/src/test/java/com/kingsrook/sampleapp/dashboard/widgets/RenderAllWidgetsTest.java b/qqq-sample-project/src/test/java/com/kingsrook/sampleapp/dashboard/widgets/RenderAllWidgetsTest.java index 4b9af90c..f5599a79 100644 --- a/qqq-sample-project/src/test/java/com/kingsrook/sampleapp/dashboard/widgets/RenderAllWidgetsTest.java +++ b/qqq-sample-project/src/test/java/com/kingsrook/sampleapp/dashboard/widgets/RenderAllWidgetsTest.java @@ -47,7 +47,7 @@ class RenderAllWidgetsTest @Test void test() throws QException { - QInstance qInstance = SampleMetaDataProvider.defineInstance(); + QInstance qInstance = SampleMetaDataProvider.defineTestInstance(); QContext.init(qInstance, new QSession()); //////////////////////////////////////////////////////////////// diff --git a/qqq-sample-project/src/test/java/com/kingsrook/sampleapp/processes/clonepeople/ClonePeopleTransformStepTest.java b/qqq-sample-project/src/test/java/com/kingsrook/sampleapp/processes/clonepeople/ClonePeopleTransformStepTest.java index 4de7c550..5dc4245e 100644 --- a/qqq-sample-project/src/test/java/com/kingsrook/sampleapp/processes/clonepeople/ClonePeopleTransformStepTest.java +++ b/qqq-sample-project/src/test/java/com/kingsrook/sampleapp/processes/clonepeople/ClonePeopleTransformStepTest.java @@ -82,7 +82,7 @@ class ClonePeopleTransformStepTest @Test void testProcessStep() throws QException { - QInstance qInstance = SampleMetaDataProvider.defineInstance(); + QInstance qInstance = SampleMetaDataProvider.defineTestInstance(); QContext.init(qInstance, new QSession()); QueryInput queryInput = new QueryInput();