OpenAPI spec for VerificationController endpoints
This commit is contained in:
parent
8280106493
commit
0d412c88fd
|
@ -15,6 +15,13 @@ import io.grpc.StatusRuntimeException;
|
||||||
import io.micrometer.core.instrument.Metrics;
|
import io.micrometer.core.instrument.Metrics;
|
||||||
import io.micrometer.core.instrument.Tag;
|
import io.micrometer.core.instrument.Tag;
|
||||||
import io.micrometer.core.instrument.Tags;
|
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.Valid;
|
||||||
import jakarta.validation.constraints.NotNull;
|
import jakarta.validation.constraints.NotNull;
|
||||||
import jakarta.ws.rs.BadRequestException;
|
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.entities.VerificationSessionResponse;
|
||||||
import org.whispersystems.textsecuregcm.filters.RemoteAddressFilter;
|
import org.whispersystems.textsecuregcm.filters.RemoteAddressFilter;
|
||||||
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
||||||
|
import org.whispersystems.textsecuregcm.mappers.RegistrationServiceSenderExceptionMapper;
|
||||||
import org.whispersystems.textsecuregcm.metrics.UserAgentTagUtil;
|
import org.whispersystems.textsecuregcm.metrics.UserAgentTagUtil;
|
||||||
import org.whispersystems.textsecuregcm.push.PushNotification;
|
import org.whispersystems.textsecuregcm.push.PushNotification;
|
||||||
import org.whispersystems.textsecuregcm.push.PushNotificationManager;
|
import org.whispersystems.textsecuregcm.push.PushNotificationManager;
|
||||||
|
@ -152,7 +160,19 @@ public class VerificationController {
|
||||||
@Path("/session")
|
@Path("/session")
|
||||||
@Consumes(MediaType.APPLICATION_JSON)
|
@Consumes(MediaType.APPLICATION_JSON)
|
||||||
@Produces(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 {
|
throws RateLimitExceededException {
|
||||||
|
|
||||||
final Pair<String, PushNotification.TokenType> pushTokenAndType = validateAndExtractPushToken(
|
final Pair<String, PushNotification.TokenType> pushTokenAndType = validateAndExtractPushToken(
|
||||||
|
@ -200,12 +220,28 @@ public class VerificationController {
|
||||||
@Path("/session/{sessionId}")
|
@Path("/session/{sessionId}")
|
||||||
@Consumes(MediaType.APPLICATION_JSON)
|
@Consumes(MediaType.APPLICATION_JSON)
|
||||||
@Produces(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(
|
public VerificationSessionResponse updateSession(
|
||||||
@PathParam("sessionId") final String encodedSessionId,
|
@PathParam("sessionId") final String encodedSessionId,
|
||||||
@HeaderParam(HttpHeaders.USER_AGENT) final String userAgent,
|
@HeaderParam(HttpHeaders.USER_AGENT) final String userAgent,
|
||||||
@Context ContainerRequestContext requestContext,
|
@Context final ContainerRequestContext requestContext,
|
||||||
@NotNull @Valid final UpdateVerificationSessionRequest updateVerificationSessionRequest,
|
@NotNull @Valid final UpdateVerificationSessionRequest updateVerificationSessionRequest) {
|
||||||
@Context ContainerRequestContext context) {
|
|
||||||
|
|
||||||
final String sourceHost = (String) requestContext.getProperty(RemoteAddressFilter.REMOTE_ADDRESS_ATTRIBUTE_NAME);
|
final String sourceHost = (String) requestContext.getProperty(RemoteAddressFilter.REMOTE_ADDRESS_ATTRIBUTE_NAME);
|
||||||
|
|
||||||
|
@ -216,7 +252,7 @@ public class VerificationController {
|
||||||
VerificationSession verificationSession = retrieveVerificationSession(registrationServiceSession);
|
VerificationSession verificationSession = retrieveVerificationSession(registrationServiceSession);
|
||||||
|
|
||||||
final VerificationCheck verificationCheck = registrationFraudChecker.checkVerificationAttempt(
|
final VerificationCheck verificationCheck = registrationFraudChecker.checkVerificationAttempt(
|
||||||
context,
|
requestContext,
|
||||||
verificationSession,
|
verificationSession,
|
||||||
registrationServiceSession.number(),
|
registrationServiceSession.number(),
|
||||||
updateVerificationSessionRequest);
|
updateVerificationSessionRequest);
|
||||||
|
@ -433,6 +469,15 @@ public class VerificationController {
|
||||||
@GET
|
@GET
|
||||||
@Path("/session/{sessionId}")
|
@Path("/session/{sessionId}")
|
||||||
@Produces(MediaType.APPLICATION_JSON)
|
@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) {
|
public VerificationSessionResponse getSession(@PathParam("sessionId") final String encodedSessionId) {
|
||||||
|
|
||||||
final RegistrationServiceSession registrationServiceSession = retrieveRegistrationServiceSession(encodedSessionId);
|
final RegistrationServiceSession registrationServiceSession = retrieveRegistrationServiceSession(encodedSessionId);
|
||||||
|
@ -445,10 +490,45 @@ public class VerificationController {
|
||||||
@Path("/session/{sessionId}/code")
|
@Path("/session/{sessionId}/code")
|
||||||
@Consumes(MediaType.APPLICATION_JSON)
|
@Consumes(MediaType.APPLICATION_JSON)
|
||||||
@Produces(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,
|
public VerificationSessionResponse requestVerificationCode(@PathParam("sessionId") final String encodedSessionId,
|
||||||
@HeaderParam(HttpHeaders.USER_AGENT) final String userAgent,
|
@HeaderParam(HttpHeaders.USER_AGENT) final String userAgent,
|
||||||
@HeaderParam(HttpHeaders.ACCEPT_LANGUAGE) Optional<String> acceptLanguage,
|
@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)
|
||||||
@NotNull @Valid VerificationCodeRequest verificationCodeRequest) throws Throwable {
|
final Optional<String> acceptLanguage,
|
||||||
|
@NotNull @Valid final VerificationCodeRequest verificationCodeRequest) throws Throwable {
|
||||||
|
|
||||||
final RegistrationServiceSession registrationServiceSession = retrieveRegistrationServiceSession(encodedSessionId);
|
final RegistrationServiceSession registrationServiceSession = retrieveRegistrationServiceSession(encodedSessionId);
|
||||||
final VerificationSession verificationSession = retrieveVerificationSession(registrationServiceSession);
|
final VerificationSession verificationSession = retrieveVerificationSession(registrationServiceSession);
|
||||||
|
@ -550,8 +630,31 @@ public class VerificationController {
|
||||||
@Path("/session/{sessionId}/code")
|
@Path("/session/{sessionId}/code")
|
||||||
@Consumes(MediaType.APPLICATION_JSON)
|
@Consumes(MediaType.APPLICATION_JSON)
|
||||||
@Produces(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,
|
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)
|
@NotNull @Valid final SubmitVerificationCodeRequest submitVerificationCodeRequest)
|
||||||
throws RateLimitExceededException {
|
throws RateLimitExceededException {
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,7 @@ package org.whispersystems.textsecuregcm.entities;
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
import com.fasterxml.jackson.annotation.JsonUnwrapped;
|
import com.fasterxml.jackson.annotation.JsonUnwrapped;
|
||||||
import com.google.common.annotations.VisibleForTesting;
|
import com.google.common.annotations.VisibleForTesting;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import jakarta.validation.Valid;
|
import jakarta.validation.Valid;
|
||||||
import jakarta.validation.constraints.NotBlank;
|
import jakarta.validation.constraints.NotBlank;
|
||||||
import org.whispersystems.textsecuregcm.util.E164;
|
import org.whispersystems.textsecuregcm.util.E164;
|
||||||
|
@ -16,6 +17,7 @@ import org.whispersystems.textsecuregcm.util.E164;
|
||||||
// https://github.com/FasterXML/jackson-databind/issues/1497
|
// https://github.com/FasterXML/jackson-databind/issues/1497
|
||||||
public final class CreateVerificationSessionRequest {
|
public final class CreateVerificationSessionRequest {
|
||||||
|
|
||||||
|
@Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "The e164-formatted phone number to be verified")
|
||||||
@E164
|
@E164
|
||||||
@NotBlank
|
@NotBlank
|
||||||
@JsonProperty
|
@JsonProperty
|
||||||
|
|
|
@ -7,13 +7,25 @@ package org.whispersystems.textsecuregcm.entities;
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import org.whispersystems.textsecuregcm.push.PushNotification;
|
import org.whispersystems.textsecuregcm.push.PushNotification;
|
||||||
|
|
||||||
public record UpdateVerificationSessionRequest(@Nullable String pushToken,
|
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,
|
@Nullable PushTokenType pushTokenType,
|
||||||
|
|
||||||
|
@Schema(requiredMode = Schema.RequiredMode.NOT_REQUIRED, description = "Value received by the device in the push challenge")
|
||||||
@Nullable String pushChallenge,
|
@Nullable String pushChallenge,
|
||||||
|
|
||||||
|
@Schema(requiredMode = Schema.RequiredMode.NOT_REQUIRED, description = "Captcha token returned after solving a captcha challenge")
|
||||||
@Nullable String captcha,
|
@Nullable String captcha,
|
||||||
|
|
||||||
|
@Schema(requiredMode = Schema.RequiredMode.NOT_REQUIRED, description = "Mobile country code of the phone subscriber")
|
||||||
@Nullable String mcc,
|
@Nullable String mcc,
|
||||||
|
|
||||||
|
@Schema(requiredMode = Schema.RequiredMode.NOT_REQUIRED, description = "Mobile network code of the phone subscriber")
|
||||||
@Nullable String mnc) {
|
@Nullable String mnc) {
|
||||||
|
|
||||||
public enum PushTokenType {
|
public enum PushTokenType {
|
||||||
|
|
|
@ -6,10 +6,15 @@
|
||||||
package org.whispersystems.textsecuregcm.entities;
|
package org.whispersystems.textsecuregcm.entities;
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import jakarta.validation.constraints.NotNull;
|
import jakarta.validation.constraints.NotNull;
|
||||||
import org.whispersystems.textsecuregcm.registration.MessageTransport;
|
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 {
|
public enum Transport {
|
||||||
@JsonProperty("sms")
|
@JsonProperty("sms")
|
||||||
|
|
|
@ -7,11 +7,29 @@ package org.whispersystems.textsecuregcm.entities;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import org.whispersystems.textsecuregcm.registration.VerificationSession;
|
import org.whispersystems.textsecuregcm.registration.VerificationSession;
|
||||||
|
|
||||||
public record VerificationSessionResponse(String id, @Nullable Long nextSms, @Nullable Long nextCall,
|
public record VerificationSessionResponse(
|
||||||
@Nullable Long nextVerificationAttempt, boolean allowedToRequestCode,
|
@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<VerificationSession.Information> requestedInformation,
|
List<VerificationSession.Information> requestedInformation,
|
||||||
|
|
||||||
|
@Schema(requiredMode = Schema.RequiredMode.REQUIRED, description = "Whether this session is verified")
|
||||||
boolean verified) {
|
boolean verified) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue