Skip to content

Commit

Permalink
Merge pull request #24 from privacyidea/11-feature-webauthn
Browse files Browse the repository at this point in the history
11 feature webauthn
  • Loading branch information
lukasmatusiewicz authored Sep 28, 2023
2 parents 8180fea + 8997747 commit 0fd9705
Show file tree
Hide file tree
Showing 17 changed files with 782 additions and 76 deletions.
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
<description>An IdP plugin providing MFA support.</description>
<groupId>org.privacyidea</groupId>
<artifactId>java-idp-plugin-privacyidea-parent</artifactId>
<version>0.2.0</version>
<version>0.3.0</version>
<packaging>pom</packaging>

<organization>
Expand Down
2 changes: 1 addition & 1 deletion privacyIDEA-api/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<parent>
<groupId>org.privacyidea</groupId>
<artifactId>java-idp-plugin-privacyidea-parent</artifactId>
<version>0.2.0</version>
<version>0.3.0</version>
</parent>

<artifactId>idp-plugin-privacyIDEA-api</artifactId>
Expand Down
3 changes: 2 additions & 1 deletion privacyIDEA-dist/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@
<parent>
<groupId>org.privacyidea</groupId>
<artifactId>java-idp-plugin-privacyidea-parent</artifactId>
<version>0.2.0</version>
<version>0.3.0</version>
</parent>

<artifactId>idp-plugin-privacyIDEA-dist</artifactId>
<name>Shibboleth IdP :: Plugins :: privacyIDEA Distribution</name>
<description>IdP privacyIDEA plugin packaging.</description>
<packaging>pom</packaging>


<properties>
<checkstyle.configLocation>${project.basedir}/../checkstyle.xml</checkstyle.configLocation>
<dist.plugin.finalName>idp-plugin-privacyIDEA-${project.version}</dist.plugin.finalName>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
#############################

# Available versions (e.g. 0.0.1 0.0.2)
org.privacyidea.privacyIDEA.versions = 0.1.0 0.2.0
org.privacyidea.privacyIDEA.versions = 0.1.0 0.2.0 0.3.0

#org.privacyidea.privacyIDEA.downloadURL.%{version} = https://lancelot.netknights.it/owncloud/s/XEuk1kV3veI9B7X?path=%2F0.0.1
org.privacyidea.privacyIDEA.baseName.%{version} = idp-plugin-privacyIDEA-dist-%{version}
Expand All @@ -14,7 +14,8 @@ org.privacyidea.privacyIDEA.idpVersionMax.%{version} = 5.0.0
org.privacyidea.privacyIDEA.idpVersionMin.%{version} = 4.3.0
# Support Level (OutOfDate or Current)
org.privacyidea.privacyIDEA.supportLevel.0.1.0 = OutOfDate
org.privacyidea.privacyIDEA.supportLevel.0.2.0 = Current
org.privacyidea.privacyIDEA.supportLevel.0.2.0 = OutOfDate
org.privacyidea.privacyIDEA.supportLevel.0.3.0 = Current

# Override basename for this version
#org.example.ExamplePlugin.baseName.1.0.0 = plugin-dist-old
2 changes: 1 addition & 1 deletion privacyIDEA-impl/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<parent>
<groupId>org.privacyidea</groupId>
<artifactId>java-idp-plugin-privacyidea-parent</artifactId>
<version>0.2.0</version>
<version>0.3.0</version>
</parent>

