params = new HashMap<>();
+ params.put(PARAM_KEY_USERNAME, _config.getServiceAccountName());
+ params.put(PARAM_KEY_PASSWORD, _config.getServiceAccountPass());
+ JsonObject body = sendRequest(ENDPOINT_AUTH, params, false, POST);
+ JsonObject result = body.getJsonObject(JSON_KEY_RESULT);
+ JsonObject value = result.getJsonObject(JSON_KEY_VALUE);
+ _authToken = value.getString(JSON_KEY_TOKEN);
+ if (_authToken == null) {
+ _log.error("Failed to get authorization token.");
+ _log.error("Unable to read response from privacyIDEA.");
+ }
+ }
+}
diff --git a/src/main/java/org/privacyidea/authenticator/PrivacyIDEAAuthenticator.java b/src/main/java/org/privacyidea/authenticator/PrivacyIDEAAuthenticator.java
new file mode 100644
index 0000000..9a3fb95
--- /dev/null
+++ b/src/main/java/org/privacyidea/authenticator/PrivacyIDEAAuthenticator.java
@@ -0,0 +1,287 @@
+package org.privacyidea.authenticator;
+
+import org.jboss.logging.Logger;
+import org.keycloak.authentication.AuthenticationFlowContext;
+import org.keycloak.authentication.AuthenticationFlowError;
+import org.keycloak.forms.login.LoginFormsProvider;
+import org.keycloak.models.GroupModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserModel;
+
+import javax.json.JsonArray;
+import javax.json.JsonObject;
+import javax.ws.rs.core.MultivaluedMap;
+import javax.ws.rs.core.Response;
+import java.util.*;
+
+import static org.privacyidea.authenticator.Const.*;
+
+/**
+ * Copyright 2019 NetKnights GmbH - micha.preusser@netknights.it
+ * nils.behlen@netknights.it
+ * - Modified
+ *
+ * Based on original code:
+ *
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * 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.
+ */
+public class PrivacyIDEAAuthenticator implements org.keycloak.authentication.Authenticator {
+
+ private static Logger _log = Logger.getLogger(PrivacyIDEAAuthenticator.class);
+
+ private String _transactionID;
+ private String _currentUserName;
+ private Configuration _config;
+ private Endpoint _endpoint;
+
+ /**
+ * This function will be called when the authentication flow triggers the privacyIDEA execution.
+ * i.e. after the username + password have been submitted.
+ *
+ * @param context AuthenticationFlowContext
+ */
+ @Override
+ public void authenticate(AuthenticationFlowContext context) {
+ _config = new Configuration(context.getAuthenticatorConfig().getConfig());
+ _endpoint = new Endpoint(_config);
+
+ UserModel user = context.getUser();
+ _currentUserName = user.getUsername();
+
+ Set groupModelSet = user.getGroups();
+ GroupModel[] groupModels = groupModelSet.toArray(new GroupModel[0]);
+
+ // Check if privacyIDEA is enabled for the current user
+ for (GroupModel groupModel : groupModels) {
+ for (String excludedGroup : _config.getExcludedGroups()) {
+ if (groupModel.getName().equals(excludedGroup)) {
+ context.success();
+ return;
+ }
+ }
+ }
+
+ // Trigger challenge for current user
+ int tokenCounter = 0;
+ String tokenType = TOKEN_TYPE_OTP;
+ // Check which kinds of tokens the user has to adapt the options of the form
+ boolean userHasPushToken = false;
+ boolean userHasOTPToken = false;
+ // Collect the messages for the tokens to display
+ List pushMessages = new ArrayList<>();
+ List otpMessages = new ArrayList<>();
+ if (_config.doTriggerChallenge()) {
+ Map params = new HashMap<>();
+ params.put(PARAM_KEY_USER, _currentUserName);
+ JsonObject body = _endpoint.sendRequest(ENDPOINT_TRIGGERCHALLENGE, params, true, POST);
+ try {
+ JsonObject detail = body.getJsonObject(JSON_KEY_DETAIL);
+ JsonObject result = body.getJsonObject(JSON_KEY_RESULT);
+ tokenCounter = result.getInt(JSON_KEY_VALUE);
+ if (tokenCounter > 0) {
+ _transactionID = detail.getString(JSON_KEY_TRANSACTION_ID);
+ JsonArray multi_challenge = detail.getJsonArray(JSON_KEY_MULTI_CHALLENGE);
+ for (int i = 0; i < multi_challenge.size(); i++) {
+ JsonObject challenge = multi_challenge.getJsonObject(i);
+ String msg = challenge.getString(JSON_KEY_MESSAGE);
+ if (challenge.getString(JSON_KEY_TYPE).equals(TOKEN_TYPE_PUSH)) {
+ userHasPushToken = true;
+ if (!pushMessages.contains(msg)) {
+ pushMessages.add(msg);
+ }
+ } else {
+ userHasOTPToken = true;
+ if (!otpMessages.contains(msg)) {
+ otpMessages.add(msg);
+ }
+ }
+ }
+ if (userHasPushToken) {
+ tokenType = TOKEN_TYPE_PUSH;
+ }
+ }
+ } catch (Exception e) {
+ _log.error(e);
+ _log.error("Trigger challenge was not successful.");
+ }
+ }
+
+ // Enroll token if enabled and user does not have one (counted from trigger challenge)
+ String tokenEnrollmentQR = "";
+ if (_config.doEnrollToken() && tokenCounter == 0) {
+ // Check if user has a token
+ Map params = new HashMap<>();
+ params.put(PARAM_KEY_USER, _currentUserName);
+ /*JsonObject response = _endpoint.sendRequest(ENDPOINT_TOKEN, params, true, GET);
+
+ JsonObject result = response.getJsonObject(JSON_KEY_RESULT);
+ JsonObject value = result.getJsonObject(JSON_KEY_VALUE);
+ JsonArray tokens = value.getJsonArray(JSON_KEY_TOKENS);
+ log.info("tokens size:" + tokens.size()); */
+ //if (tokens.size() < 1) {
+ params.put(PARAM_KEY_TYPE, _config.getEnrollingTokenType());
+ params.put(PARAM_KEY_GENKEY, "1");
+ JsonObject response = _endpoint.sendRequest(ENDPOINT_TOKEN_INIT, params, true, POST);
+ try {
+ JsonObject detail = response.getJsonObject(JSON_KEY_DETAIL);
+ JsonObject googleurl = detail.getJsonObject(JSON_KEY_GOOGLEURL);
+ tokenEnrollmentQR = googleurl.getString(JSON_KEY_IMG);
+ } catch (Exception e) {
+ _log.error("Token enrollment failed");
+ }
+ //}
+ }
+ context.getAuthenticationSession().setAuthNote(AUTH_NOTE_AUTH_COUNTER, "0");
+
+ // Create login form
+ String pushMessage = Utilities.buildPromptMessage(pushMessages, DEFAULT_PUSH_MESSAGE);
+ String otpMessage = Utilities.buildPromptMessage(otpMessages, DEFAULT_OTP_MESSAGE);
+
+ Response challenge = context.form()
+ .setAttribute(FORM_PUSHTOKEN_INTERVAL, _config.getPushtokenPollingInterval().get(0))
+ .setAttribute(FORM_TOKEN_ENROLLMENT_QR, tokenEnrollmentQR)
+ .setAttribute(FORM_TOKENTYPE, tokenType)
+ .setAttribute(FORM_PUSHTOKEN, userHasPushToken)
+ .setAttribute(FORM_OTPTOKEN, userHasOTPToken)
+ .setAttribute(FORM_PUSH_MESSAGE, pushMessage)
+ .setAttribute(FORM_OTP_MESSAGE, otpMessage)
+ .createForm(FORM_FILE_NAME);
+ context.challenge(challenge);
+ }
+
+ /**
+ * This function will be called if the user submitted the OTP form
+ *
+ * @param context AuthenticationFlowContext
+ */
+ @Override
+ public void action(AuthenticationFlowContext context) {
+ MultivaluedMap formData = context.getHttpRequest().getDecodedFormParameters();
+ if (formData.containsKey("cancel")) {
+ context.cancelLogin();
+ return;
+ }
+ /*log.info("formData:");
+ formData.forEach((k, v) -> log.info("key=" + k + ", value=" + v)); */
+
+ // Get data from form
+ String tokenEnrollmentQR = formData.getFirst(FORM_TOKEN_ENROLLMENT_QR);
+ String tokenType = formData.getFirst(FORM_TOKENTYPE);
+ boolean pushToken = formData.getFirst(FORM_PUSHTOKEN).equals(TRUE);
+ boolean otpToken = formData.getFirst(FORM_OTPTOKEN).equals(TRUE);
+ String pushMessage = formData.getFirst(FORM_PUSH_MESSAGE);
+ String otpMessage = formData.getFirst(FORM_OTP_MESSAGE);
+ String tokenTypeChanged = formData.getFirst(FORM_TOKENTYPE_CHANGED);
+
+ if (!validateResponse(context)) {
+ int authCounter = Integer.parseInt(context.getAuthenticationSession().getAuthNote(AUTH_NOTE_AUTH_COUNTER)) + 1;
+ authCounter = (authCounter >= _config.getPushtokenPollingInterval().size() ? _config.getPushtokenPollingInterval().size() - 1 : authCounter);
+ context.getAuthenticationSession().setAuthNote(AUTH_NOTE_AUTH_COUNTER, Integer.toString(authCounter));
+
+ LoginFormsProvider form = context.form()
+ .setAttribute(FORM_PUSHTOKEN_INTERVAL, _config.getPushtokenPollingInterval().get(authCounter))
+ .setAttribute(FORM_TOKEN_ENROLLMENT_QR, tokenEnrollmentQR)
+ .setAttribute(FORM_TOKENTYPE, tokenType)
+ .setAttribute(FORM_PUSHTOKEN, pushToken)
+ .setAttribute(FORM_OTPTOKEN, otpToken)
+ .setAttribute(FORM_PUSH_MESSAGE, pushMessage == null ? DEFAULT_PUSH_MESSAGE : pushMessage)
+ .setAttribute(FORM_OTP_MESSAGE, otpMessage == null ? DEFAULT_OTP_MESSAGE : otpMessage);
+
+ // Dont display the error if the token type was switched
+ if (!tokenTypeChanged.equals(TRUE)) {
+ form.setError(tokenType.equals(TOKEN_TYPE_PUSH) ? "Authentication not verified yet." : "Authentication failed.");
+ //log.info("Authentication failed for user " + context.getUser().getUsername());
+ }
+ Response challenge = form.createForm(FORM_FILE_NAME);
+ context.failureChallenge(AuthenticationFlowError.INVALID_CREDENTIALS, challenge);
+ return;
+ }
+ context.success();
+ }
+
+ /**
+ * Check if authentication is successful
+ *
+ * @param context AuthenticationFlowContext
+ * @return true if authentication was successful, else false
+ */
+ private boolean validateResponse(AuthenticationFlowContext context) {
+ MultivaluedMap formData = context.getHttpRequest().getDecodedFormParameters();
+ if (formData.getFirst(FORM_TOKENTYPE_CHANGED).equals(TRUE)) {
+ return false;
+ }
+
+ // Get data from form
+ String tokenEnrollmentQR = formData.getFirst(FORM_TOKEN_ENROLLMENT_QR);
+ String tokenType = formData.getFirst(FORM_TOKENTYPE);
+
+ if (tokenType.equals(TOKEN_TYPE_PUSH)) {
+ Map params = new HashMap<>();
+ params.put(PARAM_KEY_TRANSACTION_ID, _transactionID);
+ JsonObject body = _endpoint.sendRequest(ENDPOINT_TOKEN_CHALLENGES, params, true, GET);
+ try {
+ JsonObject result = body.getJsonObject(JSON_KEY_RESULT);
+ JsonObject value = result.getJsonObject(JSON_KEY_VALUE);
+ JsonArray challenges = value.getJsonArray(JSON_KEY_CHALLENGES);
+ for (int i = 0; i < challenges.size(); i++) {
+ JsonObject challenge = challenges.getJsonObject(i);
+ if (challenge.getBoolean(JSON_KEY_OTP_VALID)) {
+ return true;
+ }
+ }
+ } catch (Exception e) {
+ _log.error("Push token verification failed.");
+ }
+ return false;
+ }
+
+ String otp = formData.getFirst(FORM_PI_OTP);
+ Map params = new HashMap<>();
+ params.put(PARAM_KEY_USER, _currentUserName);
+ params.put(PARAM_KEY_PASS, otp);
+ params.put(PARAM_KEY_REALM, _config.getRealm());
+ if (_config.doTriggerChallenge() && tokenEnrollmentQR.isEmpty()) {
+ params.put(PARAM_KEY_TRANSACTION_ID, _transactionID);
+ }
+ JsonObject body = _endpoint.sendRequest(ENDPOINT_VALIDATE_CHECK, params, false, POST);
+ try {
+ JsonObject result = body.getJsonObject(JSON_KEY_RESULT);
+ return result.getBoolean(JSON_KEY_VALUE);
+ } catch (Exception e) {
+ _log.error("Verification was not successful: Invalid response from privacyIDEA");
+ }
+ return false;
+ }
+
+ @Override
+ public boolean requiresUser() {
+ return true;
+ }
+
+ @Override
+ public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) {
+ return true;
+ }
+
+ @Override
+ public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) {
+ }
+
+ @Override
+ public void close() {
+ }
+}
diff --git a/src/main/java/org/privacyidea/authenticator/privacyIDEAAuthenticatorFactory.java b/src/main/java/org/privacyidea/authenticator/PrivacyIDEAAuthenticatorFactory.java
similarity index 74%
rename from src/main/java/org/privacyidea/authenticator/privacyIDEAAuthenticatorFactory.java
rename to src/main/java/org/privacyidea/authenticator/PrivacyIDEAAuthenticatorFactory.java
index 12c4808..7afec4f 100644
--- a/src/main/java/org/privacyidea/authenticator/privacyIDEAAuthenticatorFactory.java
+++ b/src/main/java/org/privacyidea/authenticator/PrivacyIDEAAuthenticatorFactory.java
@@ -1,9 +1,6 @@
package org.privacyidea.authenticator;
-import org.jboss.logging.Logger;
import org.keycloak.Config;
-import org.keycloak.authentication.Authenticator;
-import org.keycloak.authentication.AuthenticatorFactory;
import org.keycloak.authentication.ConfigurableAuthenticatorFactory;
import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.models.KeycloakSession;
@@ -13,8 +10,11 @@
import java.util.ArrayList;
import java.util.List;
+import static org.privacyidea.authenticator.Const.*;
+
/**
- * Copyright 2019 NetKnights GmbH - micha.preusser@neknights.it
+ * Copyright 2019 NetKnights GmbH - micha.preusser@netknights.it
+ * nils.behlen@netknights.it
* - Modified
*
* Based on original code:
@@ -34,23 +34,18 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-public class privacyIDEAAuthenticatorFactory implements AuthenticatorFactory, ConfigurableAuthenticatorFactory {
-
- //private static Logger log = Logger.getLogger(privacyIDEAAuthenticatorFactory.class);
+public class PrivacyIDEAAuthenticatorFactory implements org.keycloak.authentication.AuthenticatorFactory, ConfigurableAuthenticatorFactory {
- private static final String PROVIDER_ID = "privacyidea-authenticator";
- private static final privacyIDEAAuthenticator SINGLETON = new privacyIDEAAuthenticator();
+ private static final PrivacyIDEAAuthenticator SINGLETON = new PrivacyIDEAAuthenticator();
private static final List configProperties = new ArrayList<>();
@Override
public String getId() {
- //log.debug("AUTHENTICATOR FACTORY: GET ID");
return PROVIDER_ID;
}
@Override
- public Authenticator create(KeycloakSession session) {
- //log.debug("AUTHENTICATOR FACTORY: CREATE");
+ public org.keycloak.authentication.Authenticator create(KeycloakSession session) {
return SINGLETON;
}
@@ -61,105 +56,99 @@ public Authenticator create(KeycloakSession session) {
@Override
public AuthenticationExecutionModel.Requirement[] getRequirementChoices() {
- //log.debug("AUTHENTICATOR FACTORY: getRequirement Choices");
return REQUIREMENT_CHOICES;
}
@Override
public boolean isUserSetupAllowed() {
- // log.debug("AUTHENTICATOR FACTORY: isUserSetupAllowed");
return false;
}
@Override
public boolean isConfigurable() {
- //log.debug("AUTHENTICATOR FACTORY: isConfigurable");
return true;
}
@Override
public List getConfigProperties() {
- //log.debug("AUTHENTICATOR FACTORY: getConfigProperies");
return configProperties;
}
static {
- //log.debug("AUTHENTICATOR FACTORY: STATIC INIT");
-
ProviderConfigProperty piServerUrl = new ProviderConfigProperty();
piServerUrl.setType(ProviderConfigProperty.STRING_TYPE);
- piServerUrl.setName("piserver");
+ piServerUrl.setName(CONFIG_SERVER);
piServerUrl.setLabel("URL");
- piServerUrl.setHelpText("The URL to the privacyIDEA server");
+ piServerUrl.setHelpText("The URL of the privacyIDEA server");
configProperties.add(piServerUrl);
ProviderConfigProperty piRealm = new ProviderConfigProperty();
piRealm.setType(ProviderConfigProperty.STRING_TYPE);
- piRealm.setName("pirealm");
+ piRealm.setName(CONFIG_REALM);
piRealm.setLabel("Realm");
piRealm.setHelpText("Select the realm where your users are stored. Leave empty for default.");
configProperties.add(piRealm);
ProviderConfigProperty piVerifySSL = new ProviderConfigProperty();
piVerifySSL.setType(ProviderConfigProperty.BOOLEAN_TYPE);
- piVerifySSL.setName("piverifyssl");
+ piVerifySSL.setName(CONFIG_VERIFYSSL);
piVerifySSL.setLabel("Verify SSL");
piVerifySSL.setHelpText("Do not uncheck this in productive environment");
configProperties.add(piVerifySSL);
ProviderConfigProperty piDoTriggerChallenge = new ProviderConfigProperty();
piDoTriggerChallenge.setType(ProviderConfigProperty.BOOLEAN_TYPE);
- piDoTriggerChallenge.setName("pidotriggerchallenge");
+ piDoTriggerChallenge.setName(CONFIG_DOTRIGGERCHALLENGE);
piDoTriggerChallenge.setLabel("Enable trigger challenge");
piDoTriggerChallenge.setHelpText("Choose if you want to do trigger challenge");
configProperties.add(piDoTriggerChallenge);
ProviderConfigProperty piServiceAccount = new ProviderConfigProperty();
piServiceAccount.setType(ProviderConfigProperty.STRING_TYPE);
- piServiceAccount.setName("piserviceaccount");
+ piServiceAccount.setName(CONFIG_SERVICEACCOUNT);
piServiceAccount.setLabel("Service account");
piServiceAccount.setHelpText("Username of the service account. Needed for trigger challenge, token enrollment and push tokens.");
configProperties.add(piServiceAccount);
ProviderConfigProperty piServicePass = new ProviderConfigProperty();
piServicePass.setType(ProviderConfigProperty.PASSWORD);
- piServicePass.setName("piservicepass");
+ piServicePass.setName(CONFIG_SERVICEPASS);
piServicePass.setLabel("Service account password");
piServicePass.setHelpText("Password of the service account. Needed for trigger challenge, token enrollment and push tokens");
configProperties.add(piServicePass);
ProviderConfigProperty piExcludeGroups = new ProviderConfigProperty();
piExcludeGroups.setType(ProviderConfigProperty.STRING_TYPE);
- piExcludeGroups.setName("piexcludegroups");
+ piExcludeGroups.setName(CONFIG_EXCLUDEGROUPS);
piExcludeGroups.setLabel("Exclude groups");
piExcludeGroups.setHelpText("You can select groups, which will not do 2FA. Enter the group names and separate them with comma e.g. 'group1,group2'");
configProperties.add(piExcludeGroups);
ProviderConfigProperty piEnrollToken = new ProviderConfigProperty();
piEnrollToken.setType(ProviderConfigProperty.BOOLEAN_TYPE);
- piEnrollToken.setName("pienrolltoken");
+ piEnrollToken.setName(CONFIG_ENROLLTOKEN);
piEnrollToken.setLabel("Enable token enrollment");
- piEnrollToken.setHelpText("If enabled, the users can enroll a token themselves, if they do not have one yet. Service account is needed");
+ piEnrollToken.setHelpText("If enabled, the users can enroll a token themselves, if they do not have one yet. Trigger Challenge has to be enabled and a service account is needed");
piEnrollToken.setDefaultValue("false");
configProperties.add(piEnrollToken);
List tokenTypes = new ArrayList<>();
- tokenTypes.add("hotp");
- tokenTypes.add("totp");
+ tokenTypes.add("HOTP");
+ tokenTypes.add("TOTP");
ProviderConfigProperty piTokenType = new ProviderConfigProperty();
piTokenType.setType(ProviderConfigProperty.LIST_TYPE);
- piTokenType.setName("pienrolltokentype");
+ piTokenType.setName(CONFIG_ENROLLTOKENTYPE);
piTokenType.setLabel("Token type");
- piTokenType.setHelpText("Select the token type, the users can enroll, if they do not have a token yet. Service account is needed");
+ piTokenType.setHelpText("Select the token type that users can enroll, if they do not have a token yet. Service account is needed");
piTokenType.setOptions(tokenTypes);
- piTokenType.setDefaultValue("hotp");
+ piTokenType.setDefaultValue("HOTP");
configProperties.add(piTokenType);
ProviderConfigProperty piPushTokenInterval = new ProviderConfigProperty();
piPushTokenInterval.setType(ProviderConfigProperty.STRING_TYPE);
- piPushTokenInterval.setName("pipushtokeninterval");
+ piPushTokenInterval.setName(CONFIG_PUSHTOKENINTERVAL);
piPushTokenInterval.setLabel("Refresh interval for push tokens");
- piPushTokenInterval.setHelpText("Set refresh interval for push tokens in seconds. You can use a comma separated list. The last entry will be repeated.");
+ piPushTokenInterval.setHelpText("Set the refresh interval for push tokens in seconds. Use a comma separated list. The last entry will be repeated.");
configProperties.add(piPushTokenInterval);
}
diff --git a/src/main/java/org/privacyidea/authenticator/Utilities.java b/src/main/java/org/privacyidea/authenticator/Utilities.java
new file mode 100644
index 0000000..6f543d4
--- /dev/null
+++ b/src/main/java/org/privacyidea/authenticator/Utilities.java
@@ -0,0 +1,49 @@
+package org.privacyidea.authenticator;
+
+import javax.json.*;
+import javax.json.stream.JsonGenerator;
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+class Utilities {
+
+ private Configuration _config;
+
+ public Utilities(Configuration config) {
+ this._config = config;
+ }
+
+ static String prettyPrintJson(String json) {
+ StringWriter sw = new StringWriter();
+ try {
+ JsonReader jr = Json.createReader(new StringReader(json));
+ JsonObject jobj = jr.readObject();
+
+ Map properties = new HashMap<>(1);
+ properties.put(JsonGenerator.PRETTY_PRINTING, true);
+
+ JsonWriterFactory writerFactory = Json.createWriterFactory(properties);
+ JsonWriter jsonWriter = writerFactory.createWriter(sw);
+
+ jsonWriter.writeObject(jobj);
+ jsonWriter.close();
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ return sw.toString();
+ }
+
+ static String buildPromptMessage(List messages, String defaultMessage) {
+ String res = defaultMessage;
+ if (messages.size() > 1) {
+ res = messages.remove(0);
+ res += messages.stream().reduce("", (a, s) -> a += " or " + s);
+ } else if (messages.size() == 1) {
+ res = messages.get(0);
+ }
+ return res;
+ }
+}
diff --git a/src/main/java/org/privacyidea/authenticator/privacyIDEAAuthenticator.java b/src/main/java/org/privacyidea/authenticator/privacyIDEAAuthenticator.java
deleted file mode 100644
index 1bdb94d..0000000
--- a/src/main/java/org/privacyidea/authenticator/privacyIDEAAuthenticator.java
+++ /dev/null
@@ -1,460 +0,0 @@
-package org.privacyidea.authenticator;
-
-import org.jboss.logging.Logger;
-import org.keycloak.authentication.AuthenticationFlowContext;
-import org.keycloak.authentication.AuthenticationFlowError;
-import org.keycloak.authentication.Authenticator;
-import org.keycloak.forms.login.LoginFormsProvider;
-import org.keycloak.models.*;
-
-import javax.json.Json;
-import javax.json.JsonArray;
-import javax.json.JsonObject;
-import javax.json.JsonReader;
-import javax.net.ssl.*;
-import javax.ws.rs.core.MultivaluedMap;
-import javax.ws.rs.core.Response;
-import java.io.*;
-import java.net.HttpURLConnection;
-import java.net.URL;
-import java.net.URLEncoder;
-import java.nio.charset.StandardCharsets;
-import java.security.KeyManagementException;
-import java.security.NoSuchAlgorithmException;
-import java.util.*;
-
-/**
- * Copyright 2019 NetKnights GmbH - micha.preusser@neknights.it
- * - Modified
- *
- * Based on original code:
- *
- * Copyright 2016 Red Hat, Inc. and/or its affiliates
- * and other contributors as indicated by the @author tags.
- *
- * 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.
- */
-public class privacyIDEAAuthenticator implements Authenticator {
- public static final String CREDENTIAL_TYPE = "pi_otp";
-
- private static Logger log = Logger.getLogger(privacyIDEAAuthenticator.class);
-
- private String _serverURL;
- private String _realm;
- private boolean _doSSLVerify;
- private boolean _doTriggerChallenge;
- private String _serviceAccountName;
- private String _serviceAccountPass;
- private List _excludedGroups = new ArrayList<>();
- private boolean _doEnrollToken;
- private String _enrollingTokenType;
- private List _pushtokenPollingInterval = new ArrayList<>();
- private String _serviceAccountAuthToken;
-
- /**
- * This function will be called when the authentication flow triggers the privacyIDEA execution.
- *
- * @param context AuthenticationFlowContext
- */
- @Override
- public void authenticate(AuthenticationFlowContext context) {
- //log.debug("authenticate");
- String username;
- String tokenEnrollmentQR = "";
- String tokenType = "otp";
- boolean pushToken = false;
- boolean otpToken = false;
- StringBuilder pushMessageSB = null;
- StringBuilder otpMessageSB = null;
-
- UserModel user = context.getUser();
- username = user.getUsername();
-
- Set groupModelSet = user.getGroups();
- GroupModel[] groupModels = groupModelSet.toArray(new GroupModel[0]);
-
- AuthenticatorConfigModel acm = context.getAuthenticatorConfig();
-
- loadConfiguration(acm.getConfig());
-
- // Check if privacyIDEA is enabled for the current user
- for (GroupModel groupModel : groupModels) {
- for (String excludedGroup : _excludedGroups) {
- if (groupModel.getName().equals(excludedGroup)) {
- context.success();
- return;
- }
- }
- }
-
- int tokenCounter = 0;
-
- // Trigger challenge for current user
- if (_doTriggerChallenge) {
- Map params = new HashMap<>();
- params.put("username", _serviceAccountName);
- params.put("password", _serviceAccountPass);
- JsonObject body = send("/auth", params, null, "POST");
- try {
- JsonObject result = body.getJsonObject("result");
- JsonObject value = result.getJsonObject("value");
- _serviceAccountAuthToken = value.getString("token");
- } catch (Exception e) {
- log.error(e);
- log.error("Failed to get authorization token.");
- log.error("Unable to read response from privacyIDEA.");
- }
-
- params.put("user", username);
- body = send("/validate/triggerchallenge", params, _serviceAccountAuthToken, "POST");
-
- try {
- JsonObject detail = body.getJsonObject("detail");
- JsonObject result = body.getJsonObject("result");
- tokenCounter = result.getInt("value");
- if (tokenCounter > 0) {
- context.getAuthenticationSession().setAuthNote("pi.transaction_id", detail.getString("transaction_id"));
- JsonArray multi_challenge = detail.getJsonArray("multi_challenge");
- for (int i = 0; i < multi_challenge.size(); i++) {
- JsonObject challenge = multi_challenge.getJsonObject(i);
- if (challenge.getString("type").equals("push")) {
- pushToken = true;
- if (pushMessageSB == null) { // First time
- pushMessageSB = new StringBuilder().append(challenge.getString("message"));
- } else { // >1 times
- pushMessageSB.append(", ").append(challenge.getString("message"));
- }
- } else {
- otpToken = true;
- if (otpMessageSB == null) { // First time
- otpMessageSB = new StringBuilder().append(challenge.getString("message"));
- } else { // >1 times
- otpMessageSB.append(", ").append(challenge.getString("message"));
- }
- }
- }
- if (pushToken) {
- tokenType = "push";
- }
- }
- } catch (Exception e) {
- log.error(e);
- log.error("Trigger challenge was not successful.");
- }
-
- // Enroll token if enabled and user does not have one
- if (_doEnrollToken && tokenCounter == 0) {
- params.put("user", username);
- params.put("type", _enrollingTokenType);
- params.put("genkey", "1");
- body = send("/token/init", params, _serviceAccountAuthToken, "POST");
- try {
- JsonObject detail = body.getJsonObject("detail");
- JsonObject googleurl = detail.getJsonObject("googleurl");
- tokenEnrollmentQR = googleurl.getString("img");
- } catch (Exception e) {
- log.error("Token enrollment failed");
- }
- }
- }
-
- context.getAuthenticationSession().setAuthNote("authCounter", "0");
-
- // Create login form
- Response challenge = context.form()
- .setAttribute("pushTokenInterval", _pushtokenPollingInterval.get(0))
- .setAttribute("tokenEnrollmentQR", tokenEnrollmentQR)
- .setAttribute("tokenType", tokenType)
- .setAttribute("pushToken", pushToken)
- .setAttribute("otpToken", otpToken)
- .setAttribute("pushMessage", pushMessageSB == null ? "" : pushMessageSB.toString())
- .setAttribute("otpMessage", otpMessageSB == null ? "Please enter OTP" : otpMessageSB.toString())
- .createForm("privacyIDEA.ftl");
- context.challenge(challenge);
- }
-
- private void loadConfiguration(Map configMap) {
- _serverURL = configMap.get("piserver");
- _realm = configMap.get("pirealm") == null ? "" : configMap.get("pirealm");
- _doSSLVerify = configMap.get("piverifyssl") != null && configMap.get("piverifyssl").equals("true");
- _doTriggerChallenge = configMap.get("pidotriggerchallenge") != null && configMap.get("pidotriggerchallenge").equals("true");
- _serviceAccountName = configMap.get("piserviceaccount") == null ? "" : configMap.get("piserviceaccount");
- _serviceAccountPass = configMap.get("piservicepass") == null ? "" : configMap.get("piservicepass");
- _doEnrollToken = configMap.get("pienrolltoken") != null && configMap.get("pienrolltoken").equals("true");
- _enrollingTokenType = configMap.get("pienrolltokentype") == null ? "" : configMap.get("pienrolltokentype");
-
- String excludedGroupsStr = configMap.get("piexcludegroups");
- if (excludedGroupsStr != null) {
- _excludedGroups.addAll(Arrays.asList(excludedGroupsStr.split(",")));
- }
-
- // Set default, overwrite if configured
- _pushtokenPollingInterval.addAll(Arrays.asList(5, 1, 1, 1, 2, 3));
- String s = configMap.get("pipushtokeninterval");
- if (s != null) {
- List strPollingIntervals = Arrays.asList(s.split(","));
- if (!strPollingIntervals.isEmpty()) {
- _pushtokenPollingInterval.clear();
- for (String str : strPollingIntervals) {
- try {
- _pushtokenPollingInterval.add(Integer.parseInt(str));
- } catch (NumberFormatException e) {
- _pushtokenPollingInterval.add(3); // TODO
- }
- }
- }
- }
- }
-
- /**
- * This function will be called if the user submitted the form
- *
- * @param context AuthenticationFlowContext
- */
- @Override
- public void action(AuthenticationFlowContext context) {
- MultivaluedMap formData = context.getHttpRequest().getDecodedFormParameters();
- if (formData.containsKey("cancel")) {
- context.cancelLogin();
- return;
- }
-
- // Get data from form
- String tokenEnrollmentQR = formData.getFirst("tokenEnrollmentQR");
- String tokenType = formData.getFirst("tokenType");
- boolean pushToken = formData.getFirst("pushToken").equals("true");
- boolean otpToken = formData.getFirst("otpToken").equals("true");
- String transaction_id = context.getAuthenticationSession().getAuthNote("pi.transaction_id");
- String pushMessage = formData.getFirst("pushMessage");
- String otpMessage = formData.getFirst("otpMessage");
- String tokenTypeChanged = formData.getFirst("tokenTypeChanged");
-
- if (!validateAnswer(context)) {
- int authCounter = Integer.parseInt(context.getAuthenticationSession().getAuthNote("authCounter")) + 1;
- authCounter = (authCounter >= _pushtokenPollingInterval.size() ? _pushtokenPollingInterval.size() - 1 : authCounter);
- context.getAuthenticationSession().setAuthNote("authCounter", Integer.toString(authCounter));
-
- LoginFormsProvider form = context.form()
- .setAttribute("pushTokenInterval", _pushtokenPollingInterval.get(authCounter))
- .setAttribute("tokenEnrollmentQR", tokenEnrollmentQR)
- .setAttribute("tokenType", tokenType)
- .setAttribute("pushToken", pushToken)
- .setAttribute("otpToken", otpToken)
- .setAttribute("pushMessage", pushMessage == null ? "" : pushMessage)
- .setAttribute("otpMessage", otpMessage == null ? "" : otpMessage);
-
- if (!tokenType.equals("push") || !tokenTypeChanged.equals("true")) {
- form.setError("Authentication failed.");
- log.debug("Authentication failed for user " + context.getUser().getUsername());
- }
- Response challenge = form.createForm("privacyIDEA.ftl");
- context.failureChallenge(AuthenticationFlowError.INVALID_CREDENTIALS, challenge);
- return;
- }
- context.success();
- }
-
- /**
- * Check if authentication is successful
- *
- * @param context AuthenticationFlowContext
- * @return true if authentication was successful, else false
- */
- private boolean validateAnswer(AuthenticationFlowContext context) {
- MultivaluedMap formData = context.getHttpRequest().getDecodedFormParameters();
-
- UserModel user = context.getUser();
- String username = user.getUsername();
-
- // Get data from form
- String tokenEnrollmentQR = formData.getFirst("tokenEnrollmentQR");
- String tokenType = formData.getFirst("tokenType");
- boolean pushToken = formData.getFirst("pushToken").equals("true");
- boolean otpToken = formData.getFirst("otpToken").equals("true");
- String transaction_id = context.getAuthenticationSession().getAuthNote("pi.transaction_id");
- String pushMessage = formData.getFirst("pushMessage");
- String otpMessage = formData.getFirst("otpMessage");
-
- if (formData.getFirst("tokenTypeChanged").equals("true")) {
- return false;
- }
-
- if (tokenType.equals("push")) {
- Map params = new HashMap<>();
- params.put("transaction_id", transaction_id);
- JsonObject body = send("/token/challenges/", params, _serviceAccountAuthToken, "GET");
- try {
- JsonObject result = body.getJsonObject("result");
- JsonObject value = result.getJsonObject("value");
- JsonArray challenges = value.getJsonArray("challenges");
- for (int i = 0; i < challenges.size(); i++) {
- JsonObject challenge = challenges.getJsonObject(i);
- if (challenge.getBoolean("otp_valid")) {
- return true;
- }
- }
- } catch (Exception e) {
- log.error("Push token verification failed.");
- }
- return false;
- }
-
- String otp = formData.getFirst("pi_otp");
- Map params = new HashMap<>();
- params.put("user", username);
- params.put("pass", otp);
- params.put("realm", _realm);
- if (_doTriggerChallenge && tokenEnrollmentQR.equals("")) {
- params.put("transaction_id", transaction_id);
- }
- JsonObject body = send("/validate/check", params, null, "POST");
- try {
- JsonObject result = body.getJsonObject("result");
- return result.getBoolean("value");
- } catch (Exception e) {
- log.error("Verification was not successful: Invalid response from privacyIDEA");
- }
- return false;
- }
-
- /**
- * Make a http(s) call to the specified path, the URL is taken from the config.
- * If SSL Verification is turned off in the config, the endpoints certificate will not be verified.
- *
- * @param path Path to the API endpoint
- * @param params All necessary parameters for request
- * @param authToken The auth token for the service account (null, if not necessary)
- * @param method "POST" or "GET"
- * @return JsonObject body which contains the whole response
- */
- private JsonObject send(String path, Map params, String authToken, String method) {
- StringBuilder paramsSB = new StringBuilder();
- params.forEach((key, value) -> {
- try {
- paramsSB.append(key).append("=").append(URLEncoder.encode(value, StandardCharsets.UTF_8.toString())).append("&");
- } catch (Exception e) {
- log.error(e);
- }
- });
- paramsSB.deleteCharAt(paramsSB.length() - 1);
- try {
- URL piserverurl;
- if (method.equals("GET")) {
- piserverurl = new URL(_serverURL + path + "?" + paramsSB.toString());
- } else {
- piserverurl = new URL(_serverURL + path);
- }
-
- HttpURLConnection con;
- if (piserverurl.getProtocol().equals("https")) {
- con = (HttpsURLConnection) (piserverurl.openConnection());
- } else {
- con = (HttpURLConnection) (piserverurl.openConnection());
- }
-
- if (!_doSSLVerify && con instanceof HttpsURLConnection) {
- con = turnOffSSLVerification((HttpsURLConnection) con);
- }
-
- con.setDoOutput(true);
- con.setRequestMethod(method);
- if (authToken != null) {
- con.setRequestProperty("Authorization", authToken);
- }
- con.connect();
-
- if (method.equals("POST")) {
- byte[] outputBytes = (paramsSB.toString()).getBytes(StandardCharsets.UTF_8);
- OutputStream os = con.getOutputStream();
- os.write(outputBytes);
- os.close();
- }
-
- String response;
- try (InputStream is = con.getInputStream()) {
- BufferedReader br = new BufferedReader(new InputStreamReader(is));
- response = br.lines().reduce("", (a, s) -> a += s);
- }
- log.info("RESPONSE: " + response);
- JsonReader jsonReader = Json.createReader(new StringReader(response));
- JsonObject body = jsonReader.readObject();
- jsonReader.close();
-
- return body;
- } catch (Exception e) {
- log.error(e);
- }
- return null;
- }
-
- /**
- * This function will be called on every http request if doSSLVerify is set to false
- */
- private HttpsURLConnection turnOffSSLVerification(HttpsURLConnection con) {
- final TrustManager[] trustAllCerts = new TrustManager[]{
- new X509TrustManager() {
- @Override
- public void checkClientTrusted(java.security.cert.X509Certificate[] chain, String authType) {
- }
-
- @Override
- public void checkServerTrusted(java.security.cert.X509Certificate[] chain, String authType) {
- }
-
- @Override
- public java.security.cert.X509Certificate[] getAcceptedIssuers() {
- return new java.security.cert.X509Certificate[]{};
- }
- }
- };
- SSLContext sslContext = null;
- try {
- sslContext = SSLContext.getInstance("SSL");
- sslContext.init(null, trustAllCerts, new java.security.SecureRandom());
- } catch (NoSuchAlgorithmException | KeyManagementException e) {
- e.printStackTrace();
- }
-
- if (sslContext == null) {
- return con;
- }
-
- final SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();
- con.setSSLSocketFactory(sslSocketFactory);
- con.setHostnameVerifier((hostname, session) -> true);
-
- return con;
- }
-
- @Override
- public boolean requiresUser() {
- log.debug("requiresUser");
- return true;
- }
-
- @Override
- public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) {
- log.debug("configuredFor");
- return true;
- }
-
- @Override
- public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) {
- log.debug("setRequiredActions");
- }
-
- @Override
- public void close() {
- log.debug("close");
- }
-}
diff --git a/src/main/resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory b/src/main/resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory
index 2db4c2c..f68ea91 100644
--- a/src/main/resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory
+++ b/src/main/resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory
@@ -15,4 +15,4 @@
# limitations under the License.
#
-org.privacyidea.authenticator.privacyIDEAAuthenticatorFactory
\ No newline at end of file
+org.privacyidea.authenticator.PrivacyIDEAAuthenticatorFactory
\ No newline at end of file