trusona/trusona-server-sdk-java

View on GitHub
trusona-sdk/src/main/java/com/trusona/sdk/Trusona.java

Summary

Maintainability
A
1 hr
Test Coverage
C
73%
package com.trusona.sdk;

import java.net.URL;
import java.time.Duration;
import java.util.List;
import java.util.UUID;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.trusona.sdk.config.JacksonConfig;
import com.trusona.sdk.http.ApiCredentials;
import com.trusona.sdk.http.CallHandler;
import com.trusona.sdk.http.ErrorHandler;
import com.trusona.sdk.http.GenericErrorHandler;
import com.trusona.sdk.http.ServiceGenerator;
import com.trusona.sdk.http.client.DevicesClient;
import com.trusona.sdk.http.client.TrusonaficationClient;
import com.trusona.sdk.http.client.UsersClient;
import com.trusona.sdk.http.client.security.DefaultHmacSignatureGenerator;
import com.trusona.sdk.http.client.security.ParsedToken;
import com.trusona.sdk.http.client.v2.service.IdentityDocumentService;
import com.trusona.sdk.http.client.v2.service.TruCodeService;
import com.trusona.sdk.http.client.v2.service.UserDeviceService;
import com.trusona.sdk.http.environment.CustomEnvironment;
import com.trusona.sdk.resources.DevicesApi;
import com.trusona.sdk.resources.TrusonaApi;
import com.trusona.sdk.resources.TrusonaficationApi;
import com.trusona.sdk.resources.UsersApi;
import com.trusona.sdk.resources.dto.Device;
import com.trusona.sdk.resources.dto.IdentityDocument;
import com.trusona.sdk.resources.dto.TruCode;
import com.trusona.sdk.resources.dto.Trusonafication;
import com.trusona.sdk.resources.dto.TrusonaficationResult;
import com.trusona.sdk.resources.dto.TrusonaficationStatus;
import com.trusona.sdk.resources.dto.UserDevice;
import com.trusona.sdk.resources.dto.WebSdkConfig;
import com.trusona.sdk.resources.exception.DeviceAlreadyBoundException;
import com.trusona.sdk.resources.exception.DeviceNotFoundException;
import com.trusona.sdk.resources.exception.NoIdentityDocumentsException;
import com.trusona.sdk.resources.exception.TrusonaException;
import com.trusona.sdk.resources.exception.ValidationException;
import org.apache.commons.lang3.ThreadUtils;
import org.apache.commons.lang3.time.StopWatch;

import static org.apache.commons.lang3.Validate.notEmpty;

/**
 * The main class to interact with Trusona.
 */
public class Trusona implements TrusonaApi {

  private final ServiceGenerator serviceGenerator;
  private final ApiCredentials apiCredentials;
  private final ErrorHandler genericErrorHandler;
  private final TrusonaficationApi trusonaficationApi;
  private final DevicesApi devicesApi;
  private final UsersApi usersApi;

  Duration pollingInterval = Duration.ofSeconds(5);


  Trusona(ServiceGenerator serviceGenerator, ApiCredentials apiCredentials,
          TrusonaficationApi trusonaficationApi, DevicesApi devicesApi, UsersApi usersApi) {

    this.serviceGenerator = serviceGenerator;
    this.apiCredentials = apiCredentials;
    this.trusonaficationApi = trusonaficationApi;
    this.devicesApi = devicesApi;
    this.usersApi = usersApi;

    this.genericErrorHandler = new GenericErrorHandler();
  }

  //TODO: Remove these intermediary constructors after everything is delegated to clients.
  Trusona(ServiceGenerator sg, ApiCredentials apiCredentials) {
    this(sg, apiCredentials, new TrusonaficationClient(sg), new DevicesClient(sg),
      new UsersClient(sg));
  }

  Trusona(TrusonaEnvironment environment, ApiCredentials apiCredentials) {
    this(ServiceGenerator.create(
        EnvironmentFactory.getEnvironment(environment),
        apiCredentials,
        new DefaultHmacSignatureGenerator()),
      apiCredentials);
  }

  Trusona(CustomEnvironment environment, ApiCredentials apiCredentials) {
    this(ServiceGenerator.create(
        environment,
        apiCredentials,
        new DefaultHmacSignatureGenerator()),
      apiCredentials);
  }

  /**
   * Create a Trusona object using an endpoint URL
   *
   * @param token    Your API token, provided by Trusona
   * @param secret   Your API secret, provided by Trusona
   * @param endpoint The Trusona endpoint URL you would like to point at
   */
  public Trusona(String token, String secret, URL endpoint) {
    this(EnvironmentFactory.getCustomEnvironment(endpoint), new ApiCredentials(token, secret));
  }

