Skip to content

Commit

Permalink
adapt name changes, fix printing stacktrace (#63)
Browse files Browse the repository at this point in the history
* Change method names to those sdk 0.3. is *going* to use
* Fixed printing throwables - stacktrace is now included
* Token rollout now checks the trigger response, possibly saving a request
* Language header map is no longer immutable so that things can be added to it by the sdk (like authorziation)
  • Loading branch information
nilsbehlen authored Apr 26, 2021
1 parent c5bd0b8 commit ee02449
Show file tree
Hide file tree
Showing 3 changed files with 110 additions and 65 deletions.
44 changes: 30 additions & 14 deletions src/main/java/org/privacyidea/authenticator/Configuration.java
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand Down Expand Up @@ -82,59 +98,59 @@ 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<String> getExcludedGroups() {
List<String> excludedGroups() {
return excludedGroups;
}

boolean doEnrollToken() {
boolean enrollToken() {
return doEnrollToken;
}

String getEnrollingTokenType() {
String enrollingTokenType() {
return enrollingTokenType;
}

List<Integer> getPollingInterval() {
List<Integer> pollingInterval() {
return pollingInterval;
}

boolean doLog() {
return doLog;
}

boolean doSendPassword() {
boolean sendPassword() {
return doSendPassword;
}

String getPrefTokenType() {
String prefTokenType() {
return prefTokenType;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 {

Expand All @@ -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();
Expand All @@ -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;
Expand All @@ -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<String, String> languageHeader = Collections.emptyMap();
Map<String, String> 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";
}
Expand All @@ -114,52 +147,51 @@ 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!");
}
}

// 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<WebAuthn> signRequests = triggerResponse.getWebAuthnSignRequests();
if (triggerResponse.triggeredTokenTypes().contains(TOKEN_TYPE_WEBAUTHN)) {
List<WebAuthn> 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<TokenInfo> 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;
}
}
Expand All @@ -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)
Expand Down Expand Up @@ -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<PIResponse> futureResponse = null;
PIResponse response = null;

// Determine to which endpoint we send the data from the form based on the mode the form was in
Expand All @@ -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
Expand All @@ -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));

Expand Down Expand Up @@ -318,8 +350,6 @@ public void setRequiredActions(KeycloakSession session, RealmModel realm, UserMo

@Override
public void close() {
// Just to make sure
privacyIDEA.stopPolling();
}

// IPILogger implementation
Expand All @@ -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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down

0 comments on commit ee02449

Please sign in to comment.