diff --git a/src/main/java/org/privacyidea/authenticator/Configuration.java b/src/main/java/org/privacyidea/authenticator/Configuration.java index a2cca0e..3deaa73 100644 --- a/src/main/java/org/privacyidea/authenticator/Configuration.java +++ b/src/main/java/org/privacyidea/authenticator/Configuration.java @@ -21,7 +21,23 @@ import java.util.Map; import static org.privacyidea.PIConstants.TOKEN_TYPE_OTP; -import static org.privacyidea.authenticator.Const.*; +import static org.privacyidea.authenticator.Const.CONFIG_DO_LOG; +import static org.privacyidea.authenticator.Const.CONFIG_ENROLL_TOKEN; +import static org.privacyidea.authenticator.Const.CONFIG_ENROLL_TOKENTYPE; +import static org.privacyidea.authenticator.Const.CONFIG_EXCLUDED_GROUPS; +import static org.privacyidea.authenticator.Const.CONFIG_PREF_TOKENTYPE; +import static org.privacyidea.authenticator.Const.CONFIG_PUSH_INTERVAL; +import static org.privacyidea.authenticator.Const.CONFIG_REALM; +import static org.privacyidea.authenticator.Const.CONFIG_SEND_PASSWORD; +import static org.privacyidea.authenticator.Const.CONFIG_SERVER; +import static org.privacyidea.authenticator.Const.CONFIG_SERVICE_ACCOUNT; +import static org.privacyidea.authenticator.Const.CONFIG_SERVICE_PASS; +import static org.privacyidea.authenticator.Const.CONFIG_SERVICE_REALM; +import static org.privacyidea.authenticator.Const.CONFIG_TRIGGER_CHALLENGE; +import static org.privacyidea.authenticator.Const.CONFIG_VERIFY_SSL; +import static org.privacyidea.authenticator.Const.DEFAULT_POLLING_ARRAY; +import static org.privacyidea.authenticator.Const.DEFAULT_POLLING_INTERVAL; +import static org.privacyidea.authenticator.Const.TRUE; class Configuration { @@ -82,47 +98,47 @@ class Configuration { } } - String getServerURL() { + String serverURL() { return serverURL; } - String getRealm() { + String realm() { return realm; } - boolean doSSLVerify() { + boolean sslVerify() { return doSSLVerify; } - boolean doTriggerChallenge() { + boolean triggerChallenge() { return doTriggerChallenge; } - String getServiceAccountName() { + String serviceAccountName() { return serviceAccountName; } - String getServiceAccountPass() { + String serviceAccountPass() { return serviceAccountPass; } - String getServiceAccountRealm() { + String serviceAccountRealm() { return serviceAccountRealm; } - List getExcludedGroups() { + List excludedGroups() { return excludedGroups; } - boolean doEnrollToken() { + boolean enrollToken() { return doEnrollToken; } - String getEnrollingTokenType() { + String enrollingTokenType() { return enrollingTokenType; } - List getPollingInterval() { + List pollingInterval() { return pollingInterval; } @@ -130,11 +146,11 @@ boolean doLog() { return doLog; } - boolean doSendPassword() { + boolean sendPassword() { return doSendPassword; } - String getPrefTokenType() { + String prefTokenType() { return prefTokenType; } } diff --git a/src/main/java/org/privacyidea/authenticator/PrivacyIDEAAuthenticator.java b/src/main/java/org/privacyidea/authenticator/PrivacyIDEAAuthenticator.java index b964459..af1ee3b 100644 --- a/src/main/java/org/privacyidea/authenticator/PrivacyIDEAAuthenticator.java +++ b/src/main/java/org/privacyidea/authenticator/PrivacyIDEAAuthenticator.java @@ -23,7 +23,13 @@ package org.privacyidea.authenticator; import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.core.Response; import org.jboss.logging.Logger; import org.keycloak.authentication.AuthenticationFlowContext; import org.keycloak.authentication.AuthenticationFlowError; @@ -32,17 +38,40 @@ import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; -import org.privacyidea.*; - -import javax.ws.rs.core.MultivaluedMap; -import javax.ws.rs.core.Response; -import java.util.List; -import java.util.Optional; +import org.privacyidea.IPILogger; +import org.privacyidea.PIResponse; +import org.privacyidea.PrivacyIDEA; +import org.privacyidea.RolloutInfo; +import org.privacyidea.TokenInfo; +import org.privacyidea.WebAuthn; import static org.privacyidea.PIConstants.PASSWORD; import static org.privacyidea.PIConstants.TOKEN_TYPE_PUSH; import static org.privacyidea.PIConstants.TOKEN_TYPE_WEBAUTHN; -import static org.privacyidea.authenticator.Const.*; +import static org.privacyidea.authenticator.Const.AUTH_NOTE_ACCEPT_LANGUAGE; +import static org.privacyidea.authenticator.Const.AUTH_NOTE_AUTH_COUNTER; +import static org.privacyidea.authenticator.Const.AUTH_NOTE_TRANSACTION_ID; +import static org.privacyidea.authenticator.Const.DEFAULT_OTP_MESSAGE_DE; +import static org.privacyidea.authenticator.Const.DEFAULT_OTP_MESSAGE_EN; +import static org.privacyidea.authenticator.Const.DEFAULT_PUSH_MESSAGE_DE; +import static org.privacyidea.authenticator.Const.DEFAULT_PUSH_MESSAGE_EN; +import static org.privacyidea.authenticator.Const.FORM_FILE_NAME; +import static org.privacyidea.authenticator.Const.FORM_MODE; +import static org.privacyidea.authenticator.Const.FORM_MODE_CHANGED; +import static org.privacyidea.authenticator.Const.FORM_OTP; +import static org.privacyidea.authenticator.Const.FORM_OTP_AVAILABLE; +import static org.privacyidea.authenticator.Const.FORM_OTP_MESSAGE; +import static org.privacyidea.authenticator.Const.FORM_POLL_INTERVAL; +import static org.privacyidea.authenticator.Const.FORM_PUSH_AVAILABLE; +import static org.privacyidea.authenticator.Const.FORM_PUSH_MESSAGE; +import static org.privacyidea.authenticator.Const.FORM_TOKEN_ENROLLMENT_QR; +import static org.privacyidea.authenticator.Const.FORM_UI_LANGUAGE; +import static org.privacyidea.authenticator.Const.FORM_WEBAUTHN_ORIGIN; +import static org.privacyidea.authenticator.Const.FORM_WEBAUTHN_SIGN_REQUEST; +import static org.privacyidea.authenticator.Const.FORM_WEBAUTHN_SIGN_RESPONSE; +import static org.privacyidea.authenticator.Const.HEADER_ACCEPT_LANGUAGE; +import static org.privacyidea.authenticator.Const.PLUGIN_USER_AGENT; +import static org.privacyidea.authenticator.Const.TRUE; public class PrivacyIDEAAuthenticator implements org.keycloak.authentication.Authenticator, IPILogger { @@ -59,16 +88,20 @@ public class PrivacyIDEAAuthenticator implements org.keycloak.authentication.Aut */ @Override public void authenticate(AuthenticationFlowContext context) { + config = new Configuration(context.getAuthenticatorConfig().getConfig()); - privacyIDEA = new PrivacyIDEA.Builder(config.getServerURL(), PLUGIN_USER_AGENT) - .setSSLVerify(config.doSSLVerify()) - .setLogger(this) - .setPollingIntervals(config.getPollingInterval()) - .setRealm(config.getRealm()) - .setServiceAccount(config.getServiceAccountName(), config.getServiceAccountPass()) - .setServiceAccountRealm(config.getServiceAccountRealm()) - .build(); + if (this.privacyIDEA == null) { + privacyIDEA = PrivacyIDEA.newBuilder(config.serverURL(), PLUGIN_USER_AGENT) + .sslVerify(config.sslVerify()) + .logger(this) + .pollingIntervals(config.pollingInterval()) + .realm(config.realm()) + .serviceAccount(config.serviceAccountName(), config.serviceAccountPass()) + .serviceRealm(config.serviceAccountRealm()) + .build(); + privacyIDEA.logExcludedEndpoints(Collections.emptyList()); + } // Get the things that were submitted in the first username+password form UserModel user = context.getUser(); @@ -82,7 +115,7 @@ public void authenticate(AuthenticationFlowContext context) { // Check if the current user is member of an excluded group for (GroupModel groupModel : user.getGroups()) { - for (String excludedGroup : config.getExcludedGroups()) { + for (String excludedGroup : config.excludedGroups()) { if (excludedGroup.equals(groupModel.getName())) { context.success(); return; @@ -93,9 +126,9 @@ public void authenticate(AuthenticationFlowContext context) { // Get the language from the request headers to pass it to the ui and the privacyIDEA requests String acceptLanguage = context.getSession().getContext().getRequestHeaders().getRequestHeaders().get(HEADER_ACCEPT_LANGUAGE).get(0); String uiLanguage = "en"; - Map languageHeader = Collections.emptyMap(); + Map languageHeader = new LinkedHashMap<>(); if (acceptLanguage != null) { - languageHeader = Collections.singletonMap(HEADER_ACCEPT_LANGUAGE, acceptLanguage); + languageHeader.put(HEADER_ACCEPT_LANGUAGE, acceptLanguage); if (acceptLanguage.toLowerCase().startsWith("de")) { uiLanguage = "de"; } @@ -114,11 +147,11 @@ public void authenticate(AuthenticationFlowContext context) { String webAuthnSignRequest = ""; // Trigger challenges if configured. Service account has precedence over send password - if (config.doTriggerChallenge()) { + if (config.triggerChallenge()) { triggerResponse = privacyIDEA.triggerChallenges(currentUser, languageHeader); - } else if (config.doSendPassword()) { + } else if (config.sendPassword()) { if (currentPassword != null) { - triggerResponse = privacyIDEA.validateCheck(currentUser, currentPassword, languageHeader); + triggerResponse = privacyIDEA.validateCheck(currentUser, currentPassword, null, languageHeader); } else { log("Cannot send password because it is null!"); } @@ -126,40 +159,39 @@ public void authenticate(AuthenticationFlowContext context) { // Evaluate for possibly triggered token if (triggerResponse != null) { - transactionID = triggerResponse.getTransactionID(); + transactionID = triggerResponse.transactionID; - if (!triggerResponse.getMultiChallenge().isEmpty()) { - - pushAvailable = triggerResponse.isPushAvailable(); + if (!triggerResponse.multiChallenge().isEmpty()) { + pushAvailable = triggerResponse.pushAvailable(); if (pushAvailable) { - pushMessage = triggerResponse.getPushMessage(); + pushMessage = triggerResponse.pushMessage(); } - otpMessage = triggerResponse.getOTPMessage(); + otpMessage = triggerResponse.otpMessage(); // Check for WebAuthnSignRequest // TODO currently only gets the first sign request - if (triggerResponse.getTriggeredTokenTypes().contains(TOKEN_TYPE_WEBAUTHN)) { - List signRequests = triggerResponse.getWebAuthnSignRequests(); + if (triggerResponse.triggeredTokenTypes().contains(TOKEN_TYPE_WEBAUTHN)) { + List signRequests = triggerResponse.webAuthnSignRequests(); if (!signRequests.isEmpty()) { - webAuthnSignRequest = signRequests.get(0).getSignRequest(); + webAuthnSignRequest = signRequests.get(0).signRequest(); } } } // Check if any triggered token matches the preferred token type - if (triggerResponse.getTriggeredTokenTypes().contains(config.getPrefTokenType())) { - startingMode = config.getPrefTokenType(); + if (triggerResponse.triggeredTokenTypes().contains(config.prefTokenType())) { + startingMode = config.prefTokenType(); } } - // Enroll token if enabled and user does not have one + // Enroll token if enabled and user does not have one. If something was triggered before, don't even try. String tokenEnrollmentQR = ""; - if (config.doEnrollToken()) { + if (config.enrollToken() && (transactionID == null || transactionID.isEmpty())) { List tokenInfos = privacyIDEA.getTokenInfo(currentUser); if (tokenInfos == null || tokenInfos.isEmpty()) { - RolloutInfo rolloutInfo = privacyIDEA.tokenRollout(currentUser, config.getEnrollingTokenType()); + RolloutInfo rolloutInfo = privacyIDEA.tokenRollout(currentUser, config.enrollingTokenType()); tokenEnrollmentQR = rolloutInfo.googleurl.img; } } @@ -173,7 +205,7 @@ public void authenticate(AuthenticationFlowContext context) { } Response responseForm = context.form() - .setAttribute(FORM_POLL_INTERVAL, config.getPollingInterval().get(0)) + .setAttribute(FORM_POLL_INTERVAL, config.pollingInterval().get(0)) .setAttribute(FORM_TOKEN_ENROLLMENT_QR, tokenEnrollmentQR) .setAttribute(FORM_MODE, startingMode) .setAttribute(FORM_PUSH_AVAILABLE, pushAvailable) @@ -236,6 +268,7 @@ public void action(AuthenticationFlowContext context) { .setAttribute(FORM_UI_LANGUAGE, uiLanguage); boolean didTrigger = false; // To not show the error message if something was triggered + Future futureResponse = null; PIResponse response = null; // Determine to which endpoint we send the data from the form based on the mode the form was in @@ -253,7 +286,6 @@ public void action(AuthenticationFlowContext context) { response = privacyIDEA.validateCheckWebAuthn(currentUserName, transactionID, webAuthnSignResponse, origin, languageHeader); } } else { - if (!(TRUE.equals(tokenTypeChanged))) { String otp = formData.getFirst(FORM_OTP); // If the transaction id is not present, it will be not be added in validateCheck, so no need to check here @@ -264,32 +296,32 @@ public void action(AuthenticationFlowContext context) { // Evaluate the response if (response != null) { // On success we finish our execution - if (response.getValue()) { + if (response.value) { context.success(); return; } // If the authentication was not successful (yet), either the provided data was wrong // or another challenge was triggered - if (!response.getMultiChallenge().isEmpty()) { + if (!response.multiChallenge().isEmpty()) { // A challenge was triggered, display its message and save the transaction id in the session - otpMessage = response.getMessage(); - context.getAuthenticationSession().setAuthNote(AUTH_NOTE_TRANSACTION_ID, response.getTransactionID()); + otpMessage = response.message; + context.getAuthenticationSession().setAuthNote(AUTH_NOTE_TRANSACTION_ID, response.transactionID); didTrigger = true; } else { // The authentication failed without triggering anything so the things that have been sent before were wrong - authenticationFailureMessage += "\n" + response.getMessage(); + authenticationFailureMessage += "\n" + response.message; } } // The authCounter is also used to determine the polling interval for push // If the authCounter is bigger than the size of the polling interval list, repeat the lists last value int authCounter = Integer.parseInt(context.getAuthenticationSession().getAuthNote(AUTH_NOTE_AUTH_COUNTER)) + 1; - authCounter = (authCounter >= config.getPollingInterval().size() ? config.getPollingInterval().size() - 1 : authCounter); + authCounter = (authCounter >= config.pollingInterval().size() ? config.pollingInterval().size() - 1 : authCounter); context.getAuthenticationSession().setAuthNote(AUTH_NOTE_AUTH_COUNTER, Integer.toString(authCounter)); // The message variables could be overwritten if a challenge was triggered. Therefore, add them here at the end - form.setAttribute(FORM_POLL_INTERVAL, config.getPollingInterval().get(authCounter)) + form.setAttribute(FORM_POLL_INTERVAL, config.pollingInterval().get(authCounter)) .setAttribute(FORM_PUSH_MESSAGE, (pushMessage == null ? DEFAULT_PUSH_MESSAGE_EN : pushMessage)) .setAttribute(FORM_OTP_MESSAGE, (otpMessage == null ? DEFAULT_OTP_MESSAGE_EN : otpMessage)); @@ -318,8 +350,6 @@ public void setRequiredActions(KeycloakSession session, RealmModel realm, UserMo @Override public void close() { - // Just to make sure - privacyIDEA.stopPolling(); } // IPILogger implementation @@ -340,14 +370,14 @@ public void error(String message) { @Override public void log(Throwable t) { if (config.doLog()) { - logger.info(t); + logger.info("Exception:", t); } } @Override public void error(Throwable t) { if (config.doLog()) { - logger.error(t); + logger.error("Exception:", t); } } } diff --git a/src/main/java/org/privacyidea/authenticator/PrivacyIDEAAuthenticatorFactory.java b/src/main/java/org/privacyidea/authenticator/PrivacyIDEAAuthenticatorFactory.java index 0c9855e..aab7e17 100644 --- a/src/main/java/org/privacyidea/authenticator/PrivacyIDEAAuthenticatorFactory.java +++ b/src/main/java/org/privacyidea/authenticator/PrivacyIDEAAuthenticatorFactory.java @@ -22,16 +22,15 @@ */ package org.privacyidea.authenticator; +import java.util.ArrayList; import java.util.Arrays; +import java.util.List; import org.keycloak.Config; import org.keycloak.models.AuthenticationExecutionModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.provider.ProviderConfigProperty; -import java.util.ArrayList; -import java.util.List; - public class PrivacyIDEAAuthenticatorFactory implements org.keycloak.authentication.AuthenticatorFactory, org.keycloak.authentication.ConfigurableAuthenticatorFactory { private static final PrivacyIDEAAuthenticator SINGLETON = new PrivacyIDEAAuthenticator();