  /**
   * Create a Trusona object using a TrusonaEnvironment
   *
   * @param token       Your API token, provided by Trusona
   * @param secret      Your API secret, provided by Trusona
   * @param environment The environment you would like to point at
   */
  public Trusona(String token, String secret, TrusonaEnvironment environment) {
    this(environment, new ApiCredentials(token, secret));
  }

  /**
   * Create a Trusona object
   *
   * @param token  Your API token, provided by Trusona
   * @param secret Your API secret, provided by Trusona
   */
  public Trusona(String token, String secret) {
    this(token, secret, TrusonaEnvironment.PRODUCTION);
  }


  private UserDeviceService getUserDeviceService() {
    return serviceGenerator.getService(UserDeviceService.class);
  }

  private TruCodeService getTruCodeService() {
    return serviceGenerator.getService(TruCodeService.class);
  }

  private IdentityDocumentService getIdentityDocumentService() {
    return serviceGenerator.getService(IdentityDocumentService.class);
  }

  /**
   * Create a user device binding in Trusona between a User and a Device, referenced by their
   * identifiers. After creation, the binding will be inactive, and must be explicitly activated
   * before the User can use the Device to complete Trusonafications.
   *
   * @param userIdentifier   A unique identifier for the user. This will be how Trusona references
   *                         your users, and should not change.
   * @param deviceIdentifier A unique identifier for the device. This identifier is generated by the
   *                         Trusona mobile SDKs
   * @return a {@link UserDevice} object that represents the current state of the user device
   * binding.
   * @throws DeviceNotFoundException     If the deviceIdentifier hasn't been registered with
   *                                     Trusona.
   * @throws DeviceAlreadyBoundException If the device is already bound to a different user.
   * @throws ValidationException         If the userIdentifier or deviceIdentifier params are
   *                                     blank.
   * @throws TrusonaException            If there was a problem processing the API request.
   */
  @Override
  public UserDevice createUserDevice(String userIdentifier, String deviceIdentifier)
    throws DeviceNotFoundException, DeviceAlreadyBoundException, ValidationException, TrusonaException {
    UserDevice request = new UserDevice();
    request.setDeviceIdentifier(deviceIdentifier);
    request.setUserIdentifier(userIdentifier);

    ErrorHandler createUserDeviceErrorHandler = response -> {
      switch (response.code()) {
        case 409:
          throw new DeviceAlreadyBoundException(
            "A different user has already been bound to this device.");
        case 424:
          throw new DeviceNotFoundException(
            "The device you are attempting to bind to a user does not exist. " +
              "The device will need to be re-registered with Trusona before attempting to bind it again.");
      }
    };

    return new CallHandler<>(getUserDeviceService().createUserDevice(request))
      .handle(createUserDeviceErrorHandler, genericErrorHandler);
  }

  /**
   * Activates a user device binding in Trusona. After a binding is active, a user can respond to
   * Trusonafications. Only call this method after you have verified the identity of the user.
   *
   * @param activationCode The activation code from {@link Trusona#createUserDevice(String,
   *                       String)}
   * @return true if the device is activated
   * @throws DeviceNotFoundException If the activationCode provided couldn't be matched up with a
   *                                 device.
   * @throws ValidationException     If the activationCode is blank.
   * @throws TrusonaException        If there was a problem processing the API request.
   */
  @Override
  public boolean activateUserDevice(String activationCode)
    throws DeviceNotFoundException, ValidationException, TrusonaException {
    UserDevice request = new UserDevice();
    request.setActive(true);

    ErrorHandler activeErrorHandler = response -> {
      if (response.code() == 404) {
        throw new DeviceNotFoundException(
          "The device you are attempting to activate does not exist. " +
            "You will need to re-register the device and re-bind it to the user to get a new activation code.");
      }
    };

    return new CallHandler<>(getUserDeviceService().updateUserDevice(activationCode, request))
      .handle(activeErrorHandler, genericErrorHandler).isActive();
  }

  /**
   * Creates a Trusonafication and returns a TrusonaficationResult with the current status of the
   * Trusonafication. See {@link TrusonaficationStatus} for the possible statuses.
   *
   * @param trusonafication The Trusonafication to create
   * @return A TrusonaficationResult describing the current status of the Trusonafication
   * @throws NoIdentityDocumentsException If an executive Trusonafication was requested for a user
   *                                      with no identity documents
   * @throws TrusonaException             If there was a problem processing the API request.
   */
  @Override
  public TrusonaficationResult createTrusonafication(Trusonafication trusonafication)
    throws TrusonaException {
    return trusonaficationApi.createTrusonafication(trusonafication);
  }

