diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/exceptions/AccessTokenException.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/exceptions/AccessTokenException.java
new file mode 100644
index 00000000..76530ae4
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/exceptions/AccessTokenException.java
@@ -0,0 +1,113 @@
+/*
+ * 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 doing authentication
+ *
+ *******************************************************************************/
+public class AccessTokenException extends QAuthenticationException
+{
+ private Integer statusCode;
+
+
+
+ /*******************************************************************************
+ ** Constructor of message
+ **
+ *******************************************************************************/
+ public AccessTokenException(String message)
+ {
+ super(message);
+ }
+
+
+
+ /*******************************************************************************
+ ** Constructor of message
+ **
+ *******************************************************************************/
+ public AccessTokenException(String message, int statusCode)
+ {
+ super(message);
+ this.statusCode = statusCode;
+ }
+
+
+
+ /*******************************************************************************
+ ** Constructor of message & cause
+ **
+ *******************************************************************************/
+ public AccessTokenException(String message, Throwable cause)
+ {
+ super(message, cause);
+ }
+
+
+
+ /*******************************************************************************
+ ** Constructor of message & cause
+ **
+ *******************************************************************************/
+ public AccessTokenException(String message, Throwable cause, int statusCode)
+ {
+ super(message, cause);
+ this.statusCode = statusCode;
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for statusCode
+ **
+ *******************************************************************************/
+ public Integer getStatusCode()
+ {
+ return statusCode;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for statusCode
+ **
+ *******************************************************************************/
+ public void setStatusCode(Integer statusCode)
+ {
+ this.statusCode = statusCode;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for statusCode
+ **
+ *******************************************************************************/
+ public AccessTokenException withStatusCode(Integer statusCode)
+ {
+ this.statusCode = statusCode;
+ return (this);
+ }
+
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/authentication/Auth0AuthenticationMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/authentication/Auth0AuthenticationMetaData.java
index 7f1999e6..3cf6eabb 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/authentication/Auth0AuthenticationMetaData.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/authentication/Auth0AuthenticationMetaData.java
@@ -22,6 +22,7 @@
package com.kingsrook.qqq.backend.core.model.metadata.authentication;
+import java.io.Serializable;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.kingsrook.qqq.backend.core.model.metadata.QAuthenticationType;
import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModuleDispatcher;
@@ -43,6 +44,31 @@ public class Auth0AuthenticationMetaData extends QAuthenticationMetaData
@JsonIgnore
private String clientSecret;
+ ///////////////////////////////////////////////////////////////////////////////////////////
+ // these tables and fields are used to store auth0 application data and access data, the //
+ // access token can potentially be too large to send to qqq because of size limiations, //
+ // so we need to hash it and send the qqq user a version mapped to a smaller token //
+ ///////////////////////////////////////////////////////////////////////////////////////////
+ private String clientAuth0ApplicationTableName;
+ private String accessTokenTableName;
+
+ /////////////////////////////////////////
+ // fields on the auth0ApplicationTable //
+ /////////////////////////////////////////
+ private String applicationNameField;
+ private String auth0ClientIdField;
+ private String auth0ClientSecretMaskedField;
+ private Serializable qqqRecordIdField;
+
+
+ /////////////////////////////////////
+ // fields on the accessToken table //
+ /////////////////////////////////////
+ private String clientAuth0ApplicationIdField;
+ private String auth0AccessTokenField;
+ private String qqqAccessTokenField;
+ private String expiresInSecondsField;
+
/*******************************************************************************
@@ -189,4 +215,344 @@ public class Auth0AuthenticationMetaData extends QAuthenticationMetaData
return (this);
}
+
+
+ /*******************************************************************************
+ ** Getter for clientAuth0ApplicationTableName
+ **
+ *******************************************************************************/
+ public String getClientAuth0ApplicationTableName()
+ {
+ return clientAuth0ApplicationTableName;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for clientAuth0ApplicationTableName
+ **
+ *******************************************************************************/
+ public void setClientAuth0ApplicationTableName(String clientAuth0ApplicationTableName)
+ {
+ this.clientAuth0ApplicationTableName = clientAuth0ApplicationTableName;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for clientAuth0ApplicationTableName
+ **
+ *******************************************************************************/
+ public Auth0AuthenticationMetaData withClientAuth0ApplicationTableName(String clientAuth0ApplicationTableName)
+ {
+ this.clientAuth0ApplicationTableName = clientAuth0ApplicationTableName;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for accessTokenTableName
+ **
+ *******************************************************************************/
+ public String getAccessTokenTableName()
+ {
+ return accessTokenTableName;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for accessTokenTableName
+ **
+ *******************************************************************************/
+ public void setAccessTokenTableName(String accessTokenTableName)
+ {
+ this.accessTokenTableName = accessTokenTableName;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for accessTokenTableName
+ **
+ *******************************************************************************/
+ public Auth0AuthenticationMetaData withAccessTokenTableName(String accessTokenTableName)
+ {
+ this.accessTokenTableName = accessTokenTableName;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for applicationNameField
+ **
+ *******************************************************************************/
+ public String getApplicationNameField()
+ {
+ return applicationNameField;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for applicationNameField
+ **
+ *******************************************************************************/
+ public void setApplicationNameField(String applicationNameField)
+ {
+ this.applicationNameField = applicationNameField;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for applicationNameField
+ **
+ *******************************************************************************/
+ public Auth0AuthenticationMetaData withApplicationNameField(String applicationNameField)
+ {
+ this.applicationNameField = applicationNameField;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for auth0ClientIdField
+ **
+ *******************************************************************************/
+ public String getAuth0ClientIdField()
+ {
+ return auth0ClientIdField;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for auth0ClientIdField
+ **
+ *******************************************************************************/
+ public void setAuth0ClientIdField(String auth0ClientIdField)
+ {
+ this.auth0ClientIdField = auth0ClientIdField;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for auth0ClientIdField
+ **
+ *******************************************************************************/
+ public Auth0AuthenticationMetaData withAuth0ClientIdField(String auth0ClientIdField)
+ {
+ this.auth0ClientIdField = auth0ClientIdField;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for qqqRecordIdField
+ **
+ *******************************************************************************/
+ public Serializable getQqqRecordIdField()
+ {
+ return qqqRecordIdField;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for qqqRecordIdField
+ **
+ *******************************************************************************/
+ public void setQqqRecordIdField(Serializable qqqRecordIdField)
+ {
+ this.qqqRecordIdField = qqqRecordIdField;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for qqqRecordIdField
+ **
+ *******************************************************************************/
+ public Auth0AuthenticationMetaData withQqqRecordIdField(Serializable qqqRecordIdField)
+ {
+ this.qqqRecordIdField = qqqRecordIdField;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for auth0ClientSecretMaskedField
+ **
+ *******************************************************************************/
+ public String getAuth0ClientSecretMaskedField()
+ {
+ return auth0ClientSecretMaskedField;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for auth0ClientSecretMaskedField
+ **
+ *******************************************************************************/
+ public void setAuth0ClientSecretMaskedField(String auth0ClientSecretMaskedField)
+ {
+ this.auth0ClientSecretMaskedField = auth0ClientSecretMaskedField;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for auth0ClientSecretMaskedField
+ **
+ *******************************************************************************/
+ public Auth0AuthenticationMetaData withAuth0ClientSecretMaskedField(String auth0ClientSecretMaskedField)
+ {
+ this.auth0ClientSecretMaskedField = auth0ClientSecretMaskedField;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for clientAuth0ApplicationIdField
+ **
+ *******************************************************************************/
+ public String getClientAuth0ApplicationIdField()
+ {
+ return clientAuth0ApplicationIdField;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for clientAuth0ApplicationIdField
+ **
+ *******************************************************************************/
+ public void setClientAuth0ApplicationIdField(String clientAuth0ApplicationIdField)
+ {
+ this.clientAuth0ApplicationIdField = clientAuth0ApplicationIdField;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for clientAuth0ApplicationIdField
+ **
+ *******************************************************************************/
+ public Auth0AuthenticationMetaData withClientAuth0ApplicationIdField(String clientAuth0ApplicationIdField)
+ {
+ this.clientAuth0ApplicationIdField = clientAuth0ApplicationIdField;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for auth0AccessTokenField
+ **
+ *******************************************************************************/
+ public String getAuth0AccessTokenField()
+ {
+ return auth0AccessTokenField;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for auth0AccessTokenField
+ **
+ *******************************************************************************/
+ public void setAuth0AccessTokenField(String auth0AccessTokenField)
+ {
+ this.auth0AccessTokenField = auth0AccessTokenField;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for auth0AccessTokenField
+ **
+ *******************************************************************************/
+ public Auth0AuthenticationMetaData withAuth0AccessTokenField(String auth0AccessTokenField)
+ {
+ this.auth0AccessTokenField = auth0AccessTokenField;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for qqqAccessTokenField
+ **
+ *******************************************************************************/
+ public String getQqqAccessTokenField()
+ {
+ return qqqAccessTokenField;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for qqqAccessTokenField
+ **
+ *******************************************************************************/
+ public void setQqqAccessTokenField(String qqqAccessTokenField)
+ {
+ this.qqqAccessTokenField = qqqAccessTokenField;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for qqqAccessTokenField
+ **
+ *******************************************************************************/
+ public Auth0AuthenticationMetaData withQqqAccessTokenField(String qqqAccessTokenField)
+ {
+ this.qqqAccessTokenField = qqqAccessTokenField;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for expiresInSecondsField
+ **
+ *******************************************************************************/
+ public String getExpiresInSecondsField()
+ {
+ return expiresInSecondsField;
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for expiresInSecondsField
+ **
+ *******************************************************************************/
+ public void setExpiresInSecondsField(String expiresInSecondsField)
+ {
+ this.expiresInSecondsField = expiresInSecondsField;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for expiresInSecondsField
+ **
+ *******************************************************************************/
+ public Auth0AuthenticationMetaData withExpiresInSecondsField(String expiresInSecondsField)
+ {
+ this.expiresInSecondsField = expiresInSecondsField;
+ return (this);
+ }
+
}
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 d6dee291..6cd21ab6 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
@@ -23,9 +23,12 @@ package com.kingsrook.qqq.backend.core.modules.authentication;
import java.util.Map;
+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 org.apache.commons.lang.NotImplementedException;
/*******************************************************************************
@@ -54,4 +57,13 @@ 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/Auth0AuthenticationModule.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/Auth0AuthenticationModule.java
index b67885c3..074770cd 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
@@ -33,6 +33,7 @@ import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
+import java.util.UUID;
import com.auth0.client.auth.AuthAPI;
import com.auth0.exception.Auth0Exception;
import com.auth0.json.auth.TokenHolder;
@@ -46,18 +47,42 @@ 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.actions.tables.InsertAction;
+import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
+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.exceptions.QException;
import com.kingsrook.qqq.backend.core.logging.QLogger;
+import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
+import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
+import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
+import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
+import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
+import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput;
+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.Auth0AuthenticationMetaData;
+import com.kingsrook.qqq.backend.core.model.metadata.authentication.QAuthenticationMetaData;
+import com.kingsrook.qqq.backend.core.model.metadata.security.QSecurityKeyType;
import com.kingsrook.qqq.backend.core.model.session.QSession;
import com.kingsrook.qqq.backend.core.model.session.QUser;
import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModuleInterface;
import com.kingsrook.qqq.backend.core.state.InMemoryStateProvider;
import com.kingsrook.qqq.backend.core.state.SimpleStateKey;
import com.kingsrook.qqq.backend.core.state.StateProviderInterface;
+import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
+import com.kingsrook.qqq.backend.core.utils.JsonUtils;
+import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
+import org.apache.http.HttpStatus;
+import org.apache.http.client.entity.UrlEncodedFormEntity;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClientBuilder;
+import org.apache.http.message.BasicNameValuePair;
+import org.apache.http.util.EntityUtils;
import org.json.JSONArray;
import org.json.JSONObject;
@@ -83,7 +108,32 @@ public class Auth0AuthenticationModule implements QAuthenticationModuleInterface
public static final String INVALID_TOKEN_ERROR = "An invalid token was provided";
- private Instant now;
+ ////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ // this is how we allow the actions within this class to work without themselves having a logged-in user. //
+ ////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ private static QSession chickenAndEggSession = new QSession()
+ {
+
+ };
+
+
+
+ /*******************************************************************************
+ ** Getter for special session
+ **
+ *******************************************************************************/
+ private QSession getChickenAndEggSession()
+ {
+ for(String typeName : QContext.getQInstance().getSecurityKeyTypes().keySet())
+ {
+ QSecurityKeyType keyType = QContext.getQInstance().getSecurityKeyType(typeName);
+ if(StringUtils.hasContent(keyType.getAllAccessKeyName()))
+ {
+ chickenAndEggSession = chickenAndEggSession.withSecurityKeyValue(keyType.getAllAccessKeyName(), true);
+ }
+ }
+ return (chickenAndEggSession);
+ }
@@ -93,13 +143,14 @@ public class Auth0AuthenticationModule implements QAuthenticationModuleInterface
@Override
public QSession createSession(QInstance qInstance, Map context) throws QAuthenticationException
{
+ Auth0AuthenticationMetaData metaData = (Auth0AuthenticationMetaData) qInstance.getAuthentication();
+
///////////////////////////////////////////////////////////
// check if we are processing a Basic Auth Session first //
///////////////////////////////////////////////////////////
if(context.containsKey(BASIC_AUTH_KEY))
{
- Auth0AuthenticationMetaData metaData = (Auth0AuthenticationMetaData) qInstance.getAuthentication();
- AuthAPI auth = new AuthAPI(metaData.getBaseUrl(), metaData.getClientId(), metaData.getClientSecret());
+ AuthAPI auth = new AuthAPI(metaData.getBaseUrl(), metaData.getClientId(), metaData.getClientSecret());
try
{
/////////////////////////////////////////////////
@@ -120,10 +171,19 @@ public class Auth0AuthenticationModule implements QAuthenticationModuleInterface
}
}
- //////////////////////////////////////////////////////
- // get the jwt access token from the context object //
- //////////////////////////////////////////////////////
+ ////////////////////////////////////////////////////////////////////
+ // get the jwt id or qqq translated token from the context object //
+ ////////////////////////////////////////////////////////////////////
String accessToken = context.get(AUTH0_ACCESS_TOKEN_KEY);
+
+ /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ // check to see if the session id is a UUID, if so, that means we need to look up the 'actual' token in the access_token table //
+ /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ if(accessToken != null && StringUtils.isUUID(accessToken))
+ {
+ accessToken = lookupActualAccessToken(metaData, accessToken);
+ }
+
if(accessToken == null)
{
LOG.warn(TOKEN_NOT_PROVIDED_ERROR);
@@ -149,7 +209,7 @@ public class Auth0AuthenticationModule implements QAuthenticationModuleInterface
// 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, accessToken);
+ qSession = revalidateTokenAndBuildSession(qInstance, accessToken);
////////////////////////////////////////////////////////////////////
// put now into state so we dont check until next interval passes //
@@ -253,6 +313,14 @@ public class Auth0AuthenticationModule implements QAuthenticationModuleInterface
@Override
public boolean isSessionValid(QInstance instance, QSession session)
{
+ if(session == chickenAndEggSession)
+ {
+ ////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ // this is how we allow the actions within this class to work without themselves having a logged-in user. //
+ ////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ return (true);
+ }
+
if(session == null)
{
return (false);
@@ -283,7 +351,7 @@ public class Auth0AuthenticationModule implements QAuthenticationModuleInterface
try
{
LOG.debug("Re-validating token due to validation interval being passed: " + session.getIdReference());
- revalidateToken(instance, session.getIdReference());
+ revalidateTokenAndBuildSession(instance, session.getIdReference());
//////////////////////////////////////////////////////////////////
// update the timestamp in state provider, to avoid re-checking //
@@ -308,28 +376,37 @@ public class Auth0AuthenticationModule implements QAuthenticationModuleInterface
** makes request to check if a token is still valid and build new qSession if it is
**
*******************************************************************************/
- private QSession revalidateToken(QInstance qInstance, String accessToken) throws JwkException
+ private QSession revalidateTokenAndBuildSession(QInstance qInstance, String accessToken) throws JwkException
{
- Auth0AuthenticationMetaData metaData = (Auth0AuthenticationMetaData) qInstance.getAuthentication();
-
- DecodedJWT jwt = JWT.decode(accessToken);
- 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(accessToken);
-
+ validateToken(qInstance, accessToken);
return (buildQSessionFromToken(accessToken, qInstance));
}
+ /*******************************************************************************
+ ** tests validity of a token
+ **
+ *******************************************************************************/
+ private void validateToken(QInstance qInstance, String tokenString) throws JwkException
+ {
+ Auth0AuthenticationMetaData metaData = (Auth0AuthenticationMetaData) qInstance.getAuthentication();
+
+ DecodedJWT idToken = JWT.decode(tokenString);
+ JwkProvider provider = new UrlJwkProvider(metaData.getBaseUrl());
+ Jwk jwk = provider.get(idToken.getKeyId());
+ Algorithm algorithm = Algorithm.RSA256((RSAPublicKey) jwk.getPublicKey(), null);
+ JWT.require(algorithm)
+ .withIssuer(idToken.getIssuer())
+ .build()
+ .verify(idToken);
+ }
+
+
+
/*******************************************************************************
** extracts info from token creating a QSession
**
@@ -499,10 +576,13 @@ public class Auth0AuthenticationModule implements QAuthenticationModuleInterface
}
else
{
- String value = securityKeyValues.optString(jsonKey);
- if(value != null)
+ String values = securityKeyValues.optString(jsonKey);
+ if(StringUtils.hasContent(values))
{
- qSession.withSecurityKeyValue(securityKeyName, value);
+ for(String v : values.split(","))
+ {
+ qSession.withSecurityKeyValue(securityKeyName, v);
+ }
}
}
}
@@ -519,4 +599,185 @@ public class Auth0AuthenticationModule implements QAuthenticationModuleInterface
return (InMemoryStateProvider.getInstance());
}
+
+
+ /*******************************************************************************
+ ** Load an instance of the appropriate state provider
+ **
+ *******************************************************************************/
+ public String createAccessToken(QAuthenticationMetaData metaData, String clientId, String clientSecret) throws AccessTokenException
+ {
+ QSession sessionBefore = QContext.getQSession();
+ Auth0AuthenticationMetaData auth0MetaData = (Auth0AuthenticationMetaData) metaData;
+
+ try
+ {
+ QContext.setQSession(getChickenAndEggSession());
+
+ ///////////////////////////////////////////////////////////////////////////////////////
+ // fetch the application from database, will throw accesstokenexception if not found //
+ ///////////////////////////////////////////////////////////////////////////////////////
+ QRecord clientAuth0Application = getClientAuth0Application(auth0MetaData, clientId);
+
+ /////////////////////////////////////////////////////////////////////////////////////////////////
+ // request access token from auth0 if exception is not thrown, that means 200OK, we want to //
+ // store the actual access token in the database, and return a unique value //
+ // back to the user which will be what they use on subseqeunt requests (because token too big) //
+ /////////////////////////////////////////////////////////////////////////////////////////////////
+ JSONObject accessTokenData = requestAccessTokenFromAuth0(auth0MetaData, clientId, clientSecret);
+
+ Integer expiresInSeconds = accessTokenData.getInt("expires_in");
+ String accessToken = accessTokenData.getString("access_token");
+ String uuid = UUID.randomUUID().toString();
+
+ /////////////////////////////////
+ // store the details in the db //
+ /////////////////////////////////
+ QRecord accessTokenRecord = new QRecord()
+ .withValue(auth0MetaData.getClientAuth0ApplicationIdField(), clientAuth0Application.getValue("id"))
+ .withValue(auth0MetaData.getAuth0AccessTokenField(), accessToken)
+ .withValue(auth0MetaData.getQqqAccessTokenField(), uuid)
+ .withValue(auth0MetaData.getExpiresInSecondsField(), expiresInSeconds);
+ InsertInput input = new InsertInput();
+ input.setTableName(auth0MetaData.getAccessTokenTableName());
+ input.setRecords(List.of(accessTokenRecord));
+ new InsertAction().execute(input);
+
+ //////////////////////////////////
+ // update and send the response //
+ //////////////////////////////////
+ accessTokenData.put("access_token", uuid);
+ accessTokenData.remove("scope");
+ return (accessTokenData.toString());
+ }
+ catch(AccessTokenException ate)
+ {
+ throw (ate);
+ }
+ catch(Exception e)
+ {
+ throw (new AccessTokenException(e.getMessage(), e));
+ }
+ finally
+ {
+ QContext.setQSession(sessionBefore);
+ }
+ }
+
+
+
+ /*******************************************************************************
+ ** make http request to Auth0 for a new access token
+ **
+ *******************************************************************************/
+ public JSONObject requestAccessTokenFromAuth0(Auth0AuthenticationMetaData auth0MetaData, String clientId, String clientSecret) throws AccessTokenException
+ {
+ ///////////////////////////////////////////////////////////////////
+ // make a request to Auth0 using the client_id and client_secret //
+ ///////////////////////////////////////////////////////////////////
+ try(CloseableHttpClient httpClient = HttpClientBuilder.create().build())
+ {
+ UrlEncodedFormEntity urlEncodedFormEntity = new UrlEncodedFormEntity(List.of(
+ new BasicNameValuePair("content-type", "application/x-www-form-urlencoded"),
+ new BasicNameValuePair("grant_type", "client_credentials"),
+ new BasicNameValuePair("audience", auth0MetaData.getAudience()),
+ new BasicNameValuePair("client_id", clientId),
+ new BasicNameValuePair("client_secret", clientSecret)));
+
+ HttpPost request = new HttpPost(auth0MetaData.getBaseUrl() + "oauth/token");
+ request.setEntity(urlEncodedFormEntity);
+
+ try(CloseableHttpResponse response = httpClient.execute(request))
+ {
+ int statusCode = response.getStatusLine().getStatusCode();
+ String content = EntityUtils.toString(response.getEntity());
+
+ //////////////////////////////////////
+ // if 200OK, return the json object //
+ //////////////////////////////////////
+ if(statusCode == 200)
+ {
+ return (JsonUtils.toJSONObject(content));
+ }
+
+ //////////////////////////////////////////////////////////////////////////////////////////////////////////
+ // if not 200, throw an access token exception with the message and status code of the non-200 response //
+ //////////////////////////////////////////////////////////////////////////////////////////////////////////
+ throw (new AccessTokenException(content, statusCode));
+ }
+ }
+ catch(AccessTokenException ate)
+ {
+ throw (ate);
+ }
+ catch(Exception e)
+ {
+ throw (new AccessTokenException(e.getMessage(), e));
+ }
+ }
+
+
+
+ /*******************************************************************************
+ ** Look up access_token record, return if found.
+ **
+ *******************************************************************************/
+ String lookupActualAccessToken(Auth0AuthenticationMetaData metaData, String qqqAccessToken)
+ {
+ String accessToken = null;
+ QSession beforeSession = QContext.getQSession();
+
+ try
+ {
+ QContext.setQSession(getChickenAndEggSession());
+
+ //////////////////////////////////////////////////////////////////////////////////////
+ // try to look up existing auth0 application from database, insert one if not found //
+ //////////////////////////////////////////////////////////////////////////////////////
+ QueryInput queryInput = new QueryInput();
+ queryInput.setTableName(metaData.getAccessTokenTableName());
+ queryInput.setFilter(new QQueryFilter(new QFilterCriteria(metaData.getQqqAccessTokenField(), QCriteriaOperator.EQUALS, qqqAccessToken)));
+ QueryOutput queryOutput = new QueryAction().execute(queryInput);
+
+ if(CollectionUtils.nullSafeHasContents(queryOutput.getRecords()))
+ {
+ accessToken = queryOutput.getRecords().get(0).getValueString(metaData.getAuth0AccessTokenField());
+ }
+ }
+ catch(Exception e)
+ {
+ LOG.warn("Could not find Auth0 access token for provided qqq access token", e);
+ }
+ finally
+ {
+ QContext.setQSession(beforeSession);
+ }
+
+ return (accessToken);
+ }
+
+
+
+ /*******************************************************************************
+ ** Look up client_auth0_application record, return if found.
+ **
+ *******************************************************************************/
+ QRecord getClientAuth0Application(Auth0AuthenticationMetaData metaData, String clientId) throws QException
+ {
+ //////////////////////////////////////////////////////////////////////////////////////
+ // try to look up existing auth0 application from database, insert one if not found //
+ //////////////////////////////////////////////////////////////////////////////////////
+ QueryInput queryInput = new QueryInput();
+ queryInput.setTableName(metaData.getClientAuth0ApplicationTableName());
+ queryInput.setFilter(new QQueryFilter(new QFilterCriteria(metaData.getAuth0ClientIdField(), QCriteriaOperator.EQUALS, clientId)));
+ QueryOutput queryOutput = new QueryAction().execute(queryInput);
+
+ if(CollectionUtils.nullSafeHasContents(queryOutput.getRecords()))
+ {
+ return (queryOutput.getRecords().get(0));
+ }
+
+ throw (new AccessTokenException("This client has not been configured to use the API.", HttpStatus.SC_UNAUTHORIZED));
+ }
+
}
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 01f582d7..457f6bbb 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,7 +24,9 @@ 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;
@@ -36,6 +38,9 @@ import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModu
*******************************************************************************/
public class FullyAnonymousAuthenticationModule implements QAuthenticationModuleInterface
{
+ public static final String TEST_ACCESS_TOKEN = "b0a88d00-8439-48e8-8b48-e0ef40c40ed9";
+
+
/*******************************************************************************
**
@@ -71,4 +76,16 @@ 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/utils/StringUtils.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/StringUtils.java
index fa62e836..5f323ddf 100755
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/StringUtils.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/StringUtils.java
@@ -437,4 +437,14 @@ public class StringUtils
return (s.substring(0, 1).toUpperCase() + s.substring(1));
}
+
+
+ /*******************************************************************************
+ ** determines if a given string is a UUID
+ *******************************************************************************/
+ public static boolean isUUID(String s)
+ {
+ return (Pattern.matches("[a-f0-9]{8}(?:-[a-f0-9]{4}){4}[a-f0-9]{8}", s));
+ }
+
}
diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/Auth0AuthenticationModuleTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/Auth0AuthenticationModuleTest.java
index 7fc8e1ad..9a5d906e 100644
--- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/Auth0AuthenticationModuleTest.java
+++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/Auth0AuthenticationModuleTest.java
@@ -64,7 +64,6 @@ import static org.mockito.Mockito.verify;
*******************************************************************************/
public class Auth0AuthenticationModuleTest extends BaseTest
{
- 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";
diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandler.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandler.java
index dba52b15..f88349b5 100644
--- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandler.java
+++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandler.java
@@ -48,6 +48,7 @@ import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction;
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.exceptions.QException;
import com.kingsrook.qqq.backend.core.exceptions.QModuleDispatchException;
@@ -74,9 +75,12 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateOutput;
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.Auth0AuthenticationMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
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.modules.authentication.QAuthenticationModuleDispatcher;
+import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModuleInterface;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.ExceptionUtils;
import com.kingsrook.qqq.backend.core.utils.JsonUtils;
@@ -129,6 +133,11 @@ public class QJavalinApiHandler
{
return (() ->
{
+ /////////////////////////////
+ // authentication endpoint //
+ /////////////////////////////
+ ApiBuilder.post("/api/oauth/token", QJavalinApiHandler::handleAuthorization);
+
ApiBuilder.path("/api/{version}", () -> // todo - configurable, that /api/ bit?
{
ApiBuilder.get("/openapi.yaml", QJavalinApiHandler::doSpecYaml);
@@ -243,6 +252,78 @@ public class QJavalinApiHandler
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private static void handleAuthorization(Context context)
+ {
+ try
+ {
+ //////////////////////////////
+ // validate required inputs //
+ //////////////////////////////
+ String clientId = context.formParam("client_id");
+ if(clientId == null)
+ {
+ context.status(HttpStatus.BAD_REQUEST_400);
+ context.result("'client_id' must be provided.");
+ return;
+ }
+ String clientSecret = context.formParam("client_secret");
+ if(clientSecret == null)
+ {
+ context.status(HttpStatus.BAD_REQUEST_400);
+ context.result("'client_secret' must be provided.");
+ return;
+ }
+
+ ////////////////////////////////////////////////////////
+ // get the auth0 authentication module from qInstance //
+ ////////////////////////////////////////////////////////
+ Auth0AuthenticationMetaData metaData = (Auth0AuthenticationMetaData) qInstance.getAuthentication();
+ QAuthenticationModuleDispatcher qAuthenticationModuleDispatcher = new QAuthenticationModuleDispatcher();
+ QAuthenticationModuleInterface authenticationModule = qAuthenticationModuleDispatcher.getQModule(qInstance.getAuthentication());
+
+ try
+ {
+ /////////////////////////////////////////////////////////////////////////////////////////
+ // make call to get access token data, if no exception thrown, assume 200OK and return //
+ /////////////////////////////////////////////////////////////////////////////////////////
+ QContext.init(qInstance, null); // hmm...
+ String accessToken = authenticationModule.createAccessToken(metaData, clientId, clientSecret);
+ context.status(io.javalin.http.HttpStatus.OK);
+ context.result(accessToken);
+ QJavalinAccessLogger.logEndSuccess();
+ return;
+ }
+ catch(AccessTokenException aae)
+ {
+ ///////////////////////////////////////////////////////////////////////////
+ // if the exception has a status code, then return that code and message //
+ ///////////////////////////////////////////////////////////////////////////
+ if(aae.getStatusCode() != null)
+ {
+ context.status(aae.getStatusCode());
+ context.result(aae.getMessage());
+ QJavalinAccessLogger.logEndSuccess();
+ return;
+ }
+
+ ////////////////////////////////////////////////////////
+ // if no code, throw and handle like other exceptions //
+ ////////////////////////////////////////////////////////
+ throw (aae);
+ }
+ }
+ catch(Exception e)
+ {
+ handleException(context, e);
+ QJavalinAccessLogger.logEndFail(e);
+ }
+ }
+
+
+
/*******************************************************************************
**
*******************************************************************************/
diff --git a/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandlerTest.java b/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandlerTest.java
index 1cde9d5a..c91d1483 100644
--- a/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandlerTest.java
+++ b/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandlerTest.java
@@ -46,6 +46,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
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.tables.QTableMetaData;
+import com.kingsrook.qqq.backend.core.modules.authentication.implementations.FullyAnonymousAuthenticationModule;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.javalin.QJavalinImplementation;
import kong.unirest.HttpResponse;
@@ -75,6 +76,9 @@ class QJavalinApiHandlerTest extends BaseTest
protected static QJavalinImplementation qJavalinImplementation;
+ private static final String OAUTH_CLIENT_ID = "test-oauth-client-id";
+ private static final String OAUTH_CLIENT_SECRET = "test-oauth-client-secret";
+
/*******************************************************************************
@@ -998,6 +1002,56 @@ class QJavalinApiHandlerTest extends BaseTest
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testAuthorizeNoParams()
+ {
+ ///////////////
+ // no params //
+ ///////////////
+ HttpResponse response = Unirest.post(BASE_URL + "/api/oauth/token").asString();
+ assertEquals(HttpStatus.BAD_REQUEST_400, response.getStatus());
+ assertThat(response.getBody()).contains("client_id");
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testAuthorizeOneParam()
+ {
+ ///////////////
+ // no params //
+ ///////////////
+ HttpResponse response = Unirest.post(BASE_URL + "/api/oauth/token")
+ .body("client_id=XXXXXXXXXX").asString();
+ assertEquals(HttpStatus.BAD_REQUEST_400, response.getStatus());
+ assertThat(response.getBody()).contains("client_secret");
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void testAuthorizeAllParams()
+ {
+ ///////////////
+ // no params //
+ ///////////////
+ HttpResponse response = Unirest.post(BASE_URL + "/api/oauth/token")
+ .body("client_id=XXXXXXXXXX&client_secret=YYYYYYYYYYYY").asString();
+ assertEquals(HttpStatus.OK_200, response.getStatus());
+ assertThat(response.getBody()).isEqualTo(FullyAnonymousAuthenticationModule.TEST_ACCESS_TOKEN);
+ }
+
+
+
/*******************************************************************************
**
*******************************************************************************/
@@ -1124,4 +1178,4 @@ class QJavalinApiHandlerTest extends BaseTest
}
}
-}
\ No newline at end of file
+}