From 505bf2f5ba8b299de65ac4b379a8f755c562ab3d Mon Sep 17 00:00:00 2001 From: Dominic Byrd-McDevitt Date: Tue, 12 May 2026 22:29:22 -0400 Subject: [PATCH 1/3] Return uniform 200 response for new and existing API key registrations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The /api_key/:email endpoint previously returned HTTP 200 for new accounts and HTTP 409 for existing ones. This difference acts as a user enumeration oracle — a caller can determine whether any email address is registered with DPLA. Return 200 with the same body in both cases to close that gap. --- src/main/scala/dpla/api/Routes.scala | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/src/main/scala/dpla/api/Routes.scala b/src/main/scala/dpla/api/Routes.scala index 5c0f935..1679d9f 100644 --- a/src/main/scala/dpla/api/Routes.scala +++ b/src/main/scala/dpla/api/Routes.scala @@ -410,9 +410,9 @@ class Routes( case SmrArchiveSuccess => complete(smrArchiveSuccessMessage) case NewApiKey(email) => - complete(newKeyMessage(email)) + complete(apiKeyMessage(email)) case ExistingApiKey(email) => - complete(existingKeyResponse(email)) + complete(apiKeyMessage(email)) case DisabledApiKey(email) => complete(disabledKeyResponse(email)) case NotFoundFailure => @@ -506,16 +506,6 @@ class Routes( entity = errorEntity("bad_request", message) ) - private def existingKeyResponse(email: String): HttpResponse = - HttpResponse( - Conflict, - entity = errorEntity( - "existing_key", - s"There is already an API key for $email" + - ". We have sent a reminder message to that address." - ) - ) - private def disabledKeyResponse(email: String): HttpResponse = HttpResponse( Conflict, @@ -527,8 +517,8 @@ class Routes( ) ) - private def newKeyMessage(email: String): String = - s"API key created and sent to $email." + private def apiKeyMessage(email: String): String = + s"Your API key has been sent to $email." private val smrArchiveSuccessMessage: String = s"Your request has been received." From ed1b31360d679012deb54f84628c742e7b6c974d Mon Sep 17 00:00:00 2001 From: Kevin Payravi Date: Wed, 13 May 2026 04:59:58 +0200 Subject: [PATCH 2/3] Update test per new expected behavior --- src/test/scala/dpla/api/v2/endToEnd/PostgresErrorTest.scala | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/test/scala/dpla/api/v2/endToEnd/PostgresErrorTest.scala b/src/test/scala/dpla/api/v2/endToEnd/PostgresErrorTest.scala index 345bd84..fdd5293 100644 --- a/src/test/scala/dpla/api/v2/endToEnd/PostgresErrorTest.scala +++ b/src/test/scala/dpla/api/v2/endToEnd/PostgresErrorTest.scala @@ -44,7 +44,7 @@ class PostgresErrorTest extends AnyWordSpec with Matchers } "/api_key/[email] route" should { - "return Conflict if email has existing api key" in { + "return OK if email has existing api key" in { lazy val routes: Route = new Routes(itemRegistry, pssRegistry, apiKeyRegistryExistingKey, smrRegistry).applicationRoutes @@ -52,8 +52,8 @@ class PostgresErrorTest extends AnyWordSpec with Matchers val request = Post("/v2/api_key/email@example.com") request ~> Route.seal(routes) ~> check { - status shouldEqual StatusCodes.Conflict - contentType should === (ContentTypes.`application/json`) + status shouldEqual StatusCodes.OK + entityAs[String] shouldEqual "Your API key has been sent to email@example.com." } } } From ffc6a311ee105cb3a70d3f0fe2fc3ec5abd27d89 Mon Sep 17 00:00:00 2001 From: Kevin Payravi Date: Wed, 13 May 2026 05:03:34 +0200 Subject: [PATCH 3/3] Mirror real lookup behavior --- .../api/v2/authentication/MockPostgresClientExistingKey.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test/scala/dpla/api/v2/authentication/MockPostgresClientExistingKey.scala b/src/test/scala/dpla/api/v2/authentication/MockPostgresClientExistingKey.scala index dee9a0f..8012410 100644 --- a/src/test/scala/dpla/api/v2/authentication/MockPostgresClientExistingKey.scala +++ b/src/test/scala/dpla/api/v2/authentication/MockPostgresClientExistingKey.scala @@ -22,8 +22,8 @@ object MockPostgresClientExistingKey { case ValidApiKey(_, _) => Behaviors.unhandled - case ValidEmail(_, replyTo) => - replyTo ! AccountFound(account) + case ValidEmail(email, replyTo) => + replyTo ! AccountFound(account.copy(email = email)) Behaviors.same case _ =>