CTLE-307: added handling for translating 'too big' auth0 access_tokens into a smaller uuid when authorizing

This commit is contained in:
Tim Chamberlain
2023-03-27 21:24:30 -05:00
parent a43660a05a
commit a64a2801c0
9 changed files with 940 additions and 27 deletions

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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);
}
}

View File

@ -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);
}
}

View File

@ -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()));
}
}

View File

@ -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<String, String> 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));
}
}

View File

@ -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);
}
}

View File

@ -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));
}
}

View File

@ -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";

View File

@ -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);
}
}
/*******************************************************************************
**
*******************************************************************************/

View File

@ -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<String> 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<String> 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<String> 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
}
}
}
}