/**
 * Filter outgoing identities by name and or type. Name filter is specified using regular
 * expressions
 *
 * @author K. Benedyczak
 */
public class FilterIdentityAction extends AbstractOutputTranslationAction {
  private static final Logger log =
      Log.getLogger(Log.U_SERVER_TRANSLATION, FilterIdentityAction.class);
  private String identity;
  private Pattern idValueRegexp;

  public FilterIdentityAction(String[] params, TranslationActionDescription desc)
      throws EngineException {
    super(desc, params);
    setParameters(params);
  }

  @Override
  protected void invokeWrapped(
      TranslationInput input, Object mvelCtx, String currentProfile, TranslationResult result)
      throws EngineException {
    Set<IdentityParam> copy = new HashSet<IdentityParam>(result.getIdentities());
    for (IdentityParam i : copy) {
      if ((identity == null || i.getTypeId().equals(identity))
          && (idValueRegexp == null || idValueRegexp.matcher(i.getValue()).matches())) {
        log.debug("Filtering the identity " + i.toString());
        result.getIdentities().remove(i);
      }
    }
  }

  private void setParameters(String[] parameters) {
    if (parameters.length != 2)
      throw new IllegalArgumentException("Action requires exactly 2 parameters");
    identity = parameters[0];
    idValueRegexp = parameters[1] == null ? null : Pattern.compile(parameters[1]);
  }
}
/**
 * Email notification facility.
 *
 * @author K. Benedyczak
 */
@Component
public class EmailFacility implements NotificationFacility {
  private static final Logger log = Log.getLogger(Log.U_SERVER, EmailFacility.class);
  public static final String NAME = "email";

  private static final String CFG_USER = "******";
  private static final String CFG_PASSWD = "mailx.smtp.auth.password";
  private static final String CFG_TRUST_ALL = "mailx.smtp.trustAll";

  private ExecutorsService executorsService;
  private PKIManagement pkiManagement;
  private AttributesHelper attributesHelper;
  private DBIdentities dbIdentities;
  private IdentitiesResolver idResolver;

  @Autowired
  public EmailFacility(
      ExecutorsService executorsService,
      PKIManagement pkiManagement,
      AttributesHelper attributeHelper,
      IdentitiesResolver idResolver,
      DBIdentities dbIdentities) {
    this.executorsService = executorsService;
    this.pkiManagement = pkiManagement;
    this.attributesHelper = attributeHelper;
    this.idResolver = idResolver;
    this.dbIdentities = dbIdentities;
  }

  @Override
  public String getName() {
    return NAME;
  }

  @Override
  public String getDescription() {
    return "Sends notifications by e-mail";
  }

  @Override
  public void validateConfiguration(String configuration) throws WrongArgumentException {
    // TODO create properties helper and validate more throughly?
    Properties props = new Properties();
    try {
      props.load(new StringReader(configuration));
    } catch (IOException e) {
      throw new WrongArgumentException(
          "Email configuration is invalid: " + "not a valid properties syntax was used", e);
    }
  }

  @Override
  public NotificationChannelInstance getChannel(String configuration) {
    return new EmailChannel(configuration);
  }

  /**
   * Address is established as follows (first found is returned):
   *
   * <ol>
   *   <li>entity's identity of email type, confirmed
   *   <li>entity's attribute selected as contact email, confirmed
   *   <li>entity's identity of email type
   *   <li>entity's attribute selected as contact email
   * </ol>
   *
   * In each case if there are more then one addresses the first in the list is returned.
   */
  @Override
  public String getAddressForEntity(EntityParam recipient, SqlSession sql, String preferredAddress)
      throws EngineException {
    List<VerifiableEmail> emailIds = getEmailIdentities(recipient, sql);
    AttributeExt<?> emailAttr =
        attributesHelper.getAttributeByMetadata(
            recipient, "/", ContactEmailMetadataProvider.NAME, sql);

    if (preferredAddress != null && isPresent(preferredAddress, emailIds, emailAttr))
      return preferredAddress;

    String confirmedOnly = getAddressFrom(emailIds, emailAttr, true);
    if (confirmedOnly != null) return confirmedOnly;

    String plain = getAddressFrom(emailIds, emailAttr, false);
    if (plain != null) return plain;

    throw new IllegalIdentityValueException("The entity does not have the email address specified");
  }

  private boolean isPresent(
      String address, List<VerifiableEmail> emailIds, AttributeExt<?> emailAttr) {
    for (VerifiableEmail ve : emailIds) if (ve.getValue().equals(address)) return true;
    if (emailAttr != null) {
      for (Object emailO : emailAttr.getValues()) {
        if (emailO.toString().equals(address)) return true;
      }
    }
    return false;
  }

  /**
   * Address is established as in {@link #getAddressForEntity(EntityParam, SqlSession)} however only
   * the input from the registration request is used and the cases with "confirmed" status are
   * skipped.
   */
  @Override
  public String getAddressForRegistrationRequest(
      RegistrationRequestState currentRequest, SqlSession sql) throws EngineException {
    List<VerifiableEmail> emailIds = getEmailIdentities(currentRequest);
    Attribute<?> emailAttr = getEmailAttributeFromRequest(currentRequest, sql);

    return getAddressFrom(emailIds, emailAttr, false);
  }

  private String getAddressFrom(
      List<VerifiableEmail> emailIds, Attribute<?> emailAttr, boolean useConfirmed) {
    for (VerifiableEmail id : emailIds) if (!useConfirmed || id.isConfirmed()) return id.getValue();

    if (emailAttr != null && (!useConfirmed || emailAttr.getAttributeSyntax().isVerifiable()))
      for (Object emailO : emailAttr.getValues()) {
        if (!useConfirmed) {
          return emailO.toString();
        } else if (emailAttr.getAttributeSyntax().isVerifiable()) {
          VerifiableEmail email = (VerifiableEmail) emailO;
          if (!useConfirmed || email.isConfirmed()) return email.getValue();
        }
      }
    return null;
  }

  private List<VerifiableEmail> getEmailIdentities(EntityParam recipient, SqlSession sql)
      throws EngineException {
    List<VerifiableEmail> emailIds = new ArrayList<>();
    long entityId = idResolver.getEntityId(recipient, sql);
    Identity[] identities = dbIdentities.getIdentitiesForEntityNoContext(entityId, sql);
    for (Identity id : identities)
      if (id.getTypeId().equals(EmailIdentity.ID))
        emailIds.add(EmailIdentity.fromIdentityParam(id));
    return emailIds;
  }

  private List<VerifiableEmail> getEmailIdentities(RegistrationRequestState currentRequest)
      throws EngineException {
    List<VerifiableEmail> emailIds = new ArrayList<>();
    List<IdentityParam> identities = currentRequest.getRequest().getIdentities();
    if (identities == null) return emailIds;
    for (IdentityParam id : identities)
      if (id != null && id.getTypeId().equals(EmailIdentity.ID))
        emailIds.add(EmailIdentity.fromIdentityParam(id));
    return emailIds;
  }

  private Attribute<?> getEmailAttributeFromRequest(
      RegistrationRequestState currentRequest, SqlSession sql) throws EngineException {
    List<Attribute<?>> attrs = currentRequest.getRequest().getAttributes();
    if (attrs == null) return null;
    AttributeType at =
        attributesHelper.getAttributeTypeWithSingeltonMetadata(
            ContactEmailMetadataProvider.NAME, sql);
    if (at == null) return null;
    for (Attribute<?> ap : attrs) {
      if (ap == null) continue;
      if (ap.getName().equals(at.getName()) && ap.getGroupPath().equals("/")) return ap;
    }
    return null;
  }

  private class EmailChannel implements NotificationChannelInstance {
    private Session session;

    public EmailChannel(String configuration) {
      Properties props = new Properties();
      try {
        props.load(new StringReader(configuration));
      } catch (IOException e) {
        // really shouldn't happen
        throw new IllegalStateException(
            "Bug: can't load email properties " + "for the channel instance", e);
      }
      String smtpUser = props.getProperty(CFG_USER);
      String smtpPassword = props.getProperty(CFG_PASSWD);
      Authenticator smtpAuthn =
          (smtpUser != null && smtpPassword != null)
              ? new SimpleAuthenticator(smtpUser, smtpPassword)
              : null;
      String trustAll = props.getProperty(CFG_TRUST_ALL);
      if (trustAll != null && "true".equalsIgnoreCase(trustAll)) {
        MailSSLSocketFactory trustAllSF;
        try {
          trustAllSF = new MailSSLSocketFactory();
        } catch (GeneralSecurityException e) {
          // really shouldn't happen
          throw new IllegalStateException("Can't init trust-all SSL socket factory", e);
        }
        trustAllSF.setTrustAllHosts(true);
        props.put("mail.smtp.ssl.socketFactory", trustAllSF);
      } else {
        X509CertChainValidator validator = pkiManagement.getMainAuthnAndTrust().getValidator();
        SSLSocketFactory factory = SocketFactoryCreator.getSocketFactory(null, validator);
        props.put("mail.smtp.ssl.socketFactory", factory);
      }
      session = Session.getInstance(props, smtpAuthn);
    }

    @Override
    public Future<NotificationStatus> sendNotification(
        final String recipientAddress, final String msgSubject, final String message) {
      final NotificationStatus retStatus = new NotificationStatus();
      return executorsService
          .getService()
          .submit(
              new Runnable() {
                @Override
                public void run() {
                  try {
                    sendEmail(msgSubject, message, recipientAddress);
                  } catch (Exception e) {
                    log.error("E-mail notification failed", e);
                    retStatus.setProblem(e);
                  }
                }
              },
              retStatus);
    }

    private void sendEmail(String subject, String body, String to) throws MessagingException {
      log.debug("Sending e-mail message to '" + to + "' with subject: " + subject);
      MimeMessage msg = new MimeMessage(session);
      msg.setFrom();
      msg.setRecipients(Message.RecipientType.TO, to);
      msg.setSubject(subject);
      msg.setSentDate(new Date());
      msg.setText(body);
      Transport.send(msg);
    }

    @Override
    public String getFacilityId() {
      return NAME;
    }
  }

  private static class SimpleAuthenticator extends Authenticator {
    private String user;
    private String password;

    public SimpleAuthenticator(String user, String password) {
      this.user = user;
      this.password = password;
    }

    protected PasswordAuthentication getPasswordAuthentication() {
      return new PasswordAuthentication(user, password);
    }
  }
}
/**
 * Implementation of {@link SessionManagement}
 *
 * @author K. Benedyczak
 */
@Component
public class SessionManagementImpl implements SessionManagement {
  private static final Logger log = Log.getLogger(Log.U_SERVER, SessionManagementImpl.class);
  public static final long DB_ACTIVITY_WRITE_DELAY = 3000;
  public static final String SESSION_TOKEN_TYPE = "session";
  private TokensManagement tokensManagement;
  private LoginToHttpSessionBinder sessionBinder;
  private SessionParticipantTypesRegistry participantTypesRegistry;
  private DBIdentities dbIdentities;
  private DBSessionManager db;

  /** map of timestamps indexed by session ids, when the last activity update was written to DB. */
  private Map<String, Long> recentUsageUpdates = new WeakHashMap<>();

  @Autowired
  public SessionManagementImpl(
      TokensManagement tokensManagement,
      ExecutorsService execService,
      LoginToHttpSessionBinder sessionBinder,
      SessionParticipantTypesRegistry participantTypesRegistry,
      DBIdentities dbIdentities,
      DBSessionManager db) {
    this.tokensManagement = tokensManagement;
    this.sessionBinder = sessionBinder;
    this.participantTypesRegistry = participantTypesRegistry;
    this.dbIdentities = dbIdentities;
    this.db = db;
    execService
        .getService()
        .scheduleWithFixedDelay(new TerminateInactiveSessions(), 20, 30, TimeUnit.SECONDS);
  }

  @Override
  public LoginSession getCreateSession(
      long loggedEntity,
      AuthenticationRealm realm,
      String entityLabel,
      boolean outdatedCredential,
      Date absoluteExpiration) {
    Object transaction = tokensManagement.startTokenTransaction();
    try {
      try {
        LoginSession ret =
            getOwnedSession(new EntityParam(loggedEntity), realm.getName(), transaction);
        if (ret != null) {
          ret.setLastUsed(new Date());
          byte[] contents = ret.getTokenContents();
          tokensManagement.updateToken(
              SESSION_TOKEN_TYPE, ret.getId(), null, contents, transaction);

          if (log.isDebugEnabled())
            log.debug(
                "Using existing session "
                    + ret.getId()
                    + " for logged entity "
                    + ret.getEntityId()
                    + " in realm "
                    + realm.getName());
          tokensManagement.commitTokenTransaction(transaction);
          return ret;
        }
      } catch (EngineException e) {
        throw new InternalException(
            "Can't retrieve current sessions of the " + "authenticated user", e);
      }

      LoginSession ret =
          createSession(
              loggedEntity,
              realm,
              entityLabel,
              outdatedCredential,
              absoluteExpiration,
              transaction);
      tokensManagement.commitTokenTransaction(transaction);
      if (log.isDebugEnabled())
        log.debug(
            "Created a new session "
                + ret.getId()
                + " for logged entity "
                + ret.getEntityId()
                + " in realm "
                + realm.getName());
      return ret;
    } finally {
      tokensManagement.closeTokenTransaction(transaction);
      cleanScheduledRemoval(loggedEntity);
    }
  }

  private void cleanScheduledRemoval(long loggedEntity) {
    SqlSession sqlMap = db.getSqlSession(true);
    try {
      dbIdentities.clearScheduledRemovalStatus(loggedEntity, sqlMap);
      sqlMap.commit();
    } catch (EngineException e) {
      log.error("Can not clear automatic removal (if any) from the user being logged in", e);
    } finally {
      db.releaseSqlSession(sqlMap);
    }
  }

  private LoginSession createSession(
      long loggedEntity,
      AuthenticationRealm realm,
      String entityLabel,
      boolean outdatedCredential,
      Date absoluteExpiration,
      Object transaction) {
    UUID randomid = UUID.randomUUID();
    String id = randomid.toString();
    LoginSession ls =
        new LoginSession(
            id,
            new Date(),
            absoluteExpiration,
            realm.getMaxInactivity() * 1000,
            loggedEntity,
            realm.getName());
    ls.setUsedOutdatedCredential(outdatedCredential);
    ls.setEntityLabel(entityLabel);
    try {
      tokensManagement.addToken(
          SESSION_TOKEN_TYPE,
          id,
          new EntityParam(loggedEntity),
          ls.getTokenContents(),
          ls.getStarted(),
          ls.getExpires(),
          transaction);
    } catch (Exception e) {
      throw new InternalException("Can't create a new session", e);
    }
    return ls;
  }

  @Override
  public void updateSessionAttributes(String id, AttributeUpdater updater)
      throws WrongArgumentException {
    Object transaction = tokensManagement.startTokenTransaction();
    try {
      Token token = tokensManagement.getTokenById(SESSION_TOKEN_TYPE, id, transaction);
      LoginSession session = token2session(token);

      updater.updateAttributes(session.getSessionData());

      byte[] contents = session.getTokenContents();
      tokensManagement.updateToken(SESSION_TOKEN_TYPE, id, null, contents, transaction);
      tokensManagement.commitTokenTransaction(transaction);
    } finally {
      tokensManagement.closeTokenTransaction(transaction);
    }
  }

  @Override
  public void removeSession(String id, boolean soft) {
    sessionBinder.removeLoginSession(id, soft);
    try {
      tokensManagement.removeToken(SESSION_TOKEN_TYPE, id, null);
      if (log.isDebugEnabled()) log.debug("Removed session with id " + id);
    } catch (WrongArgumentException e) {
      // not found - ok
    }
  }

  @Override
  public LoginSession getSession(String id) throws WrongArgumentException {
    Token token = tokensManagement.getTokenById(SESSION_TOKEN_TYPE, id, null);
    return token2session(token);
  }

  private LoginSession getOwnedSession(EntityParam owner, String realm, Object transaction)
      throws EngineException {
    List<Token> tokens = tokensManagement.getOwnedTokens(SESSION_TOKEN_TYPE, owner, transaction);
    for (Token token : tokens) {
      LoginSession ls = token2session(token);
      if (realm.equals(ls.getRealm())) return ls;
    }
    return null;
  }

  @Override
  public LoginSession getOwnedSession(EntityParam owner, String realm) throws EngineException {
    LoginSession ret = getOwnedSession(owner, realm, null);
    if (ret == null)
      throw new WrongArgumentException("No session for this owner in the given realm");
    return ret;
  }

  @Override
  public void updateSessionActivity(String id) throws WrongArgumentException {
    Long lastWrite = recentUsageUpdates.get(id);
    if (lastWrite != null) {
      if (System.currentTimeMillis() < lastWrite + DB_ACTIVITY_WRITE_DELAY) return;
    }

    Object transaction = tokensManagement.startTokenTransaction();
    try {
      Token token = tokensManagement.getTokenById(SESSION_TOKEN_TYPE, id, transaction);
      LoginSession session = token2session(token);
      session.setLastUsed(new Date());
      byte[] contents = session.getTokenContents();
      tokensManagement.updateToken(SESSION_TOKEN_TYPE, id, null, contents, transaction);
      tokensManagement.commitTokenTransaction(transaction);
      log.trace("Updated in db session activity timestamp for " + id);
      recentUsageUpdates.put(id, System.currentTimeMillis());
    } finally {
      tokensManagement.closeTokenTransaction(transaction);
    }
  }

  @Override
  public void addSessionParticipant(SessionParticipant... participant) {
    InvocationContext invocationContext = InvocationContext.getCurrent();
    LoginSession ls = invocationContext.getLoginSession();
    try {
      SessionParticipants.AddParticipantToSessionTask addTask =
          new SessionParticipants.AddParticipantToSessionTask(
              participantTypesRegistry, participant);
      updateSessionAttributes(ls.getId(), addTask);
    } catch (WrongArgumentException e) {
      throw new InternalException("Can not add session participant to the existing session?", e);
    }
  }

  private LoginSession token2session(Token token) {
    LoginSession session = new LoginSession();
    session.deserialize(token);
    return session;
  }

  private class TerminateInactiveSessions implements Runnable {
    @Override
    public void run() {
      List<Token> tokens = tokensManagement.getAllTokens(SESSION_TOKEN_TYPE);
      long now = System.currentTimeMillis();
      for (Token t : tokens) {
        if (t.getExpires() != null) continue;
        LoginSession session = token2session(t);
        long inactiveFor = now - session.getLastUsed().getTime();
        if (inactiveFor > session.getMaxInactivity()) {
          log.debug("Expiring login session " + session + " inactive for: " + inactiveFor);
          try {
            removeSession(session.getId(), false);
          } catch (Exception e) {
            log.error("Can't expire the session " + session, e);
          }
        }
      }
    }
  }
}
/**
 * Configuration of OAuth client for custom provider.
 *
 * @author K. Benedyczak
 */
public class CustomProviderProperties extends UnityPropertiesHelper {
  private static final Logger log = Log.getLogger(Log.U_SERVER_CFG, CustomProviderProperties.class);

  public enum AccessTokenFormat {
    standard,
    httpParams
  };

  @DocumentationReferencePrefix public static final String P = "unity.oauth2.client.CLIENT_ID.";

  public static final String PROVIDER_TYPE = "type";
  public static final String PROVIDER_LOCATION = "authEndpoint";
  public static final String ACCESS_TOKEN_ENDPOINT = "accessTokenEndpoint";
  public static final String PROFILE_ENDPOINT = "profileEndpoint";
  public static final String PROVIDER_NAME = "name";
  public static final String CLIENT_ID = "clientId";
  public static final String CLIENT_SECRET = "clientSecret";
  public static final String CLIENT_AUTHN_MODE = "clientAuthenticationMode";
  public static final String SCOPES = "scopes";
  public static final String ACCESS_TOKEN_FORMAT = "accessTokenFormat";
  public static final String OPENID_CONNECT = "openIdConnect";
  public static final String OPENID_DISCOVERY = "openIdConnectDiscoveryEndpoint";
  public static final String ICON_URL = "iconUrl";
  public static final String CLIENT_TRUSTSTORE = "httpClientTruststore";
  public static final String CLIENT_HOSTNAME_CHECKING = "httpClientHostnameChecking";

  @DocumentationReferenceMeta
  public static final Map<String, PropertyMD> META = new HashMap<String, PropertyMD>();

  static {
    META.put(
        PROVIDER_TYPE,
        new PropertyMD(Providers.custom)
            .setDescription(
                "Type of provider. Either a well known provider type can be specified"
                    + " or 'custom'. In the first case only few additional settings are required: "
                    + "client id, secret and translation profile. Other settings as scope "
                    + "can be additionally set to fine tune the remote authentication. "
                    + "In the latter 'custom' case all mandatory options must be set."));
    META.put(
        PROVIDER_LOCATION,
        new PropertyMD()
            .setDescription(
                "Location (URL) of OAuth2 provider's authorization endpoint. "
                    + "It is mandatory for non OpenID Connect providers, in whose case "
                    + "the endopint can be discovered."));
    META.put(
        ACCESS_TOKEN_ENDPOINT,
        new PropertyMD()
            .setDescription(
                "Location (URL) of OAuth2 provider's access token endpoint. "
                    + "In case of OpenID Connect mode can be discovered, otherwise mandatory."));
    META.put(
        PROFILE_ENDPOINT,
        new PropertyMD()
            .setDescription(
                "Location (URL) of OAuth2 provider's user's profile endpoint. "
                    + "It is used to obtain additional user's attributes. "
                    + "It can be autodiscovered for OpenID Connect mode. Otherwise it must be"
                    + " set as otherwise there is no information about the user identity."));
    META.put(
        PROVIDER_NAME,
        new PropertyMD()
            .setMandatory()
            .setCanHaveSubkeys()
            .setDescription(
                "Name of the OAuth provider to be displayed. Can be localized with locale subkeys."));
    META.put(
        ICON_URL,
        new PropertyMD()
            .setCanHaveSubkeys()
            .setDescription(
                "URL to provider's logo. Can be http(s), file or data scheme. Can be localized."));
    META.put(
        CLIENT_ID,
        new PropertyMD()
            .setMandatory()
            .setDescription(
                "Client identifier, obtained during Unity's " + "registration at the provider"));
    META.put(
        CLIENT_SECRET,
        new PropertyMD()
            .setSecret()
            .setMandatory()
            .setDescription(
                "Client secret, obtained during Unity's " + "registration at the provider"));
    META.put(
        CLIENT_AUTHN_MODE,
        new PropertyMD(ClientAuthnMode.secretBasic)
            .setDescription(
                "Defines how the client secret and id should be passed to the provider."));
    META.put(
        SCOPES,
        new PropertyMD()
            .setDescription(
                "Space separated list of authorization scopes to be requested. "
                    + "Most often required if in non OpenID Connect mode, otherwise has a default "
                    + "value of 'openid email'"));
    META.put(
        ACCESS_TOKEN_FORMAT,
        new PropertyMD(AccessTokenFormat.standard)
            .setDescription(
                "Some providers (Facebook) use legacy format of a response to "
                    + "the access token query. Non standard format can be set here."));
    META.put(
        OPENID_CONNECT,
        new PropertyMD("false")
            .setDescription(
                "If set to true, then the provider is treated as OpenID "
                    + "Connect 1.0 provider. For such providers specifying "
                    + PROFILE_ENDPOINT
                    + " is not mandatory as the basic user information "
                    + "is retrieved together with access token. However the "
                    + "discovery endpoint must be set."));
    META.put(
        OPENID_DISCOVERY,
        new PropertyMD()
            .setDescription(
                "OpenID Connect Discovery endpoint address, relevant (and required) "
                    + "only when OpenID Connect mode is turned on."));
    META.put(
        CommonWebAuthnProperties.REGISTRATION_FORM,
        new PropertyMD()
            .setDescription(
                "Registration form to be shown for the locally unknown users which "
                    + "were successfuly authenticated remotely."));
    META.put(
        CommonWebAuthnProperties.TRANSLATION_PROFILE,
        new PropertyMD()
            .setMandatory()
            .setDescription(
                "Translation profile which will be used to map received user "
                    + "information to a local representation."));
    META.put(
        CommonWebAuthnProperties.ENABLE_ASSOCIATION,
        new PropertyMD("true")
            .setDescription(
                "If true then unknown remote user gets an option to associate "
                    + "the remote identity with an another local (already existing) account."));
    META.put(
        CLIENT_HOSTNAME_CHECKING,
        new PropertyMD(ServerHostnameCheckingMode.FAIL)
            .setDescription(
                "Controls how to react on the DNS name mismatch with "
                    + "the server's certificate. Unless in testing environment "
                    + "should be left on the default setting."));
    META.put(
        CLIENT_TRUSTSTORE,
        new PropertyMD()
            .setDescription(
                "Name of the truststore which should be used"
                    + " to validate TLS peer's certificates. "
                    + "If undefined then the system Java tuststore is used."));
  }

  private X509CertChainValidator validator = null;

  public CustomProviderProperties(Properties properties, String prefix, PKIManagement pkiManagement)
      throws ConfigurationException {
    super(prefix, properties, META, log);
    boolean openIdConnect = getBooleanValue(OPENID_CONNECT);
    if (openIdConnect) {
      if (!isSet(SCOPES)) setProperty(SCOPES, "openid email");
      if (!isSet(OPENID_DISCOVERY))
        throw new ConfigurationException(
            getKeyDescription(OPENID_DISCOVERY) + " is mandatory in OpenID Connect mode");

    } else {
      if (!isSet(PROVIDER_LOCATION))
        throw new ConfigurationException(
            getKeyDescription(PROVIDER_LOCATION) + " is mandatory in non OpenID Connect mode");
      if (!isSet(ACCESS_TOKEN_ENDPOINT))
        throw new ConfigurationException(
            getKeyDescription(ACCESS_TOKEN_ENDPOINT) + " is mandatory in non OpenID Connect mode");
      if (!isSet(PROFILE_ENDPOINT))
        throw new ConfigurationException(
            getKeyDescription(PROFILE_ENDPOINT) + " is mandatory in non OpenID Connect mode");
    }

    if (!isSet(PROVIDER_NAME))
      throw new ConfigurationException(getKeyDescription(PROVIDER_NAME) + " is mandatory");

    String validatorName = getValue(CLIENT_TRUSTSTORE);
    if (validatorName != null) {
      try {
        if (!pkiManagement.getValidatorNames().contains(validatorName))
          throw new ConfigurationException(
              "The validator "
                  + validatorName
                  + " for the OAuth verification client does not exist");
        validator = pkiManagement.getValidator(validatorName);
      } catch (EngineException e) {
        throw new ConfigurationException(
            "Can not establish the validator "
                + validatorName
                + " for the OAuth verification client",
            e);
      }
    }
  }

  public Properties getProperties() {
    return properties;
  }

  public X509CertChainValidator getValidator() {
    return validator;
  }
}