<artifactId>idp-plugin-privacyIDEA-impl</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import org.privacyidea.PIResponse;
import org.privacyidea.PrivacyIDEA;
import org.privacyidea.context.PIContext;
import org.privacyidea.context.PIFormContext;
import org.privacyidea.context.PIServerConfigContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand All @@ -27,12 +28,16 @@ public class AbstractChallengeResponseAction extends AbstractProfileAction imple
private static final Logger LOGGER = LoggerFactory.getLogger(AbstractChallengeResponseAction.class);
private PIServerConfigContext piServerConfigContext;
private PIContext piContext;
private PIFormContext piFormContext;
protected PrivacyIDEA privacyIDEA;
protected boolean debug = false;
@Nonnull
private final Function<ProfileRequestContext, PIContext> piContextLookupStrategy = (new ChildContextLookup(PIContext.class, false)).compose(
new ChildContextLookup(AuthenticationContext.class));
@Nonnull
private final Function<ProfileRequestContext, PIFormContext> piFormContextLookupStrategy = (new ChildContextLookup(PIFormContext.class, false)).compose(
new ChildContextLookup(AuthenticationContext.class));
@Nonnull
private final Function<ProfileRequestContext, PIServerConfigContext> piServerConfigLookupStrategy = (new ChildContextLookup(PIServerConfigContext.class, false)).compose(
new ChildContextLookup(AuthenticationContext.class));

Expand All @@ -58,22 +63,33 @@ protected final boolean doPreExecute(@Nonnull ProfileRequestContext profileReque
}
else
{
if (piServerConfigContext.getConfigParams().getDebug())
piFormContext = piFormContextLookupStrategy.apply(profileRequestContext);
if (piFormContext == null)
{
debug = piServerConfigContext.getConfigParams().getDebug();
LOGGER.error("{} Unable to create/access privacyIDEA form context.", this.getLogPrefix());
ActionSupport.buildEvent(profileRequestContext, "InvalidProfileContext");
return false;
}

if (privacyIDEA == null)
else
{
privacyIDEA = PrivacyIDEA.newBuilder(piServerConfigContext.getConfigParams().getServerURL(), "privacyIDEA-Shibboleth-Plugin")
.sslVerify(piServerConfigContext.getConfigParams().getVerifySSL())
.realm(piServerConfigContext.getConfigParams().getRealm())
.serviceAccount(piServerConfigContext.getConfigParams().getServiceName(), piServerConfigContext.getConfigParams().getServicePass())
.serviceRealm(piServerConfigContext.getConfigParams().getServiceRealm())
.logger(this)
.build();
if (piServerConfigContext.getConfigParams().getDebug())
{
debug = piServerConfigContext.getConfigParams().getDebug();
}

if (privacyIDEA == null)
{
privacyIDEA = PrivacyIDEA.newBuilder(piServerConfigContext.getConfigParams().getServerURL(), "privacyIDEA-Shibboleth-Plugin")
.sslVerify(piServerConfigContext.getConfigParams().getVerifySSL())
.realm(piServerConfigContext.getConfigParams().getRealm())
.serviceAccount(piServerConfigContext.getConfigParams().getServiceName(),
piServerConfigContext.getConfigParams().getServicePass())
.serviceRealm(piServerConfigContext.getConfigParams().getServiceRealm())
.logger(this)
.build();
}
return true;
}
return true;
}
}
}
Expand All @@ -88,9 +104,7 @@ protected final void doExecute(@Nonnull ProfileRequestContext profileRequestCont
this.doExecute(profileRequestContext, this.piContext, this.piServerConfigContext);
}

protected void doExecute(@Nonnull ProfileRequestContext profileRequestContext,
@Nonnull PIContext piContext,
@Nonnull PIServerConfigContext piServerConfigContext) {}
protected void doExecute(@Nonnull ProfileRequestContext profileRequestContext, @Nonnull PIContext piContext, @Nonnull PIServerConfigContext piServerConfigContext) {}

