/*
 * Copyright 2013 The Apache Software Foundation.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * 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 org.apache.manifoldcf.authorities.authorities.generic;

import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import org.apache.manifoldcf.core.util.URLEncoder;

import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Locale;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Unmarshaller;
import org.apache.http.HttpException;
import org.apache.http.HttpRequest;
import org.apache.http.HttpRequestInterceptor;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.Credentials;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.auth.BasicScheme;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.params.HttpConnectionParams;
import org.apache.http.protocol.HttpContext;
import org.apache.http.util.EntityUtils;
import org.apache.manifoldcf.authorities.interfaces.AuthorizationResponse;
import org.apache.manifoldcf.authorities.system.ManifoldCF;
import org.apache.manifoldcf.core.interfaces.CacheManagerFactory;
import org.apache.manifoldcf.core.interfaces.ConfigParams;
import org.apache.manifoldcf.core.interfaces.ICacheCreateHandle;
import org.apache.manifoldcf.core.interfaces.ICacheDescription;
import org.apache.manifoldcf.core.interfaces.ICacheHandle;
import org.apache.manifoldcf.core.interfaces.ICacheManager;
import org.apache.manifoldcf.core.interfaces.IHTTPOutput;
import org.apache.manifoldcf.core.interfaces.IPostParameters;
import org.apache.manifoldcf.core.interfaces.IThreadContext;
import org.apache.manifoldcf.core.interfaces.ManifoldCFException;
import org.apache.manifoldcf.core.interfaces.StringSet;
import org.apache.manifoldcf.crawler.connectors.generic.api.Auth;
import org.apache.manifoldcf.ui.util.Encoder;

/**
 *
 * @author krycek
 */
public class GenericAuthority extends org.apache.manifoldcf.authorities.authorities.BaseAuthorityConnector {

  public static final String _rcsid = "@(#)$Id: GenericAuthority.java 1496653 2013-06-25 22:05:04Z mlizewski $";

  /**
   * This is the active directory global deny token. This should be ingested
   * with all documents.
   */
  private static final String globalDenyToken = "DEAD_AUTHORITY";

  private static final AuthorizationResponse unreachableResponse = new AuthorizationResponse(new String[]{globalDenyToken},
    AuthorizationResponse.RESPONSE_UNREACHABLE);

  private static final AuthorizationResponse userNotFoundResponse = new AuthorizationResponse(new String[]{globalDenyToken},
    AuthorizationResponse.RESPONSE_USERNOTFOUND);

  private final static String ACTION_PARAM_NAME = "action";

  private final static String ACTION_AUTH = "auth";

  private final static String ACTION_CHECK = "check";

  private String genericLogin = null;

  private String genericPassword = null;

  private String genericEntryPoint = null;

  private int connectionTimeoutMillis = 60 * 1000;

  private int socketTimeoutMillis = 30 * 60 * 1000;

  private long responseLifetime = 60000L; //60sec

  private int LRUsize = 1000;

  private DefaultHttpClient client = null;

  private long sessionExpirationTime = -1L;

  /**
   * Cache manager.
   */
  private ICacheManager cacheManager = null;

  /**
   * Constructor.
   */
  public GenericAuthority() {
  }

  /**
   * Set thread context.
   */
  @Override
  public void setThreadContext(IThreadContext tc)
    throws ManifoldCFException {
    super.setThreadContext(tc);
    cacheManager = CacheManagerFactory.make(tc);
  }

  /**
   * Connect. The configuration parameters are included.
   *
   * @param configParams are the configuration parameters for this connection.
   */
  @Override
  public void connect(ConfigParams configParams) {
    super.connect(configParams);
    genericEntryPoint = getParam(configParams, "genericEntryPoint", null);
    genericLogin = getParam(configParams, "genericLogin", null);
    genericPassword = "";
    try {
      genericPassword = ManifoldCF.deobfuscate(getParam(configParams, "genericPassword", ""));
    } catch (ManifoldCFException ignore) {
    }
    connectionTimeoutMillis = Integer.parseInt(getParam(configParams, "genericConnectionTimeout", "60000"));
    if (connectionTimeoutMillis == 0) {
      connectionTimeoutMillis = 60000;
    }
    socketTimeoutMillis = Integer.parseInt(getParam(configParams, "genericSocketTimeout", "1800000"));
    if (socketTimeoutMillis == 0) {
      socketTimeoutMillis = 1800000;
    }
    responseLifetime = Long.parseLong(getParam(configParams, "genericResponseLifetime", "60000"));
    if (responseLifetime == 0) {
      responseLifetime = 60000;
    }
  }

