From a64a2801c0295bbf3f64b847fe077d60d38a3ead Mon Sep 17 00:00:00 2001 From: Tim Chamberlain Date: Mon, 27 Mar 2023 21:24:30 -0500 Subject: [PATCH] CTLE-307: added handling for translating 'too big' auth0 access_tokens into a smaller uuid when authorizing --- .../core/exceptions/AccessTokenException.java | 113 ++++++ .../Auth0AuthenticationMetaData.java | 366 ++++++++++++++++++ .../QAuthenticationModuleInterface.java | 12 + .../Auth0AuthenticationModule.java | 311 +++++++++++++-- .../FullyAnonymousAuthenticationModule.java | 17 + .../qqq/backend/core/utils/StringUtils.java | 10 + .../Auth0AuthenticationModuleTest.java | 1 - .../qqq/api/javalin/QJavalinApiHandler.java | 81 ++++ .../api/javalin/QJavalinApiHandlerTest.java | 56 ++- 9 files changed, 940 insertions(+), 27 deletions(-) create mode 100644 qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/exceptions/AccessTokenException.java 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 +}