/**
* Extract challenge data from server response, and save it in context.
Expand All @@ -101,12 +115,16 @@ protected void extractChallengeData(@Nonnull PIResponse piResponse)
{
if (piResponse.message != null && !piResponse.message.isEmpty())
{
piContext.setMessage(piResponse.message);
piFormContext.setMessage(piResponse.message);
}
if (piResponse.transactionID != null && !piResponse.transactionID.isEmpty())
{
piContext.setTransactionID(piResponse.transactionID);
}
if (piResponse.triggeredTokenTypes().contains("webauthn"))
{
piContext.setWebauthnSignRequest(piResponse.mergedSignRequest());
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import org.opensaml.profile.context.ProfileRequestContext;
import org.privacyidea.context.Config;
import org.privacyidea.context.PIContext;
import org.privacyidea.context.PIFormContext;
import org.privacyidea.context.PIServerConfigContext;
import org.privacyidea.context.User;
import org.slf4j.Logger;
Expand Down Expand Up @@ -66,6 +67,7 @@ protected void doExecute(@NotNull ProfileRequestContext profileRequestContext, @
authenticationContext.addSubcontext(piServerConfigContext);
PIContext piContext;

PIContext piContext = new PIContext(user);
if (otpLength != null)
{
try
Expand All @@ -85,6 +87,10 @@ protected void doExecute(@NotNull ProfileRequestContext profileRequestContext, @
}
log.info("{} Create PIContext {}", this.getLogPrefix(), piContext);
authenticationContext.addSubcontext(piContext);

PIFormContext piFormContext = new PIFormContext(defaultMessage, otpFieldHint);
log.info("{} Create PIFormContext {}", this.getLogPrefix(), piFormContext);
authenticationContext.addSubcontext(piFormContext);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,60 +29,78 @@ protected final void doExecute(@Nonnull ProfileRequestContext profileRequestCont
}
else
{
if (request.getParameterValues("pi_otp_input") != null)
{
String otp = request.getParameterValues("pi_otp_input")[0];

PIResponse piResponse;
piContext.setMode(request.getParameterValues("mode")[0]);
piContext.setWebauthnSignResponse(request.getParameterValues("webauthnSignResponse")[0]);
piContext.setOrigin(request.getParameterValues("origin")[0]);
piContext.setFormErrorMessage(request.getParameterValues("errorMessage")[0]);
PIResponse piResponse = null;

if (otp != null)
if (piContext.getWebauthnSignResponse() != null && !piContext.getWebauthnSignResponse().isEmpty())
{
if (piContext.getOrigin() == null || piContext.getOrigin().isEmpty())
{
piResponse = privacyIDEA.validateCheck(piContext.getUsername(), otp, piContext.getTransactionID(), headers);
LOGGER.error("Origin is missing for WebAuthn authentication!");
}
else
{
if (debug)
{
LOGGER.info("{} Cannot send password because it is null!", this.getLogPrefix());
}
return;
piResponse = privacyIDEA.validateCheckWebAuthn(piContext.getUsername(), piContext.getTransactionID(), piContext.getWebauthnSignResponse(),
piContext.getOrigin(), headers);
}

if (piResponse != null)
}
else if (piContext.getMode().equals("otp"))
{
if (request.getParameterValues("pi_otp_input") != null)
{
extractChallengeData(piResponse);

if (piResponse.error != null)
String otp = request.getParameterValues("pi_otp_input")[0];
if (otp != null)
{
LOGGER.error("{} privacyIDEA server error: {}!", this.getLogPrefix(), piResponse.error.message);
ActionSupport.buildEvent(profileRequestContext, "AuthenticationException");
return;
piResponse = privacyIDEA.validateCheck(piContext.getUsername(), otp, piContext.getTransactionID(), headers);
}

if (!piResponse.multichallenge.isEmpty())
else
{
if (debug)
{
LOGGER.info("{} Another challenge encountered. Building form...", this.getLogPrefix());
LOGGER.info("{} Cannot send password because it is null!", this.getLogPrefix());
}
ActionSupport.buildEvent(profileRequestContext, "reload");
return;
}
}
}

if (piResponse != null)
{
extractChallengeData(piResponse);

if (piResponse.error != null)
{
LOGGER.error("{} privacyIDEA server error: {}!", this.getLogPrefix(), piResponse.error.message);
ActionSupport.buildEvent(profileRequestContext, "AuthenticationException");
return;
}

if (!piResponse.multichallenge.isEmpty())
{
if (debug)
{
LOGGER.info("{} Another challenge encountered. Building form...", this.getLogPrefix());
}
else if (piResponse.value)
ActionSupport.buildEvent(profileRequestContext, "reload");
}
else if (piResponse.value)
{
if (debug)
{
if (debug)
{
LOGGER.info("{} Authentication succeeded!", this.getLogPrefix());
}
ActionSupport.buildEvent(profileRequestContext, "success");
LOGGER.info("{} Authentication succeeded!", this.getLogPrefix());
}
else
ActionSupport.buildEvent(profileRequestContext, "success");
}
else
{
if (debug)
{
if (debug)
{
LOGGER.info("{} Received a server message. Building form...", this.getLogPrefix());
}
ActionSupport.buildEvent(profileRequestContext, "reload");
LOGGER.info("{} Received a server message. Building form...", this.getLogPrefix());
}
ActionSupport.buildEvent(profileRequestContext, "reload");
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package org.privacyidea.context;

import java.util.Objects;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import net.shibboleth.utilities.java.support.logic.Constraint;
Expand All @@ -10,12 +9,19 @@ public class PIContext extends BaseContext
{
@Nonnull
private final User user;
@Nonnull
private final String defaultMessage;
private String message = "";
@Nullable
private String transactionID = null;
@Nonnull
private String webauthnSignRequest = "";
@Nullable
private String webauthnSignResponse = null;
@Nullable
private String formErrorMessage = null;
@Nullable
private String origin = null;
@Nonnull
private String mode = "otp";
@Nonnull
private final String otpFieldHint;
@Nullable
private final Integer otpLength;
Expand All @@ -42,6 +48,31 @@ public PIContext(@Nonnull User user, @Nullable String defaultMessage, @Nullable
@Nullable
public String getTransactionID() {return transactionID;}

@Nullable
public String getWebauthnSignResponse() {return webauthnSignResponse;}

public void setWebauthnSignResponse(@Nullable String webauthnSignResponse) {this.webauthnSignResponse = webauthnSignResponse;}

@Nullable
public String getOrigin() {return origin;}

public void setOrigin(@Nullable String origin) {this.origin = origin;}

@Nonnull
public String getMode() {return mode;}

public void setMode(@Nonnull String mode) {this.mode = mode;}

@Nonnull
public String getWebauthnSignRequest() {return webauthnSignRequest;}

public void setWebauthnSignRequest(@Nonnull String webauthnSignRequest) {this.webauthnSignRequest = webauthnSignRequest;}

@Nullable
public String getFormErrorMessage() {return formErrorMessage;}

public void setFormErrorMessage(@Nullable String formErrorMessage) {this.formErrorMessage = formErrorMessage;}

@Nonnull
public String getOtpFieldHint() {return otpFieldHint;}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package org.privacyidea.context;

import java.util.Objects;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import org.opensaml.messaging.context.BaseContext;

public class PIFormContext extends BaseContext
{
@Nonnull
private final String defaultMessage;
private String message = "";
@Nonnull
private final String otpFieldHint;

public PIFormContext(@Nullable String defaultMessage, @Nullable String otpFieldHint)
{
this.defaultMessage = Objects.requireNonNullElse(defaultMessage, "Please enter the OTP!");
this.otpFieldHint = Objects.requireNonNullElse(otpFieldHint, "OTP");
}

public void setMessage(String message) {this.message = message;}

@Nonnull
public String getMessage() {return (!message.isEmpty()) ? message : defaultMessage;}

@Nonnull
public String getOtpFieldHint() {return otpFieldHint;}
}
Loading

0 comments on commit 0fd9705

Please sign in to comment.