  protected DefaultHttpClient getClient() throws ManifoldCFException {
    synchronized (this) {
      if (client != null) {
        return client;
      }
      DefaultHttpClient cl = new DefaultHttpClient();
      if (genericLogin != null && !genericLogin.isEmpty()) {
        try {
          URL url = new URL(genericEntryPoint);
          Credentials credentials = new UsernamePasswordCredentials(genericLogin, genericPassword);
          cl.getCredentialsProvider().setCredentials(new AuthScope(url.getHost(), url.getPort() > 0 ? url.getPort() : 80, AuthScope.ANY_REALM), credentials);
          cl.addRequestInterceptor(new PreemptiveAuth(credentials), 0);
        } catch (MalformedURLException ex) {
          client = null;
          sessionExpirationTime = -1L;
          throw new ManifoldCFException("getClient exception: " + ex.getMessage(), ex);
        }
      }
      HttpConnectionParams.setConnectionTimeout(cl.getParams(), connectionTimeoutMillis);
      HttpConnectionParams.setSoTimeout(cl.getParams(), socketTimeoutMillis);
      sessionExpirationTime = System.currentTimeMillis() + 300000L;
      client = cl;
      return cl;
    }
  }

  /**
   * Poll. The connection should be closed if it has been idle for too long.
   */
  @Override
  public void poll()
    throws ManifoldCFException {
    if (client != null && System.currentTimeMillis() > sessionExpirationTime) {
      disconnectSession();
    }
    super.poll();
  }

  /** This method is called to assess whether to count this connector instance should
  * actually be counted as being connected.
  *@return true if the connector instance is actually connected.
  */
  @Override
  public boolean isConnected()
  {
    return client != null;
  }

  /**
   * Check connection for sanity.
   */
  @Override
  public String check()
    throws ManifoldCFException {
    HttpClient client = getClient();
    try {
      CheckThread checkThread = new CheckThread(client, genericEntryPoint + "?" + ACTION_PARAM_NAME + "=" + ACTION_CHECK);
      checkThread.start();
      checkThread.join();
      if (checkThread.getException() != null) {
        Throwable thr = checkThread.getException();
        return "Check exception: " + thr.getMessage();
      }
      return checkThread.getResult();
    } catch (InterruptedException ex) {
      throw new ManifoldCFException(ex.getMessage(), ex, ManifoldCFException.INTERRUPTED);
    }
  }

  /**
   * Close the connection. Call this before discarding the repository connector.
   */
  @Override
  public void disconnect()
    throws ManifoldCFException {
    disconnectSession();
    super.disconnect();

    // Zero out all the stuff that we want to be sure we don't use again
    genericEntryPoint = null;
    genericLogin = null;
    genericPassword = null;
  }

  protected String createCacheConnectionString() {
    StringBuilder sb = new StringBuilder();
    sb.append(genericEntryPoint).append("#").append(genericLogin);
    return sb.toString();
  }

  /**
   * Obtain the access tokens for a given user name.
   *
   * @param userName is the user name or identifier.
   * @return the response tokens (according to the current authority). (Should
   * throws an exception only when a condition cannot be properly described
   * within the authorization response object.)
   */
  @Override
  public AuthorizationResponse getAuthorizationResponse(String userName)
    throws ManifoldCFException {

    HttpClient client = getClient();
    // Construct a cache description object
    ICacheDescription objectDescription = new GenericAuthorizationResponseDescription(userName,
      createCacheConnectionString(), this.responseLifetime, this.LRUsize);

    // Enter the cache
    ICacheHandle ch = cacheManager.enterCache(new ICacheDescription[]{objectDescription}, null, null);
    try {
      ICacheCreateHandle createHandle = cacheManager.enterCreateSection(ch);
      try {
        // Lookup the object
        AuthorizationResponse response = (AuthorizationResponse) cacheManager.lookupObject(createHandle, objectDescription);
        if (response != null) {
          return response;
        }
        // Create the object.
        response = getAuthorizationResponseUncached(client, userName);
        // Save it in the cache
        cacheManager.saveObject(createHandle, objectDescription, response);
        // And return it...
        return response;
      } finally {
        cacheManager.leaveCreateSection(createHandle);
      }
    } finally {
      cacheManager.leaveCache(ch);
    }
  }

