[java-idp-plugin-duo] branch main updated: JDUO-87 - Add an admin flow for managing the passwordless cookie
Scott Cantor
cantor.2 at osu.edu
Tue Apr 23 19:43:58 UTC 2024
This is an automated email from the git hooks/post-receive script.
scantor pushed a commit to branch main
in repository java-idp-plugin-duo.
View the commit online:
http://git.shibboleth.net/view/?p=java-idp-plugin-duo.git;a=commit;h=ed715b7ed5c94b2291c7988018df3e7ce6cfaaea
The following commit(s) were added to refs/heads/main by this push:
new ed715b7e JDUO-87 - Add an admin flow for managing the passwordless cookie
ed715b7e is described below
commit ed715b7ed5c94b2291c7988018df3e7ce6cfaaea
Author: Scott Cantor <cantor.2 at osu.edu>
AuthorDate: Tue Apr 23 15:43:55 2024 -0400
JDUO-87 - Add an admin flow for managing the passwordless cookie
https://shibboleth.atlassian.net/browse/JDUO-87
---
idp-duo-impl/pom.xml | 17 +-
.../authn/duo/admin/impl/AdminAccessPredicate.java | 78 ++++++++
.../authn/duo/admin/impl/AdminFlowDescriptor.java | 81 +++++++++
.../duo/admin/impl/ManagePasswordlessCookie.java | 197 +++++++++++++++++++++
.../plugin/authn/duo/admin/impl/package-info.java | 16 ++
.../plugin/authn/duo/audit/impl/package-info.java | 16 ++
.../authn/duo/impl/ClearPasswordlessCookie.java | 2 +-
.../authn/duo/impl/CreatePasswordlessCookie.java | 2 +-
.../META-INF/net.shibboleth.idp/postconfig.xml | 21 ++-
.../flows/admin/DuoOIDC/duo-oidc-admin-beans.xml | 40 +++++
.../flows/admin/DuoOIDC/duo-oidc-admin-flow.xml | 57 ++++++
.../idp/plugin/authn/duo/messages.properties | 10 ++
.../authn/duo/views/admin/passwordless-admin.vm | 98 ++++++++++
.../duo/nimbus/conf/authn/duo-oidc.properties | 9 +
.../idp/plugin/authn/duo/nimbus/module.properties | 2 +
.../authn/duo/sdk/conf/authn/duo-oidc.properties | 9 +
.../idp/plugin/authn/duo/sdk/module.properties | 2 +
17 files changed, 648 insertions(+), 9 deletions(-)
diff --git a/idp-duo-impl/pom.xml b/idp-duo-impl/pom.xml
index 95285957..2537c0c8 100644
--- a/idp-duo-impl/pom.xml
+++ b/idp-duo-impl/pom.xml
@@ -40,12 +40,7 @@
</dependency>
<dependency>
<groupId>${idp.groupId}</groupId>
- <artifactId>idp-saml-api</artifactId>
- <scope>provided</scope>
- </dependency>
- <dependency>
- <groupId>${idp.groupId}</groupId>
- <artifactId>idp-session-api</artifactId>
+ <artifactId>idp-admin-api</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
@@ -64,6 +59,16 @@
<artifactId>idp-profile-api</artifactId>
<scope>provided</scope>
</dependency>
+ <dependency>
+ <groupId>${idp.groupId}</groupId>
+ <artifactId>idp-saml-api</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>${idp.groupId}</groupId>
+ <artifactId>idp-session-api</artifactId>
+ <scope>provided</scope>
+ </dependency>
<dependency>
<groupId>${idp.groupId}</groupId>
<artifactId>idp-ui</artifactId>
diff --git a/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/admin/impl/AdminAccessPredicate.java b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/admin/impl/AdminAccessPredicate.java
new file mode 100644
index 00000000..1bc9c466
--- /dev/null
+++ b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/admin/impl/AdminAccessPredicate.java
@@ -0,0 +1,78 @@
+/*
+ * 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.
+ */
+
+package net.shibboleth.idp.plugin.authn.duo.admin.impl;
+
+import java.util.function.Supplier;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+
+import jakarta.servlet.http.HttpServletRequest;
+import net.shibboleth.profile.context.RelyingPartyContext;
+import net.shibboleth.profile.context.logic.AbstractRelyingPartyPredicate;
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.security.AccessControlService;
+
+/**
+ * Wrapper predicate for implementing access control check for admin flow.
+ */
+public class AdminAccessPredicate extends AbstractRelyingPartyPredicate {
+
+ /** Access control service. */
+ @NonnullAfterInit private AccessControlService accessControlService;
+
+ /** ServletRequest supplier. */
+ @Nullable private Supplier<HttpServletRequest> requestSupplier;
+
+ /**
+ * Sets {@link AccessControlService} to use for guarding admin access to cookie.
+ *
+ * @param service access control service
+ */
+ public void setAccessControlService(@Nonnull final AccessControlService service) {
+
+ accessControlService = Constraint.isNotNull(service, "AccessControlService cannot be null");
+ }
+
+ /**
+ * Sets the supplier of the active {@link HttpServletRequest}.
+ *
+ * @param supplier request supplier
+ */
+ public void setHttpServletRequestSupplier(@Nonnull final Supplier<HttpServletRequest> supplier) {
+ requestSupplier = Constraint.isNotNull(supplier, "HttpServletRequest supplier cannot be null");
+ }
+
+ /** {@inheritDoc} */
+ public boolean test(@Nullable final ProfileRequestContext input) {
+
+ final HttpServletRequest request = requestSupplier != null ? requestSupplier.get() : null;
+ if (request != null) {
+ final RelyingPartyContext rpCtx = getRelyingPartyContext(input);
+ if (rpCtx != null && rpCtx.getProfileConfig() instanceof AdminFlowDescriptor duo) {
+ final String policy = duo.getAdminPolicyName(input);
+ if (policy != null) {
+ return accessControlService.getInstance(policy).checkAccess(request, "write", "cookie");
+ }
+ }
+ }
+
+ return false;
+ }
+
+}
\ No newline at end of file
diff --git a/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/admin/impl/AdminFlowDescriptor.java b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/admin/impl/AdminFlowDescriptor.java
new file mode 100644
index 00000000..02272b70
--- /dev/null
+++ b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/admin/impl/AdminFlowDescriptor.java
@@ -0,0 +1,81 @@
+/*
+ * 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.
+ */
+
+package net.shibboleth.idp.plugin.authn.duo.admin.impl;
+
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+
+import net.shibboleth.idp.admin.BasicAdministrativeFlowDescriptor;
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.logic.FunctionSupport;
+import net.shibboleth.shared.primitive.StringSupport;
+
+/**
+ * Flow descriptor for Duo admin flow.
+ *
+ * <p>This is primarily for managing passwordless opt-in state, but may be extended for
+ * other purposes in the future.</p>
+ */
+public class AdminFlowDescriptor extends BasicAdministrativeFlowDescriptor {
+
+ /** Lookup strategy for access control policy to apply for determining admin vs. user-level access. */
+ @Nonnull private Function<ProfileRequestContext,String> adminPolicyNameLookupStrategy;
+
+ /**
+ * Constructor.
+ *
+ * @param id
+ */
+ public AdminFlowDescriptor(@Nonnull @NotEmpty final String id) {
+ super(id);
+
+ adminPolicyNameLookupStrategy = FunctionSupport.constant(null);
+ }
+
+ /**
+ * Get the access control policy name to apply for admin access.
+ *
+ * @param profileRequestContext profile request context
+ *
+ * @return admin policy name
+ */
+ @Nullable public String getAdminPolicyName(@Nullable final ProfileRequestContext profileRequestContext) {
+ return adminPolicyNameLookupStrategy.apply(profileRequestContext);
+ }
+
+ /**
+ * Set the explicit access control policy name to apply for admin access.
+ *
+ * @param name admin policy name
+ */
+ public void setAdminPolicyName(@Nullable final String name) {
+ adminPolicyNameLookupStrategy = FunctionSupport.constant(StringSupport.trimOrNull(name));
+ }
+
+ /**
+ * Set a lookup strategy to use to obtain the admin-level access control policy for this flow.
+ *
+ * @param strategy lookup strategy
+ */
+ public void setAdminPolicyNameLookupStrategy(@Nonnull final Function<ProfileRequestContext,String> strategy) {
+ adminPolicyNameLookupStrategy = Constraint.isNotNull(strategy, "Admin policy lookup strategy cannot be null");
+ }
+
+}
\ No newline at end of file
diff --git a/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/admin/impl/ManagePasswordlessCookie.java b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/admin/impl/ManagePasswordlessCookie.java
new file mode 100644
index 00000000..60caa511
--- /dev/null
+++ b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/admin/impl/ManagePasswordlessCookie.java
@@ -0,0 +1,197 @@
+/*
+ * 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.
+ */
+
+package net.shibboleth.idp.plugin.authn.duo.admin.impl;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.profile.action.ActionSupport;
+import org.opensaml.profile.action.EventIds;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+
+import jakarta.servlet.http.HttpServletRequest;
+import net.shibboleth.idp.authn.context.SubjectContext;
+import net.shibboleth.idp.plugin.authn.duo.PasswordlessCookieManager;
+import net.shibboleth.idp.profile.AbstractProfileAction;
+import net.shibboleth.profile.context.RelyingPartyContext;
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.annotation.constraint.NonnullBeforeExec;
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.security.AccessControlService;
+
+/**
+ * Administrative management of passwordless cookie by end-user or administrator.
+ *
+ * <p>Incoming form fields are used to determine what to do and for whom, with admin
+ * manipulation of a cookie for a different identity based on a separate access policy.
+ *
+ * @event {@link EventIds#PROCEED_EVENT_ID}
+ * @event {@link EventIds#INVALID_PROFILE_CTX}
+ * @event {@link EventIds#INVALID_MESSAGE}
+ * @event {@link EventIds#ACCESS_DENIED}
+ * @event {@link EventIds#IO_ERROR}
+ *
+ * @since 2.1.0
+ */
+public class ManagePasswordlessCookie extends AbstractProfileAction {
+
+ /** Name of form field signalling username to operate with. */
+ @Nonnull @NotEmpty public static final String USERNAME_FIELD_NAME = "j_username";
+
+ /** Name of form field signalling the operation to perform. */
+ @Nonnull @NotEmpty public static final String OPERATION_FIELD_NAME = "operation";
+
+ /** Class logger.*/
+ @Nonnull private final Logger log = LoggerFactory.getLogger(ManagePasswordlessCookie.class);
+
+ /** Access control service. */
+ @NonnullAfterInit private AccessControlService accessControlService;
+
+ /** Cookie manager to use. */
+ @NonnullAfterInit private PasswordlessCookieManager cookieManager;
+
+ /** Currently authenticated user. */
+ @NonnullBeforeExec private String currentUsername;
+
+ /**
+ * Sets {@link AccessControlService} to use for guarding admin access to cookie.
+ *
+ * @param service access control service
+ */
+ public void setAccessControlService(@Nonnull final AccessControlService service) {
+ checkSetterPreconditions();
+
+ accessControlService = Constraint.isNotNull(service, "AccessControlService cannot be null");
+ }
+
+ /**
+ * Sets {@link PasswordlessCookieManager} to use.
+ *
+ * @param manager cookie manager
+ */
+ public void setPasswordlessCookieManager(@Nullable final PasswordlessCookieManager manager) {
+ checkSetterPreconditions();
+
+ cookieManager = Constraint.isNotNull(manager, "PasswordlessCookieManager cannot be null");
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void doInitialize() throws ComponentInitializationException {
+ super.doInitialize();
+
+ if (accessControlService == null) {
+ throw new ComponentInitializationException("AccessControlService cannot be null");
+ } else if (cookieManager == null) {
+ throw new ComponentInitializationException("PasswordlessCookieManager cannot be null");
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+
+ if (!super.doPreExecute(profileRequestContext)) {
+ return false;
+ } else if (getHttpServletRequest() == null) {
+ log.warn("{} No HttpServletRequest available", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+ return false;
+ }
+
+ final SubjectContext subjectCtx = profileRequestContext.getSubcontext(SubjectContext.class);
+ currentUsername = subjectCtx != null ? subjectCtx.getPrincipalName() : null;
+
+ if (currentUsername == null) {
+ log.warn("{} No current username available", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, EventIds.ACCESS_DENIED);
+ return false;
+ }
+
+ return true;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+
+ final HttpServletRequest request = ensureHttpServletRequest();
+
+ final String operation = request.getParameter(OPERATION_FIELD_NAME);
+ if ("clear".equals(operation)) {
+ cookieManager.clearCookie();
+ } else if ("optout".equals(operation)) {
+ if (cookieManager.writeCookie(null)) {
+ log.debug("{} Created passwordless cookie for opt-out", getLogPrefix());
+ } else {
+ log.warn("{} Failed to create passwordless cookie for opt-out", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, EventIds.IO_ERROR);
+ }
+ } else if ("optin".equals(operation)) {
+ String targetName = request.getParameter(USERNAME_FIELD_NAME);
+ if (targetName == null) {
+ targetName = currentUsername;
+ } else if (!targetName.equals(currentUsername)) {
+ if (!isAdminAllowed(profileRequestContext, targetName)) {
+ ActionSupport.buildEvent(profileRequestContext, EventIds.ACCESS_DENIED);
+ return;
+ }
+ }
+
+ log.debug("{} Creating passwordless cookie for username '{}'", getLogPrefix(), targetName);
+
+ if (!cookieManager.writeCookie(targetName)) {
+ log.warn("{} Failed to create passwordless cookie for username '{}'", getLogPrefix(), targetName);
+ ActionSupport.buildEvent(profileRequestContext, EventIds.IO_ERROR);
+ }
+ }
+ }
+
+ /**
+ * Tests whether an admin operation (on a different user than is accessing the flow) should be permitted.
+ *
+ * @param profileRequestContext profile request context
+ * @param username username to operate on
+ *
+ * @return true iff the operation should be permitted
+ */
+ private boolean isAdminAllowed(@Nonnull final ProfileRequestContext profileRequestContext,
+ @Nonnull @NotEmpty final String username) {
+
+ final RelyingPartyContext rpCtx = profileRequestContext.getSubcontext(RelyingPartyContext.class);
+ if (rpCtx != null && rpCtx.getProfileConfig() instanceof AdminFlowDescriptor duo) {
+ final String policy = duo.getAdminPolicyName(profileRequestContext);
+ if (policy != null) {
+ log.debug("{} Checking for admin access to Duo passwordless cookie with policy {}", getLogPrefix(),
+ policy);
+
+ final boolean result = accessControlService.getInstance(policy).checkAccess(ensureHttpServletRequest(),
+ "write", username);
+ if (!result) {
+ log.warn("{} Access to passwordless cookie for username '{}' denied", getLogPrefix(), username);
+ }
+ return result;
+ }
+ }
+
+ log.warn("{} Unable to obtain admin access policy to use", getLogPrefix());
+ return false;
+ }
+
+}
\ No newline at end of file
diff --git a/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/admin/impl/package-info.java b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/admin/impl/package-info.java
new file mode 100644
index 00000000..d90882c7
--- /dev/null
+++ b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/admin/impl/package-info.java
@@ -0,0 +1,16 @@
+/*
+ * 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.
+ */
+
+/** Duo OIDC 2FA login flow admin implementation classes. */
+package net.shibboleth.idp.plugin.authn.duo.admin.impl;
diff --git a/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/audit/impl/package-info.java b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/audit/impl/package-info.java
new file mode 100644
index 00000000..509d4394
--- /dev/null
+++ b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/audit/impl/package-info.java
@@ -0,0 +1,16 @@
+/*
+ * 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.
+ */
+
+/** Duo OIDC 2FA login flow audit implementation classes. */
+package net.shibboleth.idp.plugin.authn.duo.audit.impl;
diff --git a/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/ClearPasswordlessCookie.java b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/ClearPasswordlessCookie.java
index d022a6b2..1d97fec7 100644
--- a/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/ClearPasswordlessCookie.java
+++ b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/ClearPasswordlessCookie.java
@@ -41,7 +41,7 @@ public class ClearPasswordlessCookie extends AbstractProfileAction {
/** Class logger. */
@Nonnull private final Logger log = LoggerFactory.getLogger(ClearPasswordlessCookie.class);
- /** Optional cookie manager to use. */
+ /** Cookie manager to use. */
@NonnullAfterInit private PasswordlessCookieManager cookieManager;
/**
diff --git a/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/CreatePasswordlessCookie.java b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/CreatePasswordlessCookie.java
index a6bdbb9a..679a3672 100644
--- a/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/CreatePasswordlessCookie.java
+++ b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/CreatePasswordlessCookie.java
@@ -56,7 +56,7 @@ public class CreatePasswordlessCookie extends AbstractProfileAction {
/** Class logger.*/
@Nonnull private final Logger log = LoggerFactory.getLogger(CreatePasswordlessCookie.class);
- /** Optional cookie manager to use. */
+ /** Cookie manager to use. */
@NonnullAfterInit private PasswordlessCookieManager cookieManager;
/**
diff --git a/idp-duo-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml b/idp-duo-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
index 61e88e1a..700042be 100644
--- a/idp-duo-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
+++ b/idp-duo-impl/src/main/resources/META-INF/net.shibboleth.idp/postconfig.xml
@@ -30,7 +30,26 @@
c:_0="#{'%{idp.authn.DuoOIDC.supportedPrincipals:%{idp.authn.Duo.supportedPrincipals:}}'.trim()}" />
</property>
</bean>
-
+
+ <!-- Admin flow descriptor -->
+ <bean class="net.shibboleth.idp.plugin.authn.duo.admin.impl.AdminFlowDescriptor"
+ c:id="http://shibboleth.net/ns/profiles/admin/duo"
+ p:loggingId="%{idp.duo.oidc.admin.logging:DuoOIDCAdmin}"
+ p:policyName="%{idp.duo.oidc.admin.accessPolicy:AccessByAnyone}"
+ p:adminPolicyName="%{idp.duo.oidc.admin.adminAccessPolicy:AccessByAdminUser}"
+ p:nonBrowserSupported="%{idp.duo.oidc.admin.nonBrowserSupported:false}"
+ p:authenticated="%{idp.duo.oidc.admin.authenticated:true}"
+ p:resolveAttributes="%{idp.duo.oidc.admin.resolveAttributes:false}">
+ <property name="postAuthenticationFlows">
+ <bean parent="shibboleth.CommaDelimStringArray"
+ c:_0="#{'%{idp.duo.oidc.admin.postAuthenticationFlows:}'.trim()}" />
+ </property>
+ <property name="defaultAuthenticationMethodsByString">
+ <bean parent="shibboleth.CommaDelimStringArray"
+ c:_0="#{'%{idp.duo.oidc.admin.defaultAuthenticationMethods:}'.trim()}" />
+ </property>
+ </bean>
+
<!-- Principal serialization support. -->
<bean p:id="duo" class="net.shibboleth.idp.authn.principal.GenericPrincipalService"
c:claz="net.shibboleth.idp.authn.duo.DuoPrincipal">
diff --git a/idp-duo-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/admin/DuoOIDC/duo-oidc-admin-beans.xml b/idp-duo-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/admin/DuoOIDC/duo-oidc-admin-beans.xml
new file mode 100644
index 00000000..6c11302d
--- /dev/null
+++ b/idp-duo-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/admin/DuoOIDC/duo-oidc-admin-beans.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<beans
+ xmlns="http://www.springframework.org/schema/beans"
+ xmlns:c="http://www.springframework.org/schema/c"
+ xmlns:context="http://www.springframework.org/schema/context"
+ xmlns:p="http://www.springframework.org/schema/p"
+ xmlns:util="http://www.springframework.org/schema/util"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
+ http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
+ http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd"
+ default-init-method="initialize"
+ default-destroy-method="destroy">
+
+ <!-- Profile ID for flow. -->
+ <bean id="shibboleth.AdminProfileId" class="java.lang.String" c:_0="http://shibboleth.net/ns/profiles/admin/duo" />
+
+ <!-- Profile counter name. -->
+ <bean id="shibboleth.metrics.ProfileCounter" class="java.lang.String" c:_0="net.shibboleth.idp.profiles.duo.admin" />
+
+ <!-- Default operation/resource suppliers for access checks. -->
+
+ <bean id="shibboleth.AdminOperationLookupStrategy" parent="shibboleth.Functions.Constant" c:target="write" />
+
+ <bean id="shibboleth.AdminResourceLookupStrategy" parent="shibboleth.Functions.Constant" c:target="cookie" />
+
+ <!-- Work beans. -->
+
+ <bean id="AdminLandingRedirect" class="java.lang.String" c:_0="%{idp.duo.oidc.admin.landingPage:https://example.org}" />
+
+ <bean id="AdminAccessPredicate" class="net.shibboleth.idp.plugin.authn.duo.admin.impl.AdminAccessPredicate"
+ p:httpServletRequestSupplier-ref="shibboleth.HttpServletRequestSupplier"
+ p:accessControlService-ref="shibboleth.AccessControlService" />
+
+ <bean id="ManagePasswordlessCookie" class="net.shibboleth.idp.plugin.authn.duo.admin.impl.ManagePasswordlessCookie" scope="prototype"
+ p:httpServletRequestSupplier-ref="shibboleth.HttpServletRequestSupplier"
+ p:accessControlService-ref="shibboleth.AccessControlService"
+ p:passwordlessCookieManager-ref="shibboleth.authn.DuoOIDC.Passwordless.CookieManager" />
+
+</beans>
diff --git a/idp-duo-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/admin/DuoOIDC/duo-oidc-admin-flow.xml b/idp-duo-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/admin/DuoOIDC/duo-oidc-admin-flow.xml
new file mode 100644
index 00000000..af88154d
--- /dev/null
+++ b/idp-duo-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/admin/DuoOIDC/duo-oidc-admin-flow.xml
@@ -0,0 +1,57 @@
+<flow xmlns="http://www.springframework.org/schema/webflow"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://www.springframework.org/schema/webflow http://www.springframework.org/schema/webflow/spring-webflow.xsd"
+ parent="admin.abstract">
+
+ <!-- Start action. -->
+
+ <action-state id="InitializeProfileRequestContext">
+ <evaluate expression="InitializeProfileRequestContext" />
+<!-- <evaluate expression="FlowStartPopulateAuditContext" />-->
+ <evaluate expression="'proceed'" />
+
+ <!-- Branch to determine if authentication is required. -->
+ <transition on="proceed" to="DoAdminPreamble" />
+ </action-state>
+
+ <!-- Resume actual flow processing. -->
+
+ <action-state id="DoProfileWork">
+ <evaluate expression="CheckAccess" />
+ <evaluate expression="'proceed'" />
+
+ <transition on="proceed" to="AdminUI" />
+ </action-state>
+
+ <view-state id="AdminUI" view="admin/passwordless-admin">
+ <on-entry>
+ <evaluate expression="environment" result="viewScope.environment" />
+ <evaluate expression="opensamlProfileRequestContext" result="viewScope.profileRequestContext" />
+ <evaluate expression="opensamlProfileRequestContext.ensureSubcontext(T(net.shibboleth.idp.authn.context.SubjectContext)).getPrincipalName()" result="viewScope.username" />
+ <evaluate expression="AdminAccessPredicate.test(opensamlProfileRequestContext)" result="viewScope.admin" />
+ <evaluate expression="T(net.shibboleth.shared.codec.HTMLEncoder)" result="viewScope.encoder" />
+ <evaluate expression="flowRequestContext.getActiveFlow().getApplicationContext().getBean('shibboleth.authn.DuoOIDC.Passwordless.CookieManager')" result="viewScope.cookieManager" />
+ <evaluate expression="flowRequestContext.getActiveFlow().getApplicationContext().getBean('shibboleth.CSPDigester')" result="viewScope.cspDigester" />
+ <evaluate expression="flowRequestContext.getActiveFlow().getApplicationContext().getBean('shibboleth.CSPNonce')" result="viewScope.cspNonce" />
+ <evaluate expression="flowRequestContext.getExternalContext().getNativeRequest()" result="viewScope.request" />
+ <evaluate expression="flowRequestContext.getExternalContext().getNativeResponse()" result="viewScope.response" />
+ <evaluate expression="flowRequestContext.getActiveFlow().getApplicationContext().containsBean('shibboleth.CustomViewContext') ? flowRequestContext.getActiveFlow().getApplicationContext().getBean('shibboleth.CustomViewContext') : null" result="viewScope.custom" />
+ </on-entry>
+
+ <transition on="proceed" to="ManagePasswordlessCookie" />
+ <transition on="end" to="end" />
+ </view-state>
+
+ <action-state id="ManagePasswordlessCookie">
+ <evaluate expression="ManagePasswordlessCookie" />
+ <evaluate expression="'proceed'" />
+
+ <transition on="proceed" to="AdminUI" />
+ </action-state>
+
+ <!-- Terminus -->
+ <end-state id="end" view="externalRedirect:#{flowRequestContext.getActiveFlow().getApplicationContext().getBean('AdminLandingRedirect')}" />
+
+ <bean-import resource="duo-oidc-admin-beans.xml" />
+
+</flow>
diff --git a/idp-duo-impl/src/main/resources/net/shibboleth/idp/plugin/authn/duo/messages.properties b/idp-duo-impl/src/main/resources/net/shibboleth/idp/plugin/authn/duo/messages.properties
index b97030f7..8390a9fc 100644
--- a/idp-duo-impl/src/main/resources/net/shibboleth/idp/plugin/authn/duo/messages.properties
+++ b/idp-duo-impl/src/main/resources/net/shibboleth/idp/plugin/authn/duo/messages.properties
@@ -17,3 +17,13 @@ idp.duo.enrollment.url = #
idp.duo.passwordless.optin = Default to using your Passkey or device to login in the future?
idp.duo.passwordless.optin.yes = Yes
idp.duo.passwordless.optin.no = No
+
+idp.duo.passwordless.admin.title = Manage Duo Passwordless Opt-In Cookie
+idp.duo.passwordless.admin.current = Current State:
+idp.duo.passwordless.admin.optedout = Opted Out
+idp.duo.passwordless.admin.optedin = Opted In As
+idp.duo.passwordless.admin.unset = Not Set
+idp.duo.passwordless.admin.optin = Opt-In
+idp.duo.passwordless.admin.optout = Opt-Out
+idp.duo.passwordless.admin.clear = Clear
+idp.duo.passwordless.admin.finish = Finish
diff --git a/idp-duo-impl/src/main/resources/net/shibboleth/idp/plugin/authn/duo/views/admin/passwordless-admin.vm b/idp-duo-impl/src/main/resources/net/shibboleth/idp/plugin/authn/duo/views/admin/passwordless-admin.vm
new file mode 100644
index 00000000..d5d76688
--- /dev/null
+++ b/idp-duo-impl/src/main/resources/net/shibboleth/idp/plugin/authn/duo/views/admin/passwordless-admin.vm
@@ -0,0 +1,98 @@
+##
+## Velocity Template for collection of username for Duo Passwordless use
+##
+## Velocity context will contain the following properties
+## flowExecutionUrl - the form action location
+## flowRequestContext - the Spring Web Flow RequestContext
+## flowExecutionKey - the SWF execution key (this is built into the flowExecutionUrl)
+## profileRequestContext - root of context tree
+## admin - boolean flag indicating admin access to override username
+## username - authenticated username
+## cookieManager - PasswordlessCookieManager object
+## encoder - HTMLEncoder class
+## cspDigester - Calculates base64-encoded SHA-2 hashes (call apply)
+## cspNonce - Calculates secure nonces (call generateIdentifier)
+## request - HttpServletRequest
+## response - HttpServletResponse
+## environment - Spring Environment object for property resolution
+## custom - arbitrary object injected by deployer
+##
+#set ($optout = $cookieManager.isOptOut())
+#set ($currentUsername = $cookieManager.readCookie())
+#if (!$optout && $currentUsername)
+ #set ($username = $currentUsername)
+#end
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>#springMessageText("idp.title", "Web Login Service")</title>
+ <meta charset="UTF-8" />
+ <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5.0">
+ <link rel="stylesheet" type="text/css" href="$request.getContextPath()#springMessageText("idp.css", "/css/placeholder.css")">
+ </head>
+ <body>
+ <main class="main">
+ <header>
+ <img class="main-logo" src="$request.getContextPath()#springMessageText("idp.logo", "/images/placeholder-logo.png")" alt="#springMessageText("idp.logo.alt-text", "logo")" />
+ </header>
+
+ <section>
+ <h1>#springMessageText("idp.duo.passwordless.admin.title", "Manage Duo Passwordless Opt-In Cookie")</h1>
+
+ <form action="$flowExecutionUrl" method="post">
+ #parse("csrf/csrf.vm")
+
+ <input type="hidden" name="_eventId_proceed" value="1" />
+
+ <p>
+ <strong>#springMessageText("idp.duo.passwordless.admin.current", "Current State:")</strong>
+ #if ($optout)
+ #springMessageText("idp.duo.passwordless.admin.optedout", "Opted Out")
+ #elseif ($currentUsername)
+ #springMessageText("idp.duo.passwordless.admin.optedin", "Opted In As")
+ $encoder.encodeForHTML($currentUsername)
+ #else
+ #springMessageText("idp.duo.passwordless.admin.unset", "Not Set")
+ #end
+ </p>
+
+ #if ($admin)
+ <label for="j_username">#springMessageText("idp.login.username", "Username")</label>
+ <input id="j_username" name="j_username" type="text" value="$encoder.encodeForHTML($username)" />
+ #end
+
+ <div class="grid">
+ <div class="grid-item">
+ <button type="submit" name="operation" value="optin"
+ >#springMessageText("idp.duo.passwordless.admin.optin", "Opt-In")</button>
+ </div>
+ <div class="grid-item">
+ <button type="submit" name="operation" value="optout"
+ >#springMessageText("idp.duo.passwordless.admin.optout", "Opt-Out")</button>
+ </div>
+ <div class="grid-item">
+ <button type="submit" name="operation" value="clear"
+ >#springMessageText("idp.duo.passwordless.admin.clear", "Clear")</button>
+ </div>
+ <div class="grid-item">
+ <button type="submit" formaction="$flowExecutionUrl&_eventId_end=1#parse("csrf/csrf-qparam.vm")"
+ >#springMessageText("idp.duo.passwordless.admin.finish", "Finish")</button>
+ </div>
+ </div>
+ </form>
+
+ <ul>
+ <li><a href="#springMessageText('idp.duo.enrollment.url', '#')">#springMessageText("idp.duo.enrollment", "Enroll New Devices")</a></li>
+ <li><a href="#springMessageText('idp.url.helpdesk', '#')">#springMessageText("idp.login.needHelp", "Need Help?")</a></li>
+ </ul>
+ </section>
+ </main>
+ <footer class="footer">
+ <div class="cc">
+ <p>#springMessageText("idp.footer", "Insert your footer text here.")</p>
+ </div>
+ </footer>
+
+ </body>
+</html>
\ No newline at end of file
diff --git a/idp-duo-nimbus-client-impl/src/main/resources/net/shibboleth/idp/plugin/authn/duo/nimbus/conf/authn/duo-oidc.properties b/idp-duo-nimbus-client-impl/src/main/resources/net/shibboleth/idp/plugin/authn/duo/nimbus/conf/authn/duo-oidc.properties
index bbff2d11..43a4f082 100644
--- a/idp-duo-nimbus-client-impl/src/main/resources/net/shibboleth/idp/plugin/authn/duo/nimbus/conf/authn/duo-oidc.properties
+++ b/idp-duo-nimbus-client-impl/src/main/resources/net/shibboleth/idp/plugin/authn/duo/nimbus/conf/authn/duo-oidc.properties
@@ -49,6 +49,15 @@ idp.duo.oidc.redirectURL = https://<hostname>:<port>/idp/profile/Authn/Duo/2FA/d
# Override to plug in your own condition bean governing eligibility.
#idp.duo.oidc.passwordless.guardCondition = (internal default)
+# Settings for Passwordless cookie management flow
+#idp.duo.oidc.admin.resolveAttributes = false
+# Policy for access by people in general
+#idp.duo.oidc.admin.accessPolicy = AccessByAnyone
+# Policy for access by administrators
+#idp.duo.oidc.admin.adminAccessPolicy = AccessByAdminUser
+# Destination after flow completion.
+idp.duo.oidc.admin.landingPage = https://example.org
+
# Non-Browser AuthAPI integration if desired
#idp.duo.oidc.nonbrowser.apiHost = %{idp.duo.oidc.apiHost}
#idp.duo.oidc.nonbrowser.integrationKey = ikey
diff --git a/idp-duo-nimbus-client-impl/src/main/resources/net/shibboleth/idp/plugin/authn/duo/nimbus/module.properties b/idp-duo-nimbus-client-impl/src/main/resources/net/shibboleth/idp/plugin/authn/duo/nimbus/module.properties
index 0a66858c..1a9351a5 100644
--- a/idp-duo-nimbus-client-impl/src/main/resources/net/shibboleth/idp/plugin/authn/duo/nimbus/module.properties
+++ b/idp-duo-nimbus-client-impl/src/main/resources/net/shibboleth/idp/plugin/authn/duo/nimbus/module.properties
@@ -17,3 +17,5 @@ idp.authn.DuoOIDC.3.src = /net/shibboleth/idp/plugin/authn/duo/views/passwordles
idp.authn.DuoOIDC.3.dest = views/passwordless.vm
idp.authn.DuoOIDC.4.src = /net/shibboleth/idp/plugin/authn/duo/views/passwordless-optin.vm
idp.authn.DuoOIDC.4.dest = views/passwordless-optin.vm
+idp.authn.DuoOIDC.5.src = /net/shibboleth/idp/plugin/authn/duo/views/admin/passwordless-admin.vm
+idp.authn.DuoOIDC.5.dest = views/admin/passwordless-admin.vm
diff --git a/idp-duo-sdk-client-impl/src/main/resources/net/shibboleth/idp/plugin/authn/duo/sdk/conf/authn/duo-oidc.properties b/idp-duo-sdk-client-impl/src/main/resources/net/shibboleth/idp/plugin/authn/duo/sdk/conf/authn/duo-oidc.properties
index dea141c9..0de63343 100644
--- a/idp-duo-sdk-client-impl/src/main/resources/net/shibboleth/idp/plugin/authn/duo/sdk/conf/authn/duo-oidc.properties
+++ b/idp-duo-sdk-client-impl/src/main/resources/net/shibboleth/idp/plugin/authn/duo/sdk/conf/authn/duo-oidc.properties
@@ -49,6 +49,15 @@ idp.duo.oidc.redirectURL = https://<hostname>:<port>/idp/profile/Authn/Duo/2FA/d
# Override to plug in your own condition bean governing eligibility.
#idp.duo.oidc.passwordless.guardCondition = (internal default)
+# Settings for Passwordless cookie management flow
+#idp.duo.oidc.admin.resolveAttributes = false
+# Policy for access by people in general
+#idp.duo.oidc.admin.accessPolicy = AccessByAnyone
+# Policy for access by administrators
+#idp.duo.oidc.admin.adminAccessPolicy = AccessByAdminUser
+# Destination after flow completion.
+idp.duo.oidc.admin.landingPage = https://example.org
+
# Non-Browser AuthAPI integration if desired
#idp.duo.oidc.nonbrowser.apiHost = %{idp.duo.oidc.apiHost}
#idp.duo.oidc.nonbrowser.integrationKey = ikey
diff --git a/idp-duo-sdk-client-impl/src/main/resources/net/shibboleth/idp/plugin/authn/duo/sdk/module.properties b/idp-duo-sdk-client-impl/src/main/resources/net/shibboleth/idp/plugin/authn/duo/sdk/module.properties
index 36a21ef7..2b1b4a6b 100644
--- a/idp-duo-sdk-client-impl/src/main/resources/net/shibboleth/idp/plugin/authn/duo/sdk/module.properties
+++ b/idp-duo-sdk-client-impl/src/main/resources/net/shibboleth/idp/plugin/authn/duo/sdk/module.properties
@@ -17,3 +17,5 @@ idp.authn.DuoOIDC.3.src = /net/shibboleth/idp/plugin/authn/duo/views/passwordles
idp.authn.DuoOIDC.3.dest = views/passwordless.vm
idp.authn.DuoOIDC.4.src = /net/shibboleth/idp/plugin/authn/duo/views/passwordless-optin.vm
idp.authn.DuoOIDC.4.dest = views/passwordless-optin.vm
+idp.authn.DuoOIDC.5.src = /net/shibboleth/idp/plugin/authn/duo/views/admin/passwordless-admin.vm
+idp.authn.DuoOIDC.5.dest = views/admin/passwordless-admin.vm
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.
More information about the commits
mailing list