From aee7e88037a4b2539b348fcef11ce1ae8a21f804 Mon Sep 17 00:00:00 2001 From: Tamas Soltesz Date: Mon, 5 Aug 2024 21:51:29 +0200 Subject: [PATCH] Feature: oauth update client api (#1020) * fix/change annotations for configs * feat: oauth2 auth API - WIP * fix: hidefromdashboard to oauth_provider service url configs * feat: OAuthAPI input parsing, basic flow * feat: first test in progress * fix: review fixes * feat: tables for oauth in sqlite * fix: remove unnecessary tables * fix: store only the necessary data in the client table * feat: oauth client - app exists check in db, a few tests * fix: review fixes * fix: review fixes * feat: new configs for handling errors from hydra * feat: using the new configs for oauth provider * fix: CHANGELOG * fix: tests for the new Util method * fix: changelog changes * fix: changelog migration section fix * fix: fixing repeated header handling in HttpRequest#sendGETRequestWithResponseHeaders * fix: more tests for oauth auth * fix: review fix - more checks for the oauth config validity * fix: review fix - throwing expection if there is no location header in response from hydra * fix: review fix - renamed exception * feat: oauth2 register client API * feat: oauth2 get clients API * feat: OAuth2 DELETE clients API * fix: following the already existing response pattern with the oauth2 apis * fix: renaming exception to be more expressive * fix: review fixes * feat: oauth2 client update support * fix: using BadRequestException instead of custom format for hydra invalid input errors * fix: renaming method * fix: remove unused constant * fix: returning 500 when oauth update client goes wrong * fix: fixing test after changing error response * chore: removing TODO from code --- CHANGELOG.md | 2 + .../supertokens/httpRequest/HttpRequest.java | 31 ++- src/main/java/io/supertokens/oauth/OAuth.java | 60 ++++- ...ava => OAuthAPIInvalidInputException.java} | 4 +- .../OAuthClientUpdateException.java | 28 ++ .../io/supertokens/webserver/InputParser.java | 4 +- .../supertokens/webserver/WebserverAPI.java | 11 +- .../webserver/api/oauth/OAuthAuthAPI.java | 2 +- .../webserver/api/oauth/OAuthClientsAPI.java | 52 +++- .../httpRequest/HttpRequestForTesting.java | 32 ++- .../test/oauth/api/OAuthClientsAPITest.java | 245 ++++++++++++++++++ 11 files changed, 445 insertions(+), 26 deletions(-) rename src/main/java/io/supertokens/oauth/exceptions/{OAuthClientRegisterInvalidInputException.java => OAuthAPIInvalidInputException.java} (84%) create mode 100644 src/main/java/io/supertokens/oauth/exceptions/OAuthClientUpdateException.java diff --git a/CHANGELOG.md b/CHANGELOG.md index ca1fa9b63..26b42640d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,8 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - Adds GET `/recipe/oauth/clients?clientId=example_id` for loading OAuth2 client - Adds DELETE `/recipe/oauth/clients` for deleting OAuth2 Clients - Creates new table `oauth_clients` +- Introduces PATCH capabilities for core (receiving and sending PATCH requests) +- Adds PATCH `/recipe/oauth/clients` for OAuth2 client update ### Migration TODO: after plugin support diff --git a/src/main/java/io/supertokens/httpRequest/HttpRequest.java b/src/main/java/io/supertokens/httpRequest/HttpRequest.java index 98112031a..7edc8183f 100644 --- a/src/main/java/io/supertokens/httpRequest/HttpRequest.java +++ b/src/main/java/io/supertokens/httpRequest/HttpRequest.java @@ -21,10 +21,9 @@ import io.supertokens.Main; import java.io.*; -import java.net.HttpURLConnection; -import java.net.MalformedURLException; -import java.net.URL; -import java.net.URLEncoder; +import java.net.*; +import java.net.http.HttpClient; +import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; import java.util.List; import java.util.Map; @@ -263,6 +262,30 @@ public static T sendJsonPUTRequest(Main main, String requestID, String url, return sendJsonRequest(main, requestID, url, requestBody, connectionTimeoutMS, readTimeoutMS, version, "PUT"); } + public static T sendJsonPATCHRequest(Main main, String url, JsonElement requestBody) + throws IOException, HttpResponseException, InterruptedException { + + HttpClient client = null; + + String body = requestBody.toString(); + java.net.http.HttpRequest rawRequest = java.net.http.HttpRequest.newBuilder() + .uri(URI.create(url)) + .method("PATCH", java.net.http.HttpRequest.BodyPublishers.ofString(body)) + .build(); + client = HttpClient.newHttpClient(); + HttpResponse response = client.send(rawRequest, HttpResponse.BodyHandlers.ofString()); + + int responseCode = response.statusCode(); + + if (responseCode < STATUS_CODE_ERROR_THRESHOLD) { + if (!isJsonValid(response.body().toString())) { + return (T) response.body().toString(); + } + return (T) (new JsonParser().parse(response.body().toString())); + } + throw new HttpResponseException(responseCode, response.body().toString()); + } + public static T sendJsonDELETERequest(Main main, String requestID, String url, JsonElement requestBody, int connectionTimeoutMS, int readTimeoutMS, Integer version) throws IOException, HttpResponseException { diff --git a/src/main/java/io/supertokens/oauth/OAuth.java b/src/main/java/io/supertokens/oauth/OAuth.java index be578630c..6b7ef3488 100644 --- a/src/main/java/io/supertokens/oauth/OAuth.java +++ b/src/main/java/io/supertokens/oauth/OAuth.java @@ -16,6 +16,7 @@ package io.supertokens.oauth; +import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonParser; @@ -103,7 +104,7 @@ public static OAuthAuthResponse getAuthorizationUrl(Main main, AppIdentifier app //This more or less acts as a pass-through for the sdks, apart from camelCase <-> snake_case key transformation and setting a few default values public static JsonObject registerOAuthClient(Main main, AppIdentifier appIdentifier, Storage storage, JsonObject paramsFromSdk) throws TenantOrAppNotFoundException, InvalidConfigException, IOException, - OAuthClientRegisterInvalidInputException, + OAuthAPIInvalidInputException, NoSuchAlgorithmException, StorageQueryException { OAuthStorage oauthStorage = StorageUtils.getOAuthStorage(storage); @@ -130,7 +131,7 @@ public static JsonObject registerOAuthClient(Main main, AppIdentifier appIdentif } else { //other error from hydra, like invalid content in json. Throw exception throw createCustomExceptionFromHttpResponseException( - e, OAuthClientRegisterInvalidInputException.class); + e, OAuthAPIInvalidInputException.class); } } catch (NoSuchMethodException | InvocationTargetException | InstantiationException | IllegalAccessException ex) { @@ -191,6 +192,61 @@ public static void deleteOAuthClient(Main main, AppIdentifier appIdentifier, Sto } } + public static JsonObject updateOauthClient(Main main, AppIdentifier appIdentifier, Storage storage, JsonObject paramsFromSdk) + throws TenantOrAppNotFoundException, InvalidConfigException, StorageQueryException, + InvocationTargetException, NoSuchMethodException, InstantiationException, + IllegalAccessException, OAuthClientNotFoundException, OAuthAPIInvalidInputException, + OAuthClientUpdateException { + + OAuthStorage oauthStorage = StorageUtils.getOAuthStorage(storage); + String adminOAuthProviderServiceUrl = Config.getConfig(appIdentifier.getAsPublicTenantIdentifier(), main).getOAuthProviderAdminServiceUrl(); + + String clientId = paramsFromSdk.get("clientId").getAsString(); + + if (!oauthStorage.doesClientIdExistForThisApp(appIdentifier, clientId)) { + throw new OAuthClientNotFoundException("Unable to locate the resource", ""); + } else { + JsonArray hydraInput = translateIncomingDataToHydraUpdateFormat(paramsFromSdk); + try { + JsonObject updatedClient = HttpRequest.sendJsonPATCHRequest(main, adminOAuthProviderServiceUrl + HYDRA_CLIENTS_ENDPOINT+ "/" + clientId, hydraInput); + return formatResponseForSDK(updatedClient); + } catch (IOException | InterruptedException e) { + throw new RuntimeException(e); + } catch (HttpResponseException e) { + int responseStatusCode = e.statusCode; + switch (responseStatusCode){ + case 400 -> throw createCustomExceptionFromHttpResponseException(e, OAuthAPIInvalidInputException.class); + case 404 -> throw createCustomExceptionFromHttpResponseException(e, OAuthClientNotFoundException.class); + case 500 -> throw createCustomExceptionFromHttpResponseException(e, OAuthClientUpdateException.class); // hydra is not so helpful with the error messages at this endpoint.. + default -> throw new RuntimeException(e); + } + } + } + } + + private static JsonArray translateIncomingDataToHydraUpdateFormat(JsonObject input){ + JsonArray hydraPatchFormat = new JsonArray(); + for (Map.Entry changeIt : input.entrySet()) { + if (changeIt.getKey().equals("clientId")) { + continue; // we are not updating clientIds! + } + hydraPatchFormat.add(translateToHydraPatch(changeIt.getKey(),changeIt.getValue())); + } + + return hydraPatchFormat; + } + + private static JsonObject translateToHydraPatch(String elementName, JsonElement newValue){ + JsonObject patchFormat = new JsonObject(); + String hydraElementName = Utils.camelCaseToSnakeCase(elementName); + patchFormat.addProperty("from", "/" + hydraElementName); + patchFormat.addProperty("path", "/" + hydraElementName); + patchFormat.addProperty("op", "replace"); // What was sent by the sdk should be handled as a complete new value for the property + patchFormat.add("value", newValue); + + return patchFormat; + } + private static T createCustomExceptionFromHttpResponseException(HttpResponseException exception, Class customExceptionClass) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException { String errorMessage = exception.rawMessage; diff --git a/src/main/java/io/supertokens/oauth/exceptions/OAuthClientRegisterInvalidInputException.java b/src/main/java/io/supertokens/oauth/exceptions/OAuthAPIInvalidInputException.java similarity index 84% rename from src/main/java/io/supertokens/oauth/exceptions/OAuthClientRegisterInvalidInputException.java rename to src/main/java/io/supertokens/oauth/exceptions/OAuthAPIInvalidInputException.java index 7b6247687..91634a198 100644 --- a/src/main/java/io/supertokens/oauth/exceptions/OAuthClientRegisterInvalidInputException.java +++ b/src/main/java/io/supertokens/oauth/exceptions/OAuthAPIInvalidInputException.java @@ -18,12 +18,12 @@ import java.io.Serial; -public class OAuthClientRegisterInvalidInputException extends OAuthException{ +public class OAuthAPIInvalidInputException extends OAuthException{ @Serial private static final long serialVersionUID = 665027786586190611L; - public OAuthClientRegisterInvalidInputException(String error, String errorDescription) { + public OAuthAPIInvalidInputException(String error, String errorDescription) { super(error, errorDescription); } } diff --git a/src/main/java/io/supertokens/oauth/exceptions/OAuthClientUpdateException.java b/src/main/java/io/supertokens/oauth/exceptions/OAuthClientUpdateException.java new file mode 100644 index 000000000..bc03389f2 --- /dev/null +++ b/src/main/java/io/supertokens/oauth/exceptions/OAuthClientUpdateException.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2024, VRAI Labs and/or its affiliates. All rights reserved. + * + * This software is licensed under the Apache License, Version 2.0 (the + * "License") as published by the Apache Software Foundation. + * + * You may not use this file except in compliance with the License. You may + * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package io.supertokens.oauth.exceptions; + +import java.io.Serial; + +public class OAuthClientUpdateException extends OAuthException{ + @Serial + private static final long serialVersionUID = -5191044905397936167L; + + public OAuthClientUpdateException(String error, String errorDescription) { + super(error, errorDescription); + } +} diff --git a/src/main/java/io/supertokens/webserver/InputParser.java b/src/main/java/io/supertokens/webserver/InputParser.java index d27bd7a84..66a98cdef 100644 --- a/src/main/java/io/supertokens/webserver/InputParser.java +++ b/src/main/java/io/supertokens/webserver/InputParser.java @@ -21,7 +21,6 @@ import com.google.gson.JsonObject; import com.google.gson.JsonParser; -import io.supertokens.webserver.api.oauth.OAuthClientsAPI; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; @@ -239,7 +238,7 @@ public static Integer parseIntOrThrowError(JsonObject element, String fieldName, } - public static List collectAllMissingRequiredFieldsOrThrowError(JsonObject input, List requiredFields) + public static void throwErrorOnMissingRequiredField(JsonObject input, List requiredFields) throws ServletException { List missingFields = new ArrayList<>(); for(String requiredField : requiredFields){ @@ -252,6 +251,5 @@ public static List collectAllMissingRequiredFieldsOrThrowError(JsonObjec throw new ServletException(new WebserverAPI.BadRequestException("Field name `" + String.join("','", missingFields) + "` is missing in JSON input")); } - return missingFields; } } diff --git a/src/main/java/io/supertokens/webserver/WebserverAPI.java b/src/main/java/io/supertokens/webserver/WebserverAPI.java index 1ba34f8a5..9f75474fb 100644 --- a/src/main/java/io/supertokens/webserver/WebserverAPI.java +++ b/src/main/java/io/supertokens/webserver/WebserverAPI.java @@ -157,6 +157,10 @@ protected void doTrace(HttpServletRequest req, HttpServletResponse resp) throws this.sendTextResponse(405, "Method not supported", resp); } + protected void doPatch(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException{ + this.sendTextResponse(405, "Method not supported", resp); + } + private void assertThatVersionIsCompatible(SemVer version) throws ServletException { if (version == null) { throw new ServletException(new BadRequestException("cdi-version not provided")); @@ -486,7 +490,12 @@ protected void service(HttpServletRequest req, HttpServletResponse resp) throws Logging.info(main, tenantIdentifier, "API called: " + req.getRequestURI() + ". Method: " + req.getMethod(), false); } - super.service(req, resp); + String method = req.getMethod(); + if(method.equals("PATCH")){ + this.doPatch(req, resp); + } else { + super.service(req, resp); + } } catch (Exception e) { Logging.error(main, tenantIdentifier, diff --git a/src/main/java/io/supertokens/webserver/api/oauth/OAuthAuthAPI.java b/src/main/java/io/supertokens/webserver/api/oauth/OAuthAuthAPI.java index 60a484e0a..559663d14 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthAuthAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthAuthAPI.java @@ -59,7 +59,7 @@ public String getPath() { protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException { JsonObject input = InputParser.parseJsonObjectOrThrowError(req); - InputParser.collectAllMissingRequiredFieldsOrThrowError(input, REQUIRED_FIELDS_FOR_POST); + InputParser.throwErrorOnMissingRequiredField(input, REQUIRED_FIELDS_FOR_POST); try { AppIdentifier appIdentifier = getAppIdentifier(req); diff --git a/src/main/java/io/supertokens/webserver/api/oauth/OAuthClientsAPI.java b/src/main/java/io/supertokens/webserver/api/oauth/OAuthClientsAPI.java index 60a04b037..a0c269ec1 100644 --- a/src/main/java/io/supertokens/webserver/api/oauth/OAuthClientsAPI.java +++ b/src/main/java/io/supertokens/webserver/api/oauth/OAuthClientsAPI.java @@ -21,7 +21,8 @@ import io.supertokens.multitenancy.exception.BadPermissionException; import io.supertokens.oauth.OAuth; import io.supertokens.oauth.exceptions.OAuthClientNotFoundException; -import io.supertokens.oauth.exceptions.OAuthClientRegisterInvalidInputException; +import io.supertokens.oauth.exceptions.OAuthAPIInvalidInputException; +import io.supertokens.oauth.exceptions.OAuthClientUpdateException; import io.supertokens.oauth.exceptions.OAuthException; import io.supertokens.pluginInterface.RECIPE_ID; import io.supertokens.pluginInterface.Storage; @@ -37,6 +38,7 @@ import java.io.IOException; import java.io.Serial; +import java.lang.reflect.InvocationTargetException; import java.security.NoSuchAlgorithmException; import java.util.Arrays; import java.util.List; @@ -47,7 +49,9 @@ public class OAuthClientsAPI extends WebserverAPI { private static final long serialVersionUID = -4482427281337641246L; private static final List REQUIRED_INPUT_FIELDS_FOR_POST = Arrays.asList(new String[]{"clientName", "scope"}); + private static final List REQUIRED_INPUT_FIELDS_FOR_PATCH = Arrays.asList(new String[]{"clientId"}); public static final String OAUTH2_CLIENT_NOT_FOUND_ERROR = "OAUTH2_CLIENT_NOT_FOUND_ERROR"; + public static final String OAUTH2_CLIENT_UPDATE_ERROR = "OAUTH2_CLIENT_UPDATE_ERROR"; @Override public String getPath() { @@ -61,7 +65,7 @@ public OAuthClientsAPI(Main main){ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException { JsonObject input = InputParser.parseJsonObjectOrThrowError(req); - InputParser.collectAllMissingRequiredFieldsOrThrowError(input, REQUIRED_INPUT_FIELDS_FOR_POST); + InputParser.throwErrorOnMissingRequiredField(input, REQUIRED_INPUT_FIELDS_FOR_POST); try { AppIdentifier appIdentifier = getAppIdentifier(req); @@ -73,7 +77,7 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I postResponseBody.add("client", response); sendJsonResponse(200, postResponseBody, resp); - } catch (OAuthClientRegisterInvalidInputException registerException) { + } catch (OAuthAPIInvalidInputException registerException) { throw new ServletException(new BadRequestException(registerException.error + " - " + registerException.errorDescription)); @@ -99,8 +103,7 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IO sendJsonResponse(200, response, resp); } catch (OAuthClientNotFoundException e) { - JsonObject errorResponse = createJsonFromException(e); - errorResponse.addProperty("status", OAUTH2_CLIENT_NOT_FOUND_ERROR); + JsonObject errorResponse = createJsonFromException(e, OAUTH2_CLIENT_NOT_FOUND_ERROR); sendJsonResponse(200, errorResponse, resp); } catch (TenantOrAppNotFoundException | InvalidConfigException | BadPermissionException @@ -125,8 +128,7 @@ protected void doDelete(HttpServletRequest req, HttpServletResponse resp) throws sendJsonResponse(200, responseBody, resp); } catch (OAuthClientNotFoundException e) { - JsonObject errorResponse = createJsonFromException(e); - errorResponse.addProperty("status", OAUTH2_CLIENT_NOT_FOUND_ERROR); + JsonObject errorResponse = createJsonFromException(e, OAUTH2_CLIENT_NOT_FOUND_ERROR); sendJsonResponse(200, errorResponse, resp); } catch (TenantOrAppNotFoundException | InvalidConfigException | BadPermissionException @@ -135,11 +137,43 @@ protected void doDelete(HttpServletRequest req, HttpServletResponse resp) throws } } - private JsonObject createJsonFromException(OAuthException exception){ + @Override + protected void doPatch(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException { + JsonObject input = InputParser.parseJsonObjectOrThrowError(req); + InputParser.throwErrorOnMissingRequiredField(input, REQUIRED_INPUT_FIELDS_FOR_PATCH); + + try { + AppIdentifier appIdentifier = getAppIdentifier(req); + Storage storage = enforcePublicTenantAndGetPublicTenantStorage(req); + + JsonObject response = OAuth.updateOauthClient(super.main, appIdentifier, storage, input); + JsonObject postResponseBody = new JsonObject(); + postResponseBody.addProperty("status", "OK"); + postResponseBody.add("client", response); + sendJsonResponse(200, postResponseBody, resp); + + } catch (OAuthAPIInvalidInputException exception) { + throw new ServletException(new BadRequestException(exception.error + " - " + exception.errorDescription)); + } catch (OAuthClientUpdateException updateException) { + //for errors with the update from hydra, which are not reported back as invalid input errors + throw new ServletException(updateException); + + } catch (OAuthClientNotFoundException clientNotFoundException) { + JsonObject errorResponse = createJsonFromException(clientNotFoundException, OAUTH2_CLIENT_NOT_FOUND_ERROR); + sendJsonResponse(200, errorResponse, resp); + + } catch (TenantOrAppNotFoundException | InvalidConfigException | StorageQueryException | + InvocationTargetException | NoSuchMethodException | InstantiationException | IllegalAccessException + | BadPermissionException ex) { + throw new ServletException(ex); + } + } + + private JsonObject createJsonFromException(OAuthException exception, String status){ JsonObject errorResponse = new JsonObject(); errorResponse.addProperty("error", exception.error); errorResponse.addProperty("errorDescription", exception.errorDescription); - + errorResponse.addProperty("status", status); return errorResponse; } } diff --git a/src/test/java/io/supertokens/test/httpRequest/HttpRequestForTesting.java b/src/test/java/io/supertokens/test/httpRequest/HttpRequestForTesting.java index 876b473c4..3faec4fd0 100644 --- a/src/test/java/io/supertokens/test/httpRequest/HttpRequestForTesting.java +++ b/src/test/java/io/supertokens/test/httpRequest/HttpRequestForTesting.java @@ -22,10 +22,9 @@ import io.supertokens.pluginInterface.multitenancy.TenantIdentifier; import java.io.*; -import java.net.HttpURLConnection; -import java.net.MalformedURLException; -import java.net.URL; -import java.net.URLEncoder; +import java.net.*; +import java.net.http.HttpClient; +import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; import java.util.Map; @@ -199,6 +198,31 @@ public static T sendJsonRequest(Main main, String requestID, String url, Jso } } + public static T sendJsonPATCHRequest(Main main, String url, JsonElement requestBody) + throws IOException, InterruptedException, + HttpResponseException { + + HttpClient client = null; + + String body = requestBody.toString(); + java.net.http.HttpRequest rawRequest = java.net.http.HttpRequest.newBuilder() + .uri(URI.create(url)) + .method("PATCH", java.net.http.HttpRequest.BodyPublishers.ofString(body)) + .build(); + client = HttpClient.newHttpClient(); + HttpResponse response = client.send(rawRequest, HttpResponse.BodyHandlers.ofString()); + + int responseCode = response.statusCode(); + + if (responseCode < STATUS_CODE_ERROR_THRESHOLD) { + if (!isJsonValid(response.body().toString())) { + return (T) response.body().toString(); + } + return (T) (new JsonParser().parse(response.body().toString())); + } + throw new io.supertokens.test.httpRequest.HttpResponseException(responseCode, response.body().toString()); + } + public static T sendJsonPOSTRequest(Main main, String requestID, String url, JsonElement requestBody, int connectionTimeoutMS, int readTimeoutMS, Integer version, String cdiVersion, String rid) diff --git a/src/test/java/io/supertokens/test/oauth/api/OAuthClientsAPITest.java b/src/test/java/io/supertokens/test/oauth/api/OAuthClientsAPITest.java index ce00c91be..a4a2c067b 100644 --- a/src/test/java/io/supertokens/test/oauth/api/OAuthClientsAPITest.java +++ b/src/test/java/io/supertokens/test/oauth/api/OAuthClientsAPITest.java @@ -22,12 +22,16 @@ import io.supertokens.ProcessState; import io.supertokens.httpRequest.HttpRequest; import io.supertokens.pluginInterface.RECIPE_ID; +import io.supertokens.pluginInterface.exceptions.StorageQueryException; +import io.supertokens.pluginInterface.multitenancy.AppIdentifier; +import io.supertokens.pluginInterface.oauth.exceptions.OAuth2ClientAlreadyExistsForAppException; import io.supertokens.pluginInterface.oauth.sqlStorage.OAuthSQLStorage; import io.supertokens.storageLayer.StorageLayer; import io.supertokens.test.TestingProcessManager; import io.supertokens.test.Utils; import io.supertokens.test.httpRequest.HttpRequestForTesting; import io.supertokens.test.httpRequest.HttpResponseException; +import jakarta.servlet.ServletException; import org.junit.*; import org.junit.rules.TestRule; @@ -224,4 +228,245 @@ public void testGETClientNotExisting_returnsError() process.kill(); assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); } + + @Test + public void testClientUpdatePatch() + throws StorageQueryException, IOException, OAuth2ClientAlreadyExistsForAppException, + io.supertokens.test.httpRequest.HttpResponseException, InterruptedException { + + String[] args = {"../"}; + + this.process = TestingProcessManager.start(args); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + String clientId = "6030f07e-c8ef-4289-80c9-c18e0bf4f679"; + String propToChangeKey = "clientName"; + String newValue = "Jozef"; + + OAuthSQLStorage oAuthStorage = (OAuthSQLStorage) StorageLayer.getStorage(process.getProcess()); + + AppIdentifier testApp = new AppIdentifier("", ""); + oAuthStorage.addClientForApp(testApp, clientId); + + JsonObject requestBody = new JsonObject(); + requestBody.addProperty("clientId", clientId); + requestBody.addProperty(propToChangeKey, newValue); + + JsonObject actualResponse = HttpRequestForTesting.sendJsonPATCHRequest(process.getProcess(), + "http://localhost:3567/recipe/oauth/clients", requestBody); + + assertEquals("OK", actualResponse.get("status").getAsString()); + assertTrue(actualResponse.has("client")); + + JsonObject updatedClient = actualResponse.get("client").getAsJsonObject(); + + assertTrue(updatedClient.has(propToChangeKey)); + assertEquals(newValue, updatedClient.get(propToChangeKey).getAsString()); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void testClientUpdatePatch_multipleFields() + throws StorageQueryException, IOException, OAuth2ClientAlreadyExistsForAppException, + io.supertokens.test.httpRequest.HttpResponseException, InterruptedException { + + String[] args = {"../"}; + + this.process = TestingProcessManager.start(args); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + String clientId = "6030f07e-c8ef-4289-80c9-c18e0bf4f679"; + String propToChangeKey = "clientName"; + String newValue = "Jozef2"; + + String listPropToChange = "grantTypes"; + JsonArray newListValue = new JsonArray(); + newListValue.add(new JsonPrimitive("test1")); + newListValue.add(new JsonPrimitive("test2")); + + OAuthSQLStorage oAuthStorage = (OAuthSQLStorage) StorageLayer.getStorage(process.getProcess()); + + AppIdentifier testApp = new AppIdentifier("", ""); + oAuthStorage.addClientForApp(testApp, clientId); + + JsonObject requestBody = new JsonObject(); + requestBody.addProperty("clientId", clientId); + requestBody.addProperty(propToChangeKey, newValue); + requestBody.add(listPropToChange, newListValue); + + JsonObject actualResponse = HttpRequestForTesting.sendJsonPATCHRequest(process.getProcess(), + "http://localhost:3567/recipe/oauth/clients", requestBody); + + assertEquals("OK", actualResponse.get("status").getAsString()); + assertTrue(actualResponse.has("client")); + + JsonObject updatedClient = actualResponse.get("client").getAsJsonObject(); + + assertTrue(updatedClient.has(propToChangeKey)); + assertEquals(newValue, updatedClient.get(propToChangeKey).getAsString()); + + assertTrue(updatedClient.has(listPropToChange)); + assertEquals(newListValue.getAsJsonArray(), updatedClient.get(listPropToChange).getAsJsonArray()); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void testClientUpdatePatch_missingClientIdResultsInError() + throws StorageQueryException, IOException, OAuth2ClientAlreadyExistsForAppException, + io.supertokens.test.httpRequest.HttpResponseException, InterruptedException { + + String[] args = {"../"}; + + this.process = TestingProcessManager.start(args); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + String clientId = "6030f07e-c8ef-4289-80c9-c18e0bf4f679"; + String propToChangeKey = "clientName"; + String newValue = "Jozef2"; + + String listPropToChange = "grantTypes"; + JsonArray newListValue = new JsonArray(); + newListValue.add(new JsonPrimitive("test1")); + newListValue.add(new JsonPrimitive("test2")); + + OAuthSQLStorage oAuthStorage = (OAuthSQLStorage) StorageLayer.getStorage(process.getProcess()); + + AppIdentifier testApp = new AppIdentifier("", ""); + oAuthStorage.addClientForApp(testApp, clientId); + + JsonObject requestBody = new JsonObject(); + //note the missing client Id + requestBody.addProperty(propToChangeKey, newValue); + requestBody.add(listPropToChange, newListValue); + + io.supertokens.test.httpRequest.HttpResponseException expected = assertThrows(io.supertokens.test.httpRequest.HttpResponseException.class, + () -> { HttpRequestForTesting.sendJsonPATCHRequest(process.getProcess(), + "http://localhost:3567/recipe/oauth/clients", requestBody); + }); + + assertEquals(400, expected.statusCode); + assertEquals("Http error. Status Code: 400. Message: Field name `clientId` is missing in JSON input\n", expected.getMessage()); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void testClientUpdatePatch_hydraErrortResultsInError() + throws StorageQueryException, IOException, OAuth2ClientAlreadyExistsForAppException, + io.supertokens.test.httpRequest.HttpResponseException, InterruptedException { + + String[] args = {"../"}; + + this.process = TestingProcessManager.start(args); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + String clientId = "6030f07e-c8ef-4289-80c9-c18e0bf4f679"; + String propToChangeKey = "clientName"; + String newValue = "Jozef2"; + + String notAlistPropToChange = "scope"; + JsonArray newListValue = new JsonArray(); + newListValue.add(new JsonPrimitive("test1")); + newListValue.add(new JsonPrimitive("test2")); + + OAuthSQLStorage oAuthStorage = (OAuthSQLStorage) StorageLayer.getStorage(process.getProcess()); + + AppIdentifier testApp = new AppIdentifier("", ""); + oAuthStorage.addClientForApp(testApp, clientId); + + JsonObject requestBody = new JsonObject(); + requestBody.addProperty("clientId", clientId); + requestBody.addProperty(propToChangeKey, newValue); + requestBody.add(notAlistPropToChange, newListValue); + + HttpResponseException expected = assertThrows(HttpResponseException.class, () -> { + HttpRequestForTesting.sendJsonPATCHRequest(process.getProcess(), + "http://localhost:3567/recipe/oauth/clients", requestBody); + }); + + assertEquals("Http error. Status Code: 500. Message: Internal Error\n", expected.getMessage()); + assertEquals(500, expected.statusCode); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void testClientUpdatePatch_invalidInputDataResultsInError() + throws StorageQueryException, IOException, OAuth2ClientAlreadyExistsForAppException, + io.supertokens.test.httpRequest.HttpResponseException, InterruptedException { + + String[] args = {"../"}; + + this.process = TestingProcessManager.start(args); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + String clientId = "6030f07e-c8ef-4289-80c9-c18e0bf4f679"; + String propToChangeKey = "clientName"; + String newValue = "Jozef2"; + + String notAlistPropToChange = "allowed_cors_origins"; + JsonArray newListValue = new JsonArray(); + newListValue.add(new JsonPrimitive("*")); + newListValue.add(new JsonPrimitive("appleTree")); + + OAuthSQLStorage oAuthStorage = (OAuthSQLStorage) StorageLayer.getStorage(process.getProcess()); + + AppIdentifier testApp = new AppIdentifier("", ""); + oAuthStorage.addClientForApp(testApp, clientId); + + JsonObject requestBody = new JsonObject(); + requestBody.addProperty("clientId", clientId); + requestBody.addProperty(propToChangeKey, newValue); + requestBody.add(notAlistPropToChange, newListValue); + + io.supertokens.test.httpRequest.HttpResponseException expected = assertThrows(io.supertokens.test.httpRequest.HttpResponseException.class, + () -> { HttpRequestForTesting.sendJsonPATCHRequest(process.getProcess(), + "http://localhost:3567/recipe/oauth/clients", requestBody); + }); + + assertEquals(400, expected.statusCode); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + @Test + public void testClientUpdatePatch_notExistingClientResultsInNotFound() + throws StorageQueryException, IOException, OAuth2ClientAlreadyExistsForAppException, + io.supertokens.test.httpRequest.HttpResponseException, InterruptedException { + + String[] args = {"../"}; + + this.process = TestingProcessManager.start(args); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED)); + + String clientId = "6030f07e-c8ef-4289-80c9-c18e0bf4f679-Not_Existing"; + String propToChangeKey = "clientName"; + String newValue = "Jozef2"; + + OAuthSQLStorage oAuthStorage = (OAuthSQLStorage) StorageLayer.getStorage(process.getProcess()); + + AppIdentifier testApp = new AppIdentifier("", ""); + oAuthStorage.addClientForApp(testApp, clientId); // exists at our end, not exists in hydra + + JsonObject requestBody = new JsonObject(); + requestBody.addProperty("clientId", clientId); + requestBody.addProperty(propToChangeKey, newValue); + + JsonObject response = HttpRequestForTesting.sendJsonPATCHRequest(process.getProcess(), + "http://localhost:3567/recipe/oauth/clients", requestBody); + + assertEquals("OAUTH2_CLIENT_NOT_FOUND_ERROR", response.get("status").getAsString()); + + process.kill(); + assertNotNull(process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED)); + } + + }