  protected AuthorizationResponse getAuthorizationResponseUncached(HttpClient client, String userName)
    throws ManifoldCFException {
    StringBuilder url = new StringBuilder(genericEntryPoint);
    url.append("?").append(ACTION_PARAM_NAME).append("=").append(ACTION_AUTH);
    url.append("&username=").append(URLEncoder.encode(userName));


    try {
      FetchTokensThread t = new FetchTokensThread(client, url.toString());
      t.start();
      t.join();
      if (t.getException() != null) {
        return unreachableResponse;
      }
      Auth auth = t.getAuthResponse();
      if (auth == null) {
        return userNotFoundResponse;
      }
      if (!auth.exists) {
        return userNotFoundResponse;
      }
      if (auth.tokens == null) {
        return new AuthorizationResponse(new String[]{}, AuthorizationResponse.RESPONSE_OK);
      }

      String[] tokens = new String[auth.tokens.size()];
      int k = 0;
      while (k < tokens.length) {
        tokens[k] = (String) auth.tokens.get(k);
        k++;
      }

      return new AuthorizationResponse(tokens, AuthorizationResponse.RESPONSE_OK);
    } catch (InterruptedException ex) {
      throw new ManifoldCFException(ex.getMessage(), ex, ManifoldCFException.INTERRUPTED);
    }
  }

  /**
   * Obtain the default access tokens for a given user name.
   *
   * @param userName is the user name or identifier.
   * @return the default response tokens, presuming that the connect method
   * fails.
   */
  @Override
  public AuthorizationResponse getDefaultAuthorizationResponse(String userName) {
    // The default response if the getConnection method fails
    return unreachableResponse;
  }

  // UI support methods.
  //
  // These support methods are involved in setting up authority connection configuration information. The configuration methods cannot assume that the
  // current authority object is connected.  That is why they receive a thread context argument.
  @Override
  public void outputConfigurationHeader(IThreadContext threadContext, IHTTPOutput out,
    Locale locale, ConfigParams parameters, List<String> tabsArray)
    throws ManifoldCFException, IOException {
    tabsArray.add(Messages.getString(locale, "generic.EntryPoint"));

    out.print(
      "<script type=\"text/javascript\">\n"
      + "<!--\n"
      + "function checkConfig() {\n"
      + "  return true;\n"
      + "}\n"
      + "\n"
      + "function checkConfigForSave() {\n"
      + "  if (editconnection.genericEntryPoint.value == \"\") {\n"
      + "    alert(\"" + Messages.getBodyJavascriptString(locale, "generic.EntryPointCannotBeBlank") + "\");\n"
      + "    SelectTab(\"" + Messages.getBodyJavascriptString(locale, "generic.EntryPoint") + "\");\n"
      + "    editconnection.genericEntryPoint.focus();\n"
      + "    return false;\n"
      + "  }\n"
      + "  return true;\n"
      + "}\n"
      + "//-->\n"
      + "</script>\n");
  }