  /**
   * Gets a TrusonaficationResult for a given Trusonafication ID. This will block until the
   * Trusonafication is no longer IN_PROGRESS
   *
   * @param trusonaficationId The ID of the trusonafication if it exists, or null
   * @return A TrusonaficationResult for the given Trusonafication.
   * @throws TrusonaException If there was a problem processing the API request, or polling was
   *                          interrupted.
   */
  @Override
  public TrusonaficationResult getTrusonaficationResult(UUID trusonaficationId)
    throws TrusonaException {
    return trusonaficationApi.getTrusonaficationResult(trusonaficationId);
  }

  @Override
  public Void cancelTrusonafication(UUID trusonaficationId) throws TrusonaException {
    return trusonaficationApi.cancelTrusonafication(trusonaficationId);
  }

  /**
   * Get the configuration for the Web SDK. Include this in your Javascript code for rendering
   * Trucodes.
   *
   * @return The Web SDK config serialized to JSON
   * @throws TrusonaException If the provided access token is invalid
   */
  @Override
  public String getWebSdkConfig() throws TrusonaException {
    ParsedToken parsedToken = apiCredentials.getParsedToken();
    if (parsedToken == null) {
      throw new TrusonaException(
        "The provided access token is invalid. Please check your configuration");
    }

    WebSdkConfig config = new WebSdkConfig(serviceGenerator.getBaseUrl(), parsedToken.getSubject());
    try {
      return JacksonConfig.getObjectMapper().writeValueAsString(config);
    } catch (JsonProcessingException e) {
      throw new TrusonaException(
        "Could not serializer Web SDK config. Contact Trusona to determine the exact cause", e);
    }
  }

  /**
   * Get a TruCode that has been paired.
   *
   * @param id The ID of the TruCode
   * @return the paired TruCode if it exists, or null
   * @throws TrusonaException If any network or server errors occur.
   */
  @Override
  public TruCode getPairedTruCode(UUID id) throws TrusonaException {
    return new CallHandler<>(getTruCodeService().getPairedTrucode(id))
      .handle(genericErrorHandler);
  }

  /**
   * Continuously look for a TruCode that has been paired.
   *
   * @param id      The ID of the TruCode
   * @param timeout The amount of time in milliseconds to look for the TruCode
   * @return the paired TruCode if it exists, or null
   * @throws TrusonaException If any network or server errors occur.
   */
  @Override
  public TruCode getPairedTruCode(UUID id, Long timeout) throws TrusonaException {
    long stopTime = System.currentTimeMillis() + timeout;
    StopWatch stopWatch = StopWatch.createStarted();

    while (stopWatch.getTime() < stopTime) {
      TruCode truCode = getPairedTruCode(id);

      if (truCode != null) {
        return truCode;
      } else {
        long remainingTime = stopTime - System.currentTimeMillis();

        if (remainingTime > 0) {
          long sleepTime = Math.min(pollingInterval.toMillis(), remainingTime);

          try {
            ThreadUtils.sleep(Duration.ofMillis(sleepTime));
          } catch (InterruptedException e) {
            throw new TrusonaException("Thread was interrupted while polling for a paired TruCode", e);
          }
        }
      }
    }

    return null;
  }

  @Override
  public List<IdentityDocument> findIdentityDocuments(String userIdentifier) throws TrusonaException {
    return new CallHandler<>(getIdentityDocumentService().findIdentityDocuments(userIdentifier))
      .handle(genericErrorHandler);
  }

  @Override
  public IdentityDocument getIdentityDocument(UUID id) throws TrusonaException {
    return new CallHandler<>(getIdentityDocumentService().getIdentityDocument(id)).handle(genericErrorHandler);
  }

  /**
   * Deactivate a user by their user identifier
   *
   * @param userIdentifier A String that would be used to identify the user.
   * @return Void
   * @throws TrusonaException If any network or server errors occur.
   */
  @Override
  public Void deactivateUser(String userIdentifier) throws TrusonaException {
    return usersApi.deactivateUser(notEmpty(userIdentifier));
  }

  /**
   * Finds the specified device.
   *
   * @param deviceIdentifier The identifier of the device
   * @return the device if found, otherwise null
   * @throws TrusonaException If any network or server errors occur.
   */
  @Override
  public Device getDevice(String deviceIdentifier) throws TrusonaException {
    return devicesApi.getDevice(deviceIdentifier);
  }
}