diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidator.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidator.java index 39889fe7..b58be39b 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidator.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidator.java @@ -646,6 +646,8 @@ public class QInstanceValidator validateSimpleCodeReference("Instance Authentication meta data customizer ", authentication.getCustomizer(), QAuthenticationModuleCustomizerInterface.class); } + authentication.validate(qInstance, this); + runPlugins(QAuthenticationMetaData.class, authentication, qInstance); } } 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 index 0c5f585c..f46fd55a 100644 --- 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 @@ -23,9 +23,12 @@ package com.kingsrook.qqq.backend.core.model.metadata.authentication; import com.fasterxml.jackson.annotation.JsonIgnore; +import com.kingsrook.qqq.backend.core.instances.QInstanceValidator; import com.kingsrook.qqq.backend.core.model.metadata.QAuthenticationType; +import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModuleDispatcher; import com.kingsrook.qqq.backend.core.modules.authentication.implementations.OAuth2AuthenticationModule; +import com.kingsrook.qqq.backend.core.utils.StringUtils; /******************************************************************************* @@ -37,6 +40,9 @@ public class OAuth2AuthenticationMetaData extends QAuthenticationMetaData private String tokenUrl; private String clientId; + private String userSessionTableName; + private String redirectStateTableName; + //////////////////////////////////////////////////////////////////////////////////////// // keep this secret, on the server - don't let it be serialized and sent to a client! // //////////////////////////////////////////////////////////////////////////////////////// @@ -61,6 +67,33 @@ public class OAuth2AuthenticationMetaData extends QAuthenticationMetaData + /*************************************************************************** + ** + ***************************************************************************/ + @Override + public void validate(QInstance qInstance, QInstanceValidator qInstanceValidator) + { + super.validate(qInstance, qInstanceValidator); + + String prefix = "OAuth2AuthenticationMetaData (named '" + getName() + "'): "; + + qInstanceValidator.assertCondition(StringUtils.hasContent(baseUrl), prefix + "baseUrl must be set"); + qInstanceValidator.assertCondition(StringUtils.hasContent(clientId), prefix + "clientId must be set"); + qInstanceValidator.assertCondition(StringUtils.hasContent(clientSecret), prefix + "clientSecret must be set"); + + if(qInstanceValidator.assertCondition(StringUtils.hasContent(userSessionTableName), prefix + "userSessionTableName must be set")) + { + qInstanceValidator.assertCondition(qInstance.getTable(userSessionTableName) != null, prefix + "userSessionTableName ('" + userSessionTableName + "') was not found in the instance"); + } + + if(qInstanceValidator.assertCondition(StringUtils.hasContent(redirectStateTableName), prefix + "redirectStateTableName must be set")) + { + qInstanceValidator.assertCondition(qInstance.getTable(redirectStateTableName) != null, prefix + "redirectStateTableName ('" + redirectStateTableName + "') was not found in the instance"); + } + } + + + /******************************************************************************* ** Fluent setter, override to help fluent flows *******************************************************************************/ @@ -189,4 +222,66 @@ public class OAuth2AuthenticationMetaData extends QAuthenticationMetaData return (this); } + + + /******************************************************************************* + ** Getter for userSessionTableName + *******************************************************************************/ + public String getUserSessionTableName() + { + return (this.userSessionTableName); + } + + + + /******************************************************************************* + ** Setter for userSessionTableName + *******************************************************************************/ + public void setUserSessionTableName(String userSessionTableName) + { + this.userSessionTableName = userSessionTableName; + } + + + + /******************************************************************************* + ** Fluent setter for userSessionTableName + *******************************************************************************/ + public OAuth2AuthenticationMetaData withUserSessionTableName(String userSessionTableName) + { + this.userSessionTableName = userSessionTableName; + return (this); + } + + + + /******************************************************************************* + ** Getter for redirectStateTableName + *******************************************************************************/ + public String getRedirectStateTableName() + { + return (this.redirectStateTableName); + } + + + + /******************************************************************************* + ** Setter for redirectStateTableName + *******************************************************************************/ + public void setRedirectStateTableName(String redirectStateTableName) + { + this.redirectStateTableName = redirectStateTableName; + } + + + + /******************************************************************************* + ** Fluent setter for redirectStateTableName + *******************************************************************************/ + public OAuth2AuthenticationMetaData withRedirectStateTableName(String redirectStateTableName) + { + this.redirectStateTableName = redirectStateTableName; + return (this); + } + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/authentication/QAuthenticationMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/authentication/QAuthenticationMetaData.java index c300e7fa..33a17a3b 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/authentication/QAuthenticationMetaData.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/authentication/QAuthenticationMetaData.java @@ -25,6 +25,7 @@ package com.kingsrook.qqq.backend.core.model.metadata.authentication; import java.util.HashMap; import java.util.Map; import com.fasterxml.jackson.annotation.JsonFilter; +import com.kingsrook.qqq.backend.core.instances.QInstanceValidator; import com.kingsrook.qqq.backend.core.model.metadata.QAuthenticationType; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.metadata.TopLevelMetaDataInterface; @@ -225,4 +226,15 @@ public class QAuthenticationMetaData implements TopLevelMetaDataInterface return (this); } + + + /*************************************************************************** + ** + ***************************************************************************/ + public void validate(QInstance qInstance, QInstanceValidator qInstanceValidator) + { + ////////////////// + // noop at base // + ////////////////// + } } 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 a5dd58ff..c8c0b784 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 @@ -84,7 +84,7 @@ public interface QAuthenticationModuleInterface /*************************************************************************** ** ***************************************************************************/ - default String getLoginRedirectUrl(String originalUrl) + default String getLoginRedirectUrl(String originalUrl) throws QAuthenticationException { throw (new NotImplementedException("The method getLoginRedirectUrl() is not implemented in the authentication module: " + this.getClass().getSimpleName())); } 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 index e832ad5d..d666ebe4 100644 --- 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 @@ -22,6 +22,7 @@ package com.kingsrook.qqq.backend.core.modules.authentication.implementations; +import java.io.IOException; import java.io.Serializable; import java.net.URI; import java.net.URLEncoder; @@ -33,7 +34,7 @@ import java.util.Base64; import java.util.HashMap; import java.util.Map; import java.util.Objects; -import java.util.UUID; +import java.util.concurrent.atomic.AtomicReference; import com.auth0.jwt.JWT; import com.auth0.jwt.interfaces.DecodedJWT; import com.kingsrook.qqq.backend.core.actions.tables.GetAction; @@ -48,16 +49,20 @@ 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.metadata.tables.QTableMetaData; 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.CollectionUtils; 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.GeneralException; +import com.nimbusds.oauth2.sdk.ParseException; import com.nimbusds.oauth2.sdk.Scope; import com.nimbusds.oauth2.sdk.TokenRequest; import com.nimbusds.oauth2.sdk.TokenResponse; @@ -65,8 +70,11 @@ 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.id.Issuer; +import com.nimbusds.oauth2.sdk.id.State; import com.nimbusds.oauth2.sdk.pkce.CodeVerifier; import com.nimbusds.oauth2.sdk.token.AccessToken; +import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata; import org.json.JSONObject; import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair; @@ -84,8 +92,8 @@ public class OAuth2AuthenticationModule implements QAuthenticationModuleInterfac .withTimeout(Duration.of(1, ChronoUnit.MINUTES)) .withMaxSize(1000); - // todo wip - private static Map stateToRedirectUrl = new HashMap<>(); + private static final Memoization oidcProviderMetadataMemoization = new Memoization() + .withMayStoreNullValues(false); @@ -101,36 +109,42 @@ public class OAuth2AuthenticationModule implements QAuthenticationModuleInterfac if(context.containsKey("code") && context.containsKey("state")) { + /////////////////////////////////////////////////////////////////////// + // handle a callback to initially auth a user for a traditional // + // (non-js) site - where the code & state params come to the backend // + /////////////////////////////////////////////////////////////////////// AuthorizationCode code = new AuthorizationCode(context.get("code")); - // todo - maybe this comes from lookup of state? - URI redirectURI = new URI(stateToRedirectUrl.get(context.get("state"))); + ///////////////////////////////////////// + // verify the state in our state table // + ///////////////////////////////////////// + AtomicReference redirectUri = new AtomicReference<>(null); + QContext.withTemporaryContext(new CapturedContext(qInstance, new QSystemUserSession()), () -> + { + QRecord redirectStateRecord = GetAction.execute(oauth2MetaData.getRedirectStateTableName(), Map.of("state", context.get("state"))); + if(redirectStateRecord == null) + { + throw (new QAuthenticationException("State not found")); + } + redirectUri.set(redirectStateRecord.getValueString("redirectUri")); + }); + URI redirectURI = new URI(redirectUri.get()); ClientSecretBasic clientSecretBasic = new ClientSecretBasic(new ClientID(oauth2MetaData.getClientId()), new Secret(oauth2MetaData.getClientSecret())); AuthorizationCodeGrant codeGrant = new AuthorizationCodeGrant(code, redirectURI); - URI tokenEndpoint = new URI(oauth2MetaData.getTokenUrl()); - TokenRequest tokenRequest = new TokenRequest(tokenEndpoint, clientSecretBasic, codeGrant); - TokenResponse tokenResponse = TokenResponse.parse(tokenRequest.toHTTPRequest().send()); + URI tokenEndpoint = getOIDCProviderMetadata(oauth2MetaData).getTokenEndpointURI(); + Scope scope = new Scope("openid profile email offline_access"); + TokenRequest tokenRequest = new TokenRequest(tokenEndpoint, clientSecretBasic, codeGrant, scope); - 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())); - } + return createSessionFromTokenRequest(tokenRequest); } else if(context.containsKey("code") && context.containsKey("redirectUri") && context.containsKey("codeVerifier")) { + //////////////////////////////////////////////////////////////////////////////// + // handle a call down to this backend code to initially auth a user for an // + // SPA that received a code (where the javascript generated the codeVerifier) // + //////////////////////////////////////////////////////////////////////////////// AuthorizationCode code = new AuthorizationCode(context.get("code")); URI callback = new URI(context.get("redirectUri")); CodeVerifier codeVerifier = new CodeVerifier(context.get("codeVerifier")); @@ -140,30 +154,18 @@ public class OAuth2AuthenticationModule implements QAuthenticationModuleInterfac Secret clientSecret = new Secret(oauth2MetaData.getClientSecret()); ClientAuthentication clientAuth = new ClientSecretBasic(clientID, clientSecret); - URI tokenEndpoint = new URI(oauth2MetaData.getTokenUrl()); + URI tokenEndpoint = getOIDCProviderMetadata(oauth2MetaData).getTokenEndpointURI(); 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())); - } + return createSessionFromTokenRequest(tokenRequest); } else if(context.containsKey("sessionUUID") || context.containsKey("sessionId") || context.containsKey("uuid")) { + ////////////////////////////////////////////////////////////////////// + // handle a "normal" request, where we aren't opening a new session // + // per-se, but instead are looking for one in our userSession table // + ////////////////////////////////////////////////////////////////////// String uuid = Objects.requireNonNullElseGet(context.get("sessionUUID"), () -> Objects.requireNonNullElseGet(context.get("sessionId"), () -> context.get("uuid"))); @@ -171,7 +173,11 @@ public class OAuth2AuthenticationModule implements QAuthenticationModuleInterfac String accessToken = getAccessTokenFromSessionUUID(uuid); QSession session = createSessionFromToken(accessToken); session.setUuid(uuid); - // todo - validate its age or against provider?? + + ////////////////////////////////////////////////////////////////// + // todo - do we need to validate its age or ping the provider?? // + ////////////////////////////////////////////////////////////////// + return (session); } else @@ -193,6 +199,36 @@ public class OAuth2AuthenticationModule implements QAuthenticationModuleInterfac + /*************************************************************************** + ** + ***************************************************************************/ + private QSession createSessionFromTokenRequest(TokenRequest tokenRequest) throws ParseException, IOException, QException + { + TokenResponse tokenResponse = TokenResponse.parse(tokenRequest.toHTTPRequest().send()); + + if(tokenResponse.indicatesSuccess()) + { + AccessToken accessToken = tokenResponse.toSuccessResponse().getTokens().getAccessToken(); + + //////////////////////////////////////////////////////////////////// + // todo - do we want to try to do anything with a refresh token?? // + //////////////////////////////////////////////////////////////////// + // 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())); + } + } + + + /*************************************************************************** ** ***************************************************************************/ @@ -228,23 +264,54 @@ public class OAuth2AuthenticationModule implements QAuthenticationModuleInterfac ** ***************************************************************************/ @Override - public String getLoginRedirectUrl(String originalUrl) + public String getLoginRedirectUrl(String originalUrl) throws QAuthenticationException { - QInstance qInstance = QContext.getQInstance(); - OAuth2AuthenticationMetaData oauth2MetaData = (OAuth2AuthenticationMetaData) qInstance.getAuthentication(); + try + { + QInstance qInstance = QContext.getQInstance(); + OAuth2AuthenticationMetaData oauth2MetaData = (OAuth2AuthenticationMetaData) qInstance.getAuthentication(); + String authUrl = getOIDCProviderMetadata(oauth2MetaData).getAuthorizationEndpointURI().toString(); - // todo wip - get from meta-data or from that thing that knows the other things? - String authUrl = oauth2MetaData.getTokenUrl().replace("token", "authorize"); + QTableMetaData stateTable = QContext.getQInstance().getTable(oauth2MetaData.getRedirectStateTableName()); + if(stateTable == null) + { + throw (new QAuthenticationException("The table specified as the oauthRedirectStateTableName [" + oauth2MetaData.getRedirectStateTableName() + "] is not defined in the QInstance")); + } - String state = UUID.randomUUID().toString(); - stateToRedirectUrl.put(state, originalUrl); + /////////////////////////////////////////////////////////////////// + // generate a secure state, of either default length (32 bytes), // + // or at a size (base64 encoded) that fits in the state table // + /////////////////////////////////////////////////////////////////// + Integer stateStringLength = stateTable.getField("state").getMaxLength(); + State state = stateStringLength == null ? new State(32) : new State((stateStringLength / 4) * 3); + String stateValue = state.getValue(); - return authUrl + - "?client_id=" + URLEncoder.encode(oauth2MetaData.getClientId(), StandardCharsets.UTF_8) + - "&redirect_uri=" + URLEncoder.encode(originalUrl, StandardCharsets.UTF_8) + - "&response_type=code" + - "&scope=" + URLEncoder.encode("openid profile email", StandardCharsets.UTF_8) + - "&state=" + URLEncoder.encode(state, StandardCharsets.UTF_8); + ///////////////////////////// + // insert the state record // + ///////////////////////////// + QContext.withTemporaryContext(new CapturedContext(qInstance, new QSystemUserSession()), () -> + { + QRecord insertedState = new InsertAction().execute(new InsertInput(oauth2MetaData.getRedirectStateTableName()).withRecord(new QRecord() + .withValue("state", stateValue) + .withValue("redirectUri", originalUrl))).getRecords().get(0); + if(CollectionUtils.nullSafeHasContents(insertedState.getErrors())) + { + throw (new QAuthenticationException("Error storing redirect state: " + insertedState.getErrorsAsString())); + } + }); + + return authUrl + + "?client_id=" + URLEncoder.encode(oauth2MetaData.getClientId(), StandardCharsets.UTF_8) + + "&redirect_uri=" + URLEncoder.encode(originalUrl, StandardCharsets.UTF_8) + + "&response_type=code" + + "&scope=" + URLEncoder.encode("openid profile email", StandardCharsets.UTF_8) + + "&state=" + URLEncoder.encode(state.getValue(), StandardCharsets.UTF_8); + } + catch(Exception e) + { + LOG.warn("Error getting login redirect url", e); + throw (new QAuthenticationException("Error getting login redirect url", e)); + } } @@ -401,4 +468,19 @@ public class OAuth2AuthenticationModule implements QAuthenticationModuleInterfac return (true); } + + + /*************************************************************************** + ** + ***************************************************************************/ + private OIDCProviderMetadata getOIDCProviderMetadata(OAuth2AuthenticationMetaData oAuth2AuthenticationMetaData) throws GeneralException, IOException + { + return oidcProviderMetadataMemoization.getResult(oAuth2AuthenticationMetaData.getName(), (name -> + { + Issuer issuer = new Issuer(oAuth2AuthenticationMetaData.getBaseUrl()); + OIDCProviderMetadata metadata = OIDCProviderMetadata.resolve(issuer); + return (metadata); + })).orElseThrow(() -> new GeneralException("Could not resolve OIDCProviderMetadata for " + oAuth2AuthenticationMetaData.getName())); + } + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/metadata/RedirectStateMetaDataProducer.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/metadata/RedirectStateMetaDataProducer.java new file mode 100644 index 00000000..c8e4f73c --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/metadata/RedirectStateMetaDataProducer.java @@ -0,0 +1,82 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2023. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.modules.authentication.implementations.metadata; + + +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducer; +import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import com.kingsrook.qqq.backend.core.model.metadata.audits.AuditLevel; +import com.kingsrook.qqq.backend.core.model.metadata.audits.QAuditRules; +import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType; +import com.kingsrook.qqq.backend.core.model.metadata.fields.ValueTooLongBehavior; +import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.tables.UniqueKey; + + +/******************************************************************************* + ** Meta Data Producer for RedirectState table + *******************************************************************************/ +public class RedirectStateMetaDataProducer extends MetaDataProducer +{ + public static final String TABLE_NAME = "redirectState"; + + private final String backendName; + + + + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public RedirectStateMetaDataProducer(String backendName) + { + this.backendName = backendName; + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public QTableMetaData produce(QInstance qInstance) throws QException + { + QTableMetaData tableMetaData = new QTableMetaData() + .withName(TABLE_NAME) + .withBackendName(backendName) + .withRecordLabelFormat("%s") + .withRecordLabelFields("state") + .withPrimaryKeyField("id") + .withUniqueKey(new UniqueKey("state")) + .withAuditRules(new QAuditRules().withAuditLevel(AuditLevel.NONE)) + + .withField(new QFieldMetaData("id", QFieldType.INTEGER).withIsEditable(false)) + .withField(new QFieldMetaData("state", QFieldType.STRING).withIsEditable(false).withMaxLength(45).withBehavior(ValueTooLongBehavior.ERROR)) + .withField(new QFieldMetaData("redirectUri", QFieldType.STRING).withIsEditable(false).withMaxLength(4096).withBehavior(ValueTooLongBehavior.ERROR)) + .withField(new QFieldMetaData("createDate", QFieldType.DATE_TIME).withIsEditable(false)); + + return tableMetaData; + } + +} 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 60656d62..814fcf83 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 @@ -137,6 +137,7 @@ import com.kingsrook.qqq.middleware.javalin.misc.DownloadFileSupplementalAction; import io.javalin.Javalin; import io.javalin.apibuilder.EndpointGroup; import io.javalin.http.Context; +import io.javalin.http.Cookie; import io.javalin.http.UploadedFile; import org.apache.commons.io.FileUtils; import org.eclipse.jetty.http.HttpStatus; @@ -535,14 +536,24 @@ public class QJavalinImplementation try { + /////////////////////////////////////////////// + // note: duplicated in ExecutorSessionUtils // + /////////////////////////////////////////////// Map authenticationContext = new HashMap<>(); String sessionIdCookieValue = context.cookie(SESSION_ID_COOKIE_NAME); String sessionUuidCookieValue = context.cookie(Auth0AuthenticationModule.SESSION_UUID_KEY); String authorizationHeaderValue = context.header("Authorization"); String apiKeyHeaderValue = context.header("x-api-key"); + String codeQueryParamValue = context.queryParam("code"); + String stateQueryParamValue = context.queryParam("state"); - if(StringUtils.hasContent(sessionIdCookieValue)) + if(StringUtils.hasContent(codeQueryParamValue) && StringUtils.hasContent(stateQueryParamValue)) + { + authenticationContext.put("code", codeQueryParamValue); + authenticationContext.put("state", stateQueryParamValue); + } + else if(StringUtils.hasContent(sessionIdCookieValue)) { /////////////////////////////////////////////////////// // sessionId - maybe used by table-based auth module // diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/ExecutorSessionUtils.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/ExecutorSessionUtils.java index 7a379fd5..db6797d0 100644 --- a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/ExecutorSessionUtils.java +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/executors/ExecutorSessionUtils.java @@ -60,14 +60,24 @@ public class ExecutorSessionUtils try { + ///////////////////////////////////////////////// + // note: duplicated in QJavalinImplementation // + ///////////////////////////////////////////////// Map authenticationContext = new HashMap<>(); String sessionIdCookieValue = context.cookie(SESSION_ID_COOKIE_NAME); String sessionUuidCookieValue = context.cookie(Auth0AuthenticationModule.SESSION_UUID_KEY); String authorizationHeaderValue = context.header("Authorization"); String apiKeyHeaderValue = context.header("x-api-key"); + String codeQueryParamValue = context.queryParam("code"); + String stateQueryParamValue = context.queryParam("state"); - if(StringUtils.hasContent(sessionIdCookieValue)) + if(StringUtils.hasContent(codeQueryParamValue) && StringUtils.hasContent(stateQueryParamValue)) + { + authenticationContext.put("code", codeQueryParamValue); + authenticationContext.put("state", stateQueryParamValue); + } + else if(StringUtils.hasContent(sessionIdCookieValue)) { /////////////////////////////////////////////////////// // sessionId - maybe used by table-based auth module // diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/routeproviders/authentication/SimpleRouteAuthenticator.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/routeproviders/authentication/SimpleRouteAuthenticator.java index 798945d0..82f83024 100644 --- a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/routeproviders/authentication/SimpleRouteAuthenticator.java +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/middleware/javalin/routeproviders/authentication/SimpleRouteAuthenticator.java @@ -55,6 +55,23 @@ public class SimpleRouteAuthenticator implements RouteAuthenticatorInterface { QSession qSession = QJavalinImplementation.setupSession(context, null); LOG.debug("Session has been activated", logPair("uuid", qSession.getUuid())); + + if(context.queryParamMap().containsKey("code") && context.queryParamMap().containsKey("state")) + { + ////////////////////////////////////////////////////////////////////////// + // if this request was a callback from oauth, with code & state params, // + // then redirect one last time removing those from the query string // + ////////////////////////////////////////////////////////////////////////// + String redirectURL = context.fullUrl().replace("code=" + context.queryParam("code"), "") + .replace("state=" + context.queryParam("state"), "") + .replaceFirst("&+$", "") + .replaceFirst("\\?&", "?") + .replaceFirst("\\?$", ""); + context.redirect(redirectURL); + LOG.debug("Redirecting request to remove code and state parameters"); + return (false); + } + return (true); } catch(QAuthenticationException e)