  @Override
  public void outputConfigurationBody(IThreadContext threadContext, IHTTPOutput out,
    Locale locale, ConfigParams parameters, String tabName)
    throws ManifoldCFException, IOException {

    String server = getParam(parameters, "genericEntryPoint", "");
    String login = getParam(parameters, "genericLogin", "");
    String password = "";
    try {
      password = out.mapPasswordToKey(ManifoldCF.deobfuscate(getParam(parameters, "genericPassword", "")));
    } catch (ManifoldCFException ignore) {
    }
    String conTimeout = getParam(parameters, "genericConnectionTimeout", "60000");
    String soTimeout = getParam(parameters, "genericSocketTimeout", "1800000");
    String respLifetime = getParam(parameters, "genericResponseLifetime", "60000");

    if (tabName.equals(Messages.getString(locale, "generic.EntryPoint"))) {
      out.print(
        "<table class=\"displaytable\">\n"
        + " <tr><td class=\"separator\" colspan=\"2\"><hr/></td></tr>\n"
        + " <tr>\n"
        + "  <td class=\"description\"><nobr>" + Messages.getBodyString(locale, "generic.EntryPointColon") + "</nobr></td>\n"
        + "  <td class=\"value\"><input type=\"text\" size=\"32\" name=\"genericEntryPoint\" value=\"" + Encoder.attributeEscape(server) + "\"/></td>\n"
        + " </tr>\n"
        + " <tr>\n"
        + "  <td class=\"description\"><nobr>" + Messages.getBodyString(locale, "generic.LoginColon") + "</nobr></td>\n"
        + "  <td class=\"value\"><input type=\"text\" size=\"32\" name=\"genericLogin\" value=\"" + Encoder.attributeEscape(login) + "\"/></td>\n"
        + " </tr>\n"
        + " <tr>\n"
        + "  <td class=\"description\"><nobr>" + Messages.getBodyString(locale, "generic.PasswordColon") + "</nobr></td>\n"
        + "  <td class=\"value\"><input type=\"password\" size=\"32\" name=\"genericPassword\" value=\"" + Encoder.attributeEscape(password) + "\"/></td>\n"
        + " </tr>\n"
        + " <tr>\n"
        + "  <td class=\"description\"><nobr>" + Messages.getBodyString(locale, "generic.ConnectionTimeoutColon") + "</nobr></td>\n"
        + "  <td class=\"value\"><input type=\"text\" size=\"32\" name=\"genericConnectorTimeout\" value=\"" + Encoder.attributeEscape(conTimeout) + "\"/></td>\n"
        + " </tr>\n"
        + " <tr>\n"
        + "  <td class=\"description\"><nobr>" + Messages.getBodyString(locale, "generic.SocketTimeoutColon") + "</nobr></td>\n"
        + "  <td class=\"value\"><input type=\"text\" size=\"32\" name=\"genericSocketTimeout\" value=\"" + Encoder.attributeEscape(soTimeout) + "\"/></td>\n"
        + " </tr>\n"
        + " <tr>\n"
        + "  <td class=\"description\"><nobr>" + Messages.getBodyString(locale, "generic.ResponseLifetimeColon") + "</nobr></td>\n"
        + "  <td class=\"value\"><input type=\"text\" size=\"32\" name=\"genericResponseLifetime\" value=\"" + Encoder.attributeEscape(respLifetime) + "\"/></td>\n"
        + " </tr>\n"
        + "</table>\n");
    } else {
      out.print("<input type=\"hidden\" name=\"genericEntryPoint\" value=\"" + Encoder.attributeEscape(server) + "\"/>\n");
      out.print("<input type=\"hidden\" name=\"genericLogin\" value=\"" + Encoder.attributeEscape(login) + "\"/>\n");
      out.print("<input type=\"hidden\" name=\"genericPassword\" value=\"" + Encoder.attributeEscape(password) + "\"/>\n");
      out.print("<input type=\"hidden\" name=\"genericConnectorTimeout\" value=\"" + Encoder.attributeEscape(conTimeout) + "\"/>\n");
      out.print("<input type=\"hidden\" name=\"genericSocketTimeout\" value=\"" + Encoder.attributeEscape(soTimeout) + "\"/>\n");
      out.print("<input type=\"hidden\" name=\"genericResponseLifetime\" value=\"" + Encoder.attributeEscape(respLifetime) + "\"/>\n");
    }
  }

