From 0d412c88fd74c3353fa6ff8131863851646fd6e5 Mon Sep 17 00:00:00 2001 From: Ameya Lokare Date: Tue, 17 Dec 2024 15:24:41 -0800 Subject: [PATCH] OpenAPI spec for VerificationController endpoints --- .../controllers/VerificationController.java | 119 ++++++++++++++++-- .../CreateVerificationSessionRequest.java | 2 + .../UpdateVerificationSessionRequest.java | 24 +++- .../entities/VerificationCodeRequest.java | 7 +- .../entities/VerificationSessionResponse.java | 26 +++- 5 files changed, 159 insertions(+), 19 deletions(-) diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/VerificationController.java b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/VerificationController.java index 0b8e02be3..07cb66b15 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/controllers/VerificationController.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/controllers/VerificationController.java @@ -15,6 +15,13 @@ import io.grpc.StatusRuntimeException; import io.micrometer.core.instrument.Metrics; import io.micrometer.core.instrument.Tag; import io.micrometer.core.instrument.Tags; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.enums.ParameterIn; +import io.swagger.v3.oas.annotations.headers.Header; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; import jakarta.validation.Valid; import jakarta.validation.constraints.NotNull; import jakarta.ws.rs.BadRequestException; @@ -68,6 +75,7 @@ import org.whispersystems.textsecuregcm.entities.VerificationCodeRequest; import org.whispersystems.textsecuregcm.entities.VerificationSessionResponse; import org.whispersystems.textsecuregcm.filters.RemoteAddressFilter; import org.whispersystems.textsecuregcm.limits.RateLimiters; +import org.whispersystems.textsecuregcm.mappers.RegistrationServiceSenderExceptionMapper; import org.whispersystems.textsecuregcm.metrics.UserAgentTagUtil; import org.whispersystems.textsecuregcm.push.PushNotification; import org.whispersystems.textsecuregcm.push.PushNotificationManager; @@ -152,7 +160,19 @@ public class VerificationController { @Path("/session") @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) - public VerificationSessionResponse createSession(@NotNull @Valid CreateVerificationSessionRequest request) + @Operation( + summary = "Creates a new verification session for a specific phone number", + description = """ + Initiates a session to be able to verify the phone number for account registration. Check the response and + submit requested information at PATCH /session/{sessionId} + """) + @ApiResponse(responseCode = "200", description = "The verification session was created successfully", useReturnTypeSchema = true) + @ApiResponse(responseCode = "422", description = "The request did not pass validation") + @ApiResponse(responseCode = "429", description = "Too many attempts", headers = @Header( + name = "Retry-After", + description = "If present, an positive integer indicating the number of seconds before a subsequent attempt could succeed", + schema = @Schema(implementation = Integer.class))) + public VerificationSessionResponse createSession(@NotNull @Valid final CreateVerificationSessionRequest request) throws RateLimitExceededException { final Pair pushTokenAndType = validateAndExtractPushToken( @@ -200,12 +220,28 @@ public class VerificationController { @Path("/session/{sessionId}") @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) + @Operation( + summary = "Update a registration verification session", + description = """ + Updates the session with requested information like an answer to a push challenge or captcha. + If `requestedInformation` in the response is empty, and `allowedToRequestCode` is `true`, proceed to call + `POST /session/{sessionId}/code`. If `requestedInformation` is empty and `allowedToRequestCode` is `false`, + then the caller must create a new verification session. + """) + @ApiResponse(responseCode = "200", description = "Session was updated successfully with the information provided", useReturnTypeSchema = true) + @ApiResponse(responseCode = "403", description = "The information provided was not accepted (e.g push challenge or captcha verification failed)") + @ApiResponse(responseCode = "422", description = "The request did not pass validation") + @ApiResponse(responseCode = "429", description = "Too many attempts", + content = @Content(schema = @Schema(implementation = VerificationSessionResponse.class)), + headers = @Header( + name = "Retry-After", + description = "If present, an positive integer indicating the number of seconds before a subsequent attempt could succeed", + schema = @Schema(implementation = Integer.class))) public VerificationSessionResponse updateSession( @PathParam("sessionId") final String encodedSessionId, @HeaderParam(HttpHeaders.USER_AGENT) final String userAgent, - @Context ContainerRequestContext requestContext, - @NotNull @Valid final UpdateVerificationSessionRequest updateVerificationSessionRequest, - @Context ContainerRequestContext context) { + @Context final ContainerRequestContext requestContext, + @NotNull @Valid final UpdateVerificationSessionRequest updateVerificationSessionRequest) { final String sourceHost = (String) requestContext.getProperty(RemoteAddressFilter.REMOTE_ADDRESS_ATTRIBUTE_NAME); @@ -216,7 +252,7 @@ public class VerificationController { VerificationSession verificationSession = retrieveVerificationSession(registrationServiceSession); final VerificationCheck verificationCheck = registrationFraudChecker.checkVerificationAttempt( - context, + requestContext, verificationSession, registrationServiceSession.number(), updateVerificationSessionRequest); @@ -433,6 +469,15 @@ public class VerificationController { @GET @Path("/session/{sessionId}") @Produces(MediaType.APPLICATION_JSON) + @Operation( + summary = "Get a registration verification session", + description = """ + Retrieve metadata of the registration verification session with the specified ID + """) + @ApiResponse(responseCode = "200", description = "Session was retrieved successfully", useReturnTypeSchema = true) + @ApiResponse(responseCode = "400", description = "Invalid session ID") + @ApiResponse(responseCode = "404", description = "Session with the specified ID could not be found") + @ApiResponse(responseCode = "422", description = "Malformed session ID encoding") public VerificationSessionResponse getSession(@PathParam("sessionId") final String encodedSessionId) { final RegistrationServiceSession registrationServiceSession = retrieveRegistrationServiceSession(encodedSessionId); @@ -445,10 +490,45 @@ public class VerificationController { @Path("/session/{sessionId}/code") @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) + @Operation( + summary = "Request a verification code", + description = """ + Sends a verification code to the phone number associated with the specified session via SMS or phone call. + This endpoint can only be called when the session metadata includes "allowedToRequestCode = true" + """) + @ApiResponse(responseCode = "200", description = "Verification code was successfully sent", useReturnTypeSchema = true) + @ApiResponse(responseCode = "400", description = "Invalid session ID") + @ApiResponse(responseCode = "404", description = "Session with the specified ID could not be found") + @ApiResponse(responseCode = "409", description = "The session is already verified or not in a state to request a code because requested information hasn't been provided yet", + content = @Content(schema = @Schema(implementation = VerificationSessionResponse.class))) + @ApiResponse(responseCode = "418", description = "The request to send a verification code with the given transport could not be fulfilled, but may succeed with a different transport", + content = @Content(schema = @Schema(implementation = VerificationSessionResponse.class))) + @ApiResponse(responseCode = "422", description = "Request did not pass validation") + @ApiResponse(responseCode = "429", description = """ + Too may attempts; the caller is not permitted to send a verification code via the requested channel at this time + and may need to wait before trying again; if the session metadata does not specify a time at which the caller may + try again, then the caller has exhausted their permitted attempts and must either try a different transport or + create a new verification session. + """, + content = @Content(schema = @Schema(implementation = VerificationSessionResponse.class)), + headers = @Header( + name = "Retry-After", + description = "If present, an positive integer indicating the number of seconds before a subsequent attempt could succeed", + schema = @Schema(implementation = Integer.class) + )) + @ApiResponse(responseCode = "440", description = """ + The attempt to send a verification code failed because an external service (e.g. the SMS provider) refused to + deliver the code. This may be a temporary or permanent failure, as indicated in the response body. If temporary, + clients may try again after a reasonable delay. If permanent, clients should not retry the request and should + communicate the permanent failure to the end user. Permanent failures may result in the server disallowing all + future attempts to request or submit verification codes (since those attempts would be all but guaranteed to fail). + """, + content = @Content(schema = @Schema(implementation = RegistrationServiceSenderExceptionMapper.SendVerificationCodeFailureResponse.class))) public VerificationSessionResponse requestVerificationCode(@PathParam("sessionId") final String encodedSessionId, @HeaderParam(HttpHeaders.USER_AGENT) final String userAgent, - @HeaderParam(HttpHeaders.ACCEPT_LANGUAGE) Optional acceptLanguage, - @NotNull @Valid VerificationCodeRequest verificationCodeRequest) throws Throwable { + @Parameter(in = ParameterIn.HEADER, description = "Ordered list of languages in which the client prefers to receive SMS or voice verification messages") @HeaderParam(HttpHeaders.ACCEPT_LANGUAGE) + final Optional acceptLanguage, + @NotNull @Valid final VerificationCodeRequest verificationCodeRequest) throws Throwable { final RegistrationServiceSession registrationServiceSession = retrieveRegistrationServiceSession(encodedSessionId); final VerificationSession verificationSession = retrieveVerificationSession(registrationServiceSession); @@ -550,8 +630,31 @@ public class VerificationController { @Path("/session/{sessionId}/code") @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) + @Operation( + summary = "Submit a verification code", + description = """ + Submits a verification code received via SMS or voice for verification + """) + @ApiResponse(responseCode = "200", description = """ + The request to check a verification code was processed (though the submitted code may not be the correct code); + the session metadata will indicate whether the submitted code was correct + """, useReturnTypeSchema = true) + @ApiResponse(responseCode = "400", description = "Invalid session ID or verification code") + @ApiResponse(responseCode = "404", description = "Session with the specified ID could not be found") + @ApiResponse(responseCode = "409", description = "The session is already verified or no code has been requested yet for this session", + content = @Content(schema = @Schema(implementation = VerificationSessionResponse.class))) + @ApiResponse(responseCode = "429", description = """ + Too many attempts; the caller is not permitted to submit a verification code at this time and may need to wait + before trying again; if the session metadata does not specify a time at which the caller may try again, then the + caller has exhausted their permitted attempts and must create a new verification session. + """, + content = @Content(schema = @Schema(implementation = VerificationSessionResponse.class)), + headers = @Header( + name = "Retry-After", + description = "If present, an positive integer indicating the number of seconds before a subsequent attempt could succeed", + schema = @Schema(implementation = Integer.class))) public VerificationSessionResponse verifyCode(@PathParam("sessionId") final String encodedSessionId, - @HeaderParam(HttpHeaders.USER_AGENT) String userAgent, + @HeaderParam(HttpHeaders.USER_AGENT) final String userAgent, @NotNull @Valid final SubmitVerificationCodeRequest submitVerificationCodeRequest) throws RateLimitExceededException { diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/entities/CreateVerificationSessionRequest.java b/service/src/main/java/org/whispersystems/textsecuregcm/entities/CreateVerificationSessionRequest.java index 57da5049a..09abf785e 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/entities/CreateVerificationSessionRequest.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/entities/CreateVerificationSessionRequest.java @@ -8,6 +8,7 @@ package org.whispersystems.textsecuregcm.entities; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonUnwrapped; import com.google.common.annotations.VisibleForTesting; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.Valid; import jakarta.validation.constraints.NotBlank; import org.whispersystems.textsecuregcm.util.E164; @@ -16,6 +17,7 @@ import org.whispersystems.textsecuregcm.util.E164; // https://github.com/FasterXML/jackson-databind/issues/1497 public final class CreateVerificationSessionRequest { + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "The e164-formatted phone number to be verified") @E164 @NotBlank @JsonProperty diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/entities/UpdateVerificationSessionRequest.java b/service/src/main/java/org/whispersystems/textsecuregcm/entities/UpdateVerificationSessionRequest.java index aca03d9ec..9cafb690b 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/entities/UpdateVerificationSessionRequest.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/entities/UpdateVerificationSessionRequest.java @@ -7,14 +7,26 @@ package org.whispersystems.textsecuregcm.entities; import com.fasterxml.jackson.annotation.JsonProperty; import javax.annotation.Nullable; +import io.swagger.v3.oas.annotations.media.Schema; import org.whispersystems.textsecuregcm.push.PushNotification; -public record UpdateVerificationSessionRequest(@Nullable String pushToken, - @Nullable PushTokenType pushTokenType, - @Nullable String pushChallenge, - @Nullable String captcha, - @Nullable String mcc, - @Nullable String mnc) { +public record UpdateVerificationSessionRequest( + @Schema(requiredMode = Schema.RequiredMode.NOT_REQUIRED, description = "The APNs or FCM device token to which a push challenge can be sent") + @Nullable String pushToken, + @Schema(requiredMode = Schema.RequiredMode.NOT_REQUIRED, description = "The type of push token") + @Nullable PushTokenType pushTokenType, + + @Schema(requiredMode = Schema.RequiredMode.NOT_REQUIRED, description = "Value received by the device in the push challenge") + @Nullable String pushChallenge, + + @Schema(requiredMode = Schema.RequiredMode.NOT_REQUIRED, description = "Captcha token returned after solving a captcha challenge") + @Nullable String captcha, + + @Schema(requiredMode = Schema.RequiredMode.NOT_REQUIRED, description = "Mobile country code of the phone subscriber") + @Nullable String mcc, + + @Schema(requiredMode = Schema.RequiredMode.NOT_REQUIRED, description = "Mobile network code of the phone subscriber") + @Nullable String mnc) { public enum PushTokenType { @JsonProperty("apn") diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/entities/VerificationCodeRequest.java b/service/src/main/java/org/whispersystems/textsecuregcm/entities/VerificationCodeRequest.java index dcbcec481..84c08d4d7 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/entities/VerificationCodeRequest.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/entities/VerificationCodeRequest.java @@ -6,10 +6,15 @@ package org.whispersystems.textsecuregcm.entities; import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; import org.whispersystems.textsecuregcm.registration.MessageTransport; -public record VerificationCodeRequest(@NotNull Transport transport, @NotNull String client) { +public record VerificationCodeRequest(@Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "Transport via which to send the verification code") + @NotNull Transport transport, + + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "Client type to facilitate platform-specific SMS verification") + @NotNull String client) { public enum Transport { @JsonProperty("sms") diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/entities/VerificationSessionResponse.java b/service/src/main/java/org/whispersystems/textsecuregcm/entities/VerificationSessionResponse.java index a3caaf972..a6990ed83 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/entities/VerificationSessionResponse.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/entities/VerificationSessionResponse.java @@ -7,11 +7,29 @@ package org.whispersystems.textsecuregcm.entities; import java.util.List; import javax.annotation.Nullable; +import io.swagger.v3.oas.annotations.media.Schema; import org.whispersystems.textsecuregcm.registration.VerificationSession; -public record VerificationSessionResponse(String id, @Nullable Long nextSms, @Nullable Long nextCall, - @Nullable Long nextVerificationAttempt, boolean allowedToRequestCode, - List requestedInformation, - boolean verified) { +public record VerificationSessionResponse( + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "A URL-safe ID for the session") + String id, + + @Schema(requiredMode = Schema.RequiredMode.NOT_REQUIRED, description = "Duration in seconds after which next SMS can be requested for this session") + @Nullable Long nextSms, + + @Schema(requiredMode = Schema.RequiredMode.NOT_REQUIRED, description = "Duration in seconds after which next voice call can be requested for this session") + @Nullable Long nextCall, + + @Schema(requiredMode = Schema.RequiredMode.NOT_REQUIRED, description = "Duration in seconds after which the client can submit a verification code for this session") + @Nullable Long nextVerificationAttempt, + + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "Whether it is allowed to request a verification code for this session") + boolean allowedToRequestCode, + + @Schema(description = "A list of requested information that the client needs to submit before requesting code delivery") + List requestedInformation, + + @Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "Whether this session is verified") + boolean verified) { }