  @Override
  public String processConfigurationPost(IThreadContext threadContext, IPostParameters variableContext,
    Locale locale, ConfigParams parameters)
    throws ManifoldCFException {

    copyParam(variableContext, parameters, "genericLogin");
    copyParam(variableContext, parameters, "genericEntryPoint");
    copyParam(variableContext, parameters, "genericConnectorTimeout");
    copyParam(variableContext, parameters, "genericSocketTimeout");
    copyParam(variableContext, parameters, "genericResponseLifetime");

    String password = variableContext.getParameter("genericPassword");
    if (password == null) {
      password = "";
    }
    parameters.setParameter("genericPassword", org.apache.manifoldcf.core.system.ManifoldCF.obfuscate(variableContext.mapKeyToPassword(password)));
    return null;
  }

  @Override
  public void viewConfiguration(IThreadContext threadContext, IHTTPOutput out,
    Locale locale, ConfigParams parameters)
    throws ManifoldCFException, IOException {
    String login = getParam(parameters, "genericLogin", "");
    String server = getParam(parameters, "genericEntryPoint", "");
    String conTimeout = getParam(parameters, "genericConnectionTimeout", "60000");
    String soTimeout = getParam(parameters, "genericSocketTimeout", "1800000");
    String respLifetime = getParam(parameters, "genericResponseLifetime", "60000");

    out.print(
      "<table class=\"displaytable\">\n"
      + " <tr><td class=\"separator\" colspan=\"2\"><hr/></td></tr>\n"
      + " <tr>\n"
      + "  <td class=\"description\"><nobr>" + Messages.getBodyString(locale, "generic.EntryPointColon") + "</nobr></td>\n"
      + "  <td class=\"value\">" + Encoder.bodyEscape(server) + "</td>\n"
      + " </tr>\n"
      + " <tr>\n"
      + "  <td class=\"description\"><nobr>" + Messages.getBodyString(locale, "generic.LoginColon") + "</nobr></td>\n"
      + "  <td class=\"value\">" + Encoder.bodyEscape(login) + "</td>\n"
      + " </tr>\n"
      + " <tr>\n"
      + "  <td class=\"description\"><nobr>" + Messages.getBodyString(locale, "generic.PasswordColon") + "</nobr></td>\n"
      + "  <td class=\"value\">**********</td>\n"
      + " </tr>\n"
      + " <tr>\n"
      + "  <td class=\"description\"><nobr>" + Messages.getBodyString(locale, "generic.ConnectionTimeoutColon") + "</nobr></td>\n"
      + "  <td class=\"value\">" + Encoder.bodyEscape(conTimeout) + "</td>\n"
      + " </tr>\n"
      + " <tr>\n"
      + "  <td class=\"description\"><nobr>" + Messages.getBodyString(locale, "generic.SocketTimeoutColon") + "</nobr></td>\n"
      + "  <td class=\"value\">" + Encoder.bodyEscape(soTimeout) + "</td>\n"
      + " </tr>\n"
      + " <tr>\n"
      + "  <td class=\"description\"><nobr>" + Messages.getBodyString(locale, "generic.ResponseLifetimeColon") + "</nobr></td>\n"
      + "  <td class=\"value\">" + Encoder.bodyEscape(respLifetime) + "</td>\n"
      + " </tr>\n"
      + "</table>\n");
  }

  private String getParam(ConfigParams parameters, String name, String def) {
    return parameters.getParameter(name) != null ? parameters.getParameter(name) : def;
  }

  private boolean copyParam(IPostParameters variableContext, ConfigParams parameters, String name) {
    String val = variableContext.getParameter(name);
    if (val == null) {
      return false;
    }
    parameters.setParameter(name, val);
    return true;
  }

  // Protected methods
  protected static StringSet emptyStringSet = new StringSet();

  private void disconnectSession() {
    synchronized (this) {
      client.getConnectionManager().shutdown();
      client = null;
    }
  }

  /**
   * This is the cache object descriptor for cached access tokens from this
   * connector.
   */
  protected class GenericAuthorizationResponseDescription extends org.apache.manifoldcf.core.cachemanager.BaseDescription {

    /**
     * The user name
     */
    protected String userName;

    /**
     * LDAP connection string with server name and base DN
     */
    protected String connectionString;

    /**
     * The response lifetime
     */
    protected long responseLifetime;

    /**
     * The expiration time
     */
    protected long expirationTime = -1;

    /**
     * Constructor.
     */
    public GenericAuthorizationResponseDescription(String userName, String connectionString, long responseLifetime, int LRUsize) {
      super("LDAPAuthority", LRUsize);
      this.userName = userName;
      this.connectionString = connectionString;
      this.responseLifetime = responseLifetime;
    }

    /**
     * Return the invalidation keys for this object.
     */
    @Override
    public StringSet getObjectKeys() {
      return emptyStringSet;
    }

    /**
     * Get the critical section name, used for synchronizing the creation of the
     * object
     */
    @Override
    public String getCriticalSectionName() {
      StringBuilder sb = new StringBuilder(getClass().getName());
      sb.append("-").append(userName).append("-").append(connectionString);
      return sb.toString();
    }

    /**
     * Return the object expiration interval
     */
    @Override
    public long getObjectExpirationTime(long currentTime) {
      if (expirationTime == -1) {
        expirationTime = currentTime + responseLifetime;
      }
      return expirationTime;
    }

    @Override
    public int hashCode() {
      return userName.hashCode() + connectionString.hashCode();
    }

    @Override
    public boolean equals(Object o) {
      if (!(o instanceof GenericAuthorizationResponseDescription)) {
        return false;
      }
      GenericAuthorizationResponseDescription ard = (GenericAuthorizationResponseDescription) o;
      if (!ard.userName.equals(userName)) {
        return false;
      }
      if (!ard.connectionString.equals(connectionString)) {
        return false;
      }
      return true;
    }
  }

  static class PreemptiveAuth implements HttpRequestInterceptor {

    private Credentials credentials;

    public PreemptiveAuth(Credentials creds) {
      this.credentials = creds;
    }

    @Override
    public void process(final HttpRequest request, final HttpContext context) throws HttpException, IOException {
      request.addHeader(new BasicScheme(StandardCharsets.US_ASCII).authenticate(credentials, request, context));
    }
  }

  protected static class CheckThread extends Thread {

    protected HttpClient client;

    protected String url;

    protected Throwable exception = null;

    protected String result = "Unknown";

    public CheckThread(HttpClient client, String url) {
      super();
      setDaemon(true);
      this.client = client;
      this.url = url;
    }

    @Override
    public void run() {
      HttpGet method = new HttpGet(url);
      try {
        HttpResponse response = client.execute(method);
        try {
          if (response.getStatusLine().getStatusCode() != HttpStatus.SC_OK) {
            result = "Connection failed: " + response.getStatusLine().getReasonPhrase();
            return;
          }
          EntityUtils.consume(response.getEntity());
          result = "Connection OK";
        } finally {
          EntityUtils.consume(response.getEntity());
          method.releaseConnection();
        }
      } catch (IOException ex) {
        exception = ex;
      }
    }

    public Throwable getException() {
      return exception;
    }

    public String getResult() {
      return result;
    }
  }

  protected static class FetchTokensThread extends Thread {

    protected HttpClient client;

    protected String url;

    protected Throwable exception = null;

    protected Auth auth;

    public FetchTokensThread(HttpClient client, String url) {
      super();
      setDaemon(true);
      this.client = client;
      this.url = url;
      this.auth = null;
    }

    @Override
    public void run() {
      try {
        HttpGet method = new HttpGet(url.toString());

        HttpResponse response = client.execute(method);
        try {
          if (response.getStatusLine().getStatusCode() != HttpStatus.SC_OK) {
            exception = new ManifoldCFException("FetchTokensThread error - interface returned incorrect return code for: " + url + " - " + response.getStatusLine().toString());
            return;
          }
          JAXBContext context;
          context = JAXBContext.newInstance(Auth.class);
          Unmarshaller m = context.createUnmarshaller();
          auth = (Auth) m.unmarshal(response.getEntity().getContent());
        } catch (JAXBException ex) {
          exception = ex;
        } finally {
          EntityUtils.consume(response.getEntity());
          method.releaseConnection();
        }
      } catch (Exception ex) {
        exception = ex;
      }
    }

    public Throwable getException() {
      return exception;
    }

    public Auth getAuthResponse() {
      return auth;
    }
  }
}
