[java-idp-plugin-duo] 01/01: Add DuoAdmin API client, response objects, dummy passwordless condition

Phil Smart philip.smart at jisc.ac.uk
Fri Jan 5 14:42:34 UTC 2024


This is an automated email from the git hooks/post-receive script.

philsmart pushed a commit to branch dev/JDUO-82
in repository java-idp-plugin-duo.

View the commit online:
http://git.shibboleth.net/view/?p=java-idp-plugin-duo.git;a=commit;h=86f205556abf15bb6c80d94480c98e5ffc54cabc

commit 86f205556abf15bb6c80d94480c98e5ffc54cabc
Author: Phil Smart <philip.smart at jisc.ac.uk>
AuthorDate: Fri Jan 5 14:42:20 2024 +0000

    Add DuoAdmin API client, response objects, dummy passwordless condition
---
 .../idp/plugin/authn/duo/DuoAdminClient.java       |  70 ++++
 .../plugin/authn/duo/DuoAdminResponseWrapper.java  |  57 +++
 .../idp/plugin/authn/duo/model/U2ftoken.java       | 110 +++++
 .../idp/plugin/authn/duo/model/User.java           | 459 +++++++++++++++++++++
 .../plugin/authn/duo/model/WebAuthnCredential.java | 149 +++++++
 .../authn/duo/impl/DefaultDuoAdminClient.java      | 333 +++++++++++++++
 .../impl/PasswordlessEnrolmentCheckCondition.java  |  74 ++++
 .../flows/authn/DuoOIDC/duo-oidc-authn-beans.xml   |  22 +
 8 files changed, 1274 insertions(+)

diff --git a/idp-duo-api/src/main/java/net/shibboleth/idp/plugin/authn/duo/DuoAdminClient.java b/idp-duo-api/src/main/java/net/shibboleth/idp/plugin/authn/duo/DuoAdminClient.java
new file mode 100644
index 00000000..28100c7d
--- /dev/null
+++ b/idp-duo-api/src/main/java/net/shibboleth/idp/plugin/authn/duo/DuoAdminClient.java
@@ -0,0 +1,70 @@
+/*
+ * 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;
+
+import java.util.Map;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+
+import net.shibboleth.idp.authn.duo.DuoIntegration;
+import net.shibboleth.idp.plugin.authn.duo.model.User;
+import net.shibboleth.shared.annotation.constraint.NonnullElements;
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
+
+/**
+ * A client that supports retrieval of information from the Duo Admin API.
+ * 
+ * <p>Clients should be thread-safe and re-usable.</p>
+ */
+public interface DuoAdminClient {
+    
+    /**
+     * Get the User that corresponds to the given username from the Duo Admin API. 
+     * 
+     * @param context context the profile request context, typically used in locating the {@link DuoIntegration} to use
+     * @param username a user name (or username alias) to look up a single user
+     * 
+     * @return the User iff found
+     * 
+     * @throws DuoException on error retrieving a response from the API
+     */
+    @Nonnull User getUser(@Nonnull final ProfileRequestContext context, @Nonnull @NotEmpty final String username) 
+            throws DuoException;
+    
+    /**
+     * Generic method for returning a {@link DuoAdminResponseWrapper} response from the given path and parameters.
+     * 
+     *  <p>Note, only GET requests should be allowed and hence only retrieval operations should be supported.</p>
+     * 
+     * @param <T> the type of response
+     * @param context the profile request context, typically used in locating the {@link DuoIntegration} to use
+     * @param path the path component of the API endpoint
+     * @param parameters any name value pairs to add to the HTTP request parameters
+     * @param wrapperTypeRef the type of response to return inside the {@link DuoAdminResponseWrapper}
+     * 
+     * @return a {@link DuoAdminResponseWrapper} with the correct response type embedded
+     * 
+     * @throws DuoException on error retrieving a response from the API
+     */
+    @Nonnull <T extends DuoAdminResponseWrapper<?>> T retrieve(@Nonnull final ProfileRequestContext context,
+            @Nonnull @NotEmpty final String path, @Nullable @NonnullElements final Map<String, String> parameters,
+            @Nonnull final TypeReference<T> wrapperTypeRef) throws DuoException;
+
+}
diff --git a/idp-duo-api/src/main/java/net/shibboleth/idp/plugin/authn/duo/DuoAdminResponseWrapper.java b/idp-duo-api/src/main/java/net/shibboleth/idp/plugin/authn/duo/DuoAdminResponseWrapper.java
new file mode 100644
index 00000000..35c02c7c
--- /dev/null
+++ b/idp-duo-api/src/main/java/net/shibboleth/idp/plugin/authn/duo/DuoAdminResponseWrapper.java
@@ -0,0 +1,57 @@
+/*
+ * 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;
+
+import javax.annotation.Nonnull;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * Handle a generic object returned from the response that will come from the Duo 
+ * AdminAPI.
+ *
+ * @param <T> the class being wrapped
+ */
+public class DuoAdminResponseWrapper<T> {
+    
+    /** the inner response. */
+    @JsonProperty("response")
+    private T response;
+    
+    /** the response status. */
+    @JsonProperty("stat")
+    private String stat;
+    
+    /**
+     * Get the inner response.
+     * 
+     * @return inner response
+     */
+    @Nonnull public T getResponse() {
+        assert response != null;
+        return response;
+    }
+
+    /**
+     * Get the response status.
+     * 
+     * @return response status
+     */
+    @Nonnull public String getStat() {
+        assert stat != null;
+        return stat;
+    }
+
+}
diff --git a/idp-duo-api/src/main/java/net/shibboleth/idp/plugin/authn/duo/model/U2ftoken.java b/idp-duo-api/src/main/java/net/shibboleth/idp/plugin/authn/duo/model/U2ftoken.java
new file mode 100644
index 00000000..35c479a1
--- /dev/null
+++ b/idp-duo-api/src/main/java/net/shibboleth/idp/plugin/authn/duo/model/U2ftoken.java
@@ -0,0 +1,110 @@
+/*
+ * 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.model;
+
+import javax.annotation.Nullable;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder;
+
+/**
+ * A U2F token. Deprecated by Duo in February 2022.
+ */
+ at JsonInclude(JsonInclude.Include.NON_NULL)
+ at JsonDeserialize(builder = U2ftoken.Builder.class)
+public class U2ftoken {
+
+    /** The date the U2F token was registered in Duo.*/
+    @JsonProperty("date_added")
+    @Nullable private final Integer dateAdded;
+    
+    /** The U2F token's registration identifier. Use with GET token by ID.*/
+    @JsonProperty("registration_id")
+    @Nullable private final String registrationId;
+
+    /**
+     * 
+     * Constructor.
+     *
+     * @param builder the builder
+     */
+    private U2ftoken(final Builder builder) {
+        this.dateAdded = builder.dateAdded;
+        this.registrationId = builder.registrationId;
+    }
+
+    /**
+     * The date the U2F token was registered in Duo.
+     * 
+     * @return the date the token was registered
+     */
+    @JsonProperty("date_added")
+    @Nullable public Integer getDateAdded() {
+        return dateAdded;
+    }
+
+    /**
+     * Get the U2F token's registration identifier. Use with GET token by ID.
+     * 
+     * @return the registration identifier of the token
+     */
+    @JsonProperty("registration_id")
+    @Nullable public String getRegistrationId() {
+        return registrationId;
+    }
+    
+    /**
+     * Get the builder for this class.
+     * 
+     * @return the builder
+     */
+    public static Builder builder() {
+        return new Builder();
+    }
+    
+    /** Builder used to build an instance of this class.*/
+    @JsonPOJOBuilder(buildMethodName = "build", withPrefix = "with")
+    public static final class Builder {
+        private Integer dateAdded;
+        private String registrationId;
+
+        /** Constructor.*/
+        private Builder() {
+        }
+
+        @JsonProperty("date_added")
+        public Builder withDateAdded(final Integer dateAdded) {
+            this.dateAdded = dateAdded;
+            return this;
+        }
+
+        @JsonProperty("registration_id")
+        public Builder withRegistrationId(final String registrationId) {
+            this.registrationId = registrationId;
+            return this;
+        }
+
+        public U2ftoken build() {
+            return new U2ftoken(this);
+        }
+    }
+
+
+    
+    
+
+}
diff --git a/idp-duo-api/src/main/java/net/shibboleth/idp/plugin/authn/duo/model/User.java b/idp-duo-api/src/main/java/net/shibboleth/idp/plugin/authn/duo/model/User.java
new file mode 100644
index 00000000..b43acc0f
--- /dev/null
+++ b/idp-duo-api/src/main/java/net/shibboleth/idp/plugin/authn/duo/model/User.java
@@ -0,0 +1,459 @@
+/*
+ * 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.model;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import com.fasterxml.jackson.annotation.JsonAnySetter;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder;
+
+import net.shibboleth.shared.annotation.constraint.NotLive;
+import net.shibboleth.shared.annotation.constraint.Unmodifiable;
+import net.shibboleth.shared.collection.CollectionSupport;
+
+/**
+ * A model object to hold the response from the Users Duo Admin API. 
+ * 
+ * <p>Those properties not explicitly listed will be included in the additionalProperties map for retrieval 
+ * if required.</p>
+ */
+ at JsonInclude(JsonInclude.Include.NON_NULL)
+ at JsonIgnoreProperties(ignoreUnknown = true) 
+ at JsonDeserialize(builder = User.Builder.class)
+public class User {
+
+    /** Generated serialUID. */
+    public static final long serialVersionUID = 8122423604710803224L;
+    
+    /** The user's creation date as a UNIX timestamp .*/
+    @JsonProperty("created")
+    @Nullable private final Integer created;
+    
+    /** The user's email address.*/
+    @JsonProperty("email")
+    @Nullable private final String email;
+    
+    /** The user's given name.*/
+    @JsonProperty("firstname")
+    @Nullable private final String firstname;
+    
+    /** 
+     * Is true if the user has a phone, hardware token, U2F token, WebAuthn security key, or other 
+     * WebAuthn method available for authentication. Otherwise, false.
+     */
+    @JsonProperty("is_enrolled")
+    @Nullable private final Boolean enrolled;
+
+    /** 
+     * An integer indicating the last update to the user via directory sync as a Unix timestamp, or null if the user 
+     * has never synced with an external directory or if the directory that originally created the user has been deleted 
+     * from Duo.
+     */
+    @JsonProperty("last_directory_sync")
+    @Nullable private final Object lastDirectorySync;
+
+    /** 
+     * An integer indicating the last time this user logged in, as a Unix timestamp, or null if the user has 
+     * not logged in.
+     */
+    @JsonProperty("last_login")
+    @Nullable private final Integer lastLogin;
+
+    /** The user's surname.*/
+    @JsonProperty("lastname")
+    @Nullable private final String lastname;
+
+    /** The user's lockout_reason.*/
+    @JsonProperty("lockout_reason")
+    @Nullable private final String lockoutReason;
+
+    /** Notes about this user. Viewable in the Duo Admin Panel .*/
+    @JsonProperty("notes")
+    @Nullable private final String notes;
+
+    /** The user's real name (or full name).*/
+    @JsonProperty("realname")
+    @Nullable private final String realname;
+
+    /** The user's status.*/
+    @JsonProperty("status")
+    @Nullable private final String status;   
+
+    /** A list of U2F tokens that this user can use.*/
+    @JsonProperty("u2ftokens")
+    @Nonnull @NotLive @Unmodifiable private final List<U2ftoken> u2ftokens;
+
+    /** The user's ID.*/
+    @JsonProperty("user_id")
+    @Nullable private final String userId;
+
+    /** The user's username.*/
+    @JsonProperty("username")
+    @Nullable private final String username;
+
+    /** The list of WebAuthn authenticators that this user can use.*/
+    @JsonProperty("webauthncredentials")
+    @Nonnull @NotLive @Unmodifiable private final List<WebAuthnCredential> webAuthnCredentials;
+
+    /** Any additional properties not explicitly captured by this class are added to this map.*/
+    @JsonIgnore
+    @Nonnull @NotLive @Unmodifiable private final Map<String, Object> additionalProperties;
+
+    /**
+     * 
+     * Constructor.
+     *
+     * @param builder the builder
+     */
+    private User(final Builder builder) {
+        this.created = builder.created;
+        this.email = builder.email;
+        this.firstname = builder.firstname;
+        this.enrolled = builder.enrolled;
+        this.lastDirectorySync = builder.lastDirectorySync;
+        this.lastLogin = builder.lastLogin;
+        this.lastname = builder.lastname;
+        this.lockoutReason = builder.lockoutReason;
+        this.notes = builder.notes;
+        this.realname = builder.realname;
+        this.status = builder.status;
+        this.u2ftokens = builder.u2ftokens;
+        this.userId = builder.userId;
+        this.username = builder.username;
+        this.webAuthnCredentials = builder.webAuthnCredentials;
+        this.additionalProperties = CollectionSupport.copyToMap(builder.additionalProperties);
+    }
+
+    /**
+     * Get The user's creation date as a UNIX timestamp.
+     * 
+     * @return
+     */
+    @JsonProperty("created")
+    @Nullable public Integer getCreated() {
+        return created;
+    }
+
+    /**
+     * Get the user's email address.
+     * 
+     * @return
+     */
+    @JsonProperty("email")
+    @Nullable public String getEmail() {
+        return email;
+    }
+
+    /**
+     * Get The user's given name.
+     * 
+     * @return
+     */
+    @JsonProperty("firstname")
+    @Nullable public Object getFirstname() {
+        return firstname;
+    }
+
+    /**
+     * Is true if the user has a phone, hardware token, U2F token, WebAuthn security key, or other WebAuthn 
+     * method available for authentication. Otherwise, false.
+     * 
+     * @return if the user has enrolled
+     */
+    @JsonProperty("is_enrolled")
+    public Boolean isEnrolled() {
+        return enrolled;
+    }
+
+    /**
+     * Get an integer indicating the last update to the user via directory sync as a Unix timestamp, or null if 
+     * the user has never synced with an external directory or if the directory that originally created the 
+     * user has been deleted from Duo.
+     * 
+     * @return
+     */
+    @JsonProperty("last_directory_sync")
+    @Nullable public Object getLastDirectorySync() {
+        return lastDirectorySync;
+    }
+
+    /**
+     * Get an integer indicating the last time this user logged in, as a Unix timestamp, or null if the user 
+     * has not logged in.
+     * 
+     * @return
+     */
+    @JsonProperty("last_login")
+    @Nullable public Integer getLastLogin() {
+        return lastLogin;
+    }    
+
+    /**
+     * Get the user's surname.
+     * 
+     * @return the surname
+     */
+    @JsonProperty("lastname")
+    @Nullable public Object getLastname() {
+        return lastname;
+    }    
+
+    /**
+     * Get the user's lockout_reason.
+     *  
+     * @return the reason the user was locked out.
+     */
+    @JsonProperty("lockout_reason")
+    @Nullable public Object getLockoutReason() {
+        return lockoutReason;
+    }
+   
+    /**
+     * Get notes about this user. Viewable in the Duo Admin Panel.
+     * 
+     * @return the notes
+     */
+    @JsonProperty("notes")
+    @Nullable public String getNotes() {
+        return notes;
+    }   
+
+    /**
+     * Get the user's real name (or full name).
+     * 
+     * @return the user's real name
+     */
+    @JsonProperty("realname")
+    @Nullable public String getRealname() {
+        return realname;
+    }   
+
+    /**
+     * Get the user's status.
+     * 
+     * @return the status
+     */
+    @JsonProperty("status")
+    @Nullable public String getStatus() {
+        return status;
+    }    
+    
+    /**
+     * Get a list of U2F tokens that this user can use.
+     *  
+     * @return the list of U2F tokens
+     */
+    @JsonProperty("u2ftokens")
+    @Nonnull @NotLive @Unmodifiable public List<U2ftoken> getU2ftokens() {
+        return u2ftokens;
+    }   
+
+    /**
+     * Get the user's ID.
+     * 
+     * @return the users id.
+     */
+    @JsonProperty("user_id")
+    @Nullable public String getUserId() {
+        return userId;
+    }    
+
+    /**
+     * Get the user's username.
+     * 
+     * @return the user's username
+     */
+    @JsonProperty("username")
+    @Nullable public String getUsername() {
+        return username;
+    }   
+
+    /**
+     * Get a list of WebAuthn authenticators that this user can use.
+     * 
+     * @return the list of WebAuthn credentials to the user can use
+     */
+    @JsonProperty("webauthncredentials")
+    @Nonnull @NotLive @Unmodifiable public List<WebAuthnCredential> getWebAuthnCredentials() {
+        return webAuthnCredentials;
+    }
+    
+    /**
+     * Get any additional properties not explicitly captured by this class.
+     * 
+     * @return the map of any additional properties not directly captured by this class.
+     */
+    @JsonIgnore
+    @Nonnull @NotLive @Unmodifiable public Map<String, Object> getAdditionalProperties() {
+        return additionalProperties;
+    }
+
+    /**
+     * Get the builder for this class.
+     * 
+     * @return the builder
+     */
+    public static Builder builder() {
+        return new Builder();
+    }
+
+    @JsonPOJOBuilder(buildMethodName = "build", withPrefix = "with")
+    @JsonIgnoreProperties(ignoreUnknown = true) 
+    /** The builder used to create a threadsafe instance of this class.*/
+    public static final class Builder {
+        @Nullable private Integer created;
+        @Nullable private String email;
+        @Nullable private String firstname;
+        @Nullable private Boolean enrolled;
+        @Nullable private Object lastDirectorySync;
+        @Nullable private Integer lastLogin;
+        @Nullable private String lastname;
+        @Nullable private String lockoutReason;
+        @Nullable private String notes;
+        @Nullable private String realname;
+        @Nullable private String status;
+        @Nonnull private List<U2ftoken> u2ftokens;
+        @Nullable private String userId;
+        @Nullable private String username;
+        @Nonnull private List<WebAuthnCredential> webAuthnCredentials;
+        @Nonnull private final Map<String, Object> additionalProperties;
+
+        /** Constructor.*/
+        private Builder() {
+            webAuthnCredentials = CollectionSupport.emptyList();
+            additionalProperties = new HashMap<>();
+            u2ftokens = CollectionSupport.emptyList();
+        }
+
+        @JsonProperty("created")
+        public Builder withCreated(@Nullable final Integer createdAt) {
+            created = createdAt;
+            return this;
+        }
+
+        @JsonProperty("email")
+        public Builder withEmail(@Nullable final String emailIn) {
+            email = emailIn;
+            return this;
+        }
+
+        @JsonProperty("firstname")
+        public Builder withFirstname(@Nullable final String firstnameIn) {
+            firstname = firstnameIn;
+            return this;
+        }
+
+        @JsonProperty("is_enrolled")
+        public Builder withIsEnrolled(@Nullable final Boolean isEnrolled) {
+            enrolled = isEnrolled;
+            return this;
+        }
+
+        @JsonProperty("last_directory_sync")
+        public Builder withLastDirectorySync(@Nullable final Object dirSync) {
+            lastDirectorySync = dirSync;
+            return this;
+        }
+
+        @JsonProperty("last_login")
+        public Builder withLastLogin(@Nullable final Integer lastLoginAt) {
+            lastLogin = lastLoginAt;
+            return this;
+        }
+
+        @JsonProperty("lastname")
+        public Builder withLastname(@Nullable final String lastnameIn) {
+            lastname = lastnameIn;
+            return this;
+        }
+
+        @JsonProperty("lockout_reason")
+        public Builder withLockoutReason(@Nullable final String lockoutReasonIn) {
+            lockoutReason = lockoutReasonIn;
+            return this;
+        }
+
+        @JsonProperty("notes")
+        public Builder withNotes(@Nullable final String notesIn) {
+            notes = notesIn;
+            return this;
+        }
+
+        @JsonProperty("realname")
+        public Builder withRealname(@Nullable final String realnameIn) {
+            realname = realnameIn;
+            return this;
+        }
+
+        @JsonProperty("status")
+        public Builder withStatus(@Nullable final String statusIn) {
+            status = statusIn;
+            return this;
+        }
+
+        @JsonProperty("u2ftokens")
+        public Builder withU2ftokens(@Nullable final List<U2ftoken> tokens) {
+            if (tokens != null) {
+                u2ftokens = CollectionSupport.copyToList(tokens);
+            }
+            return this;
+        }
+
+        @JsonProperty("user_id")
+        public Builder withUserId(@Nullable final String id) {
+            userId = id;
+            return this;
+        }
+
+        @JsonProperty("username")
+        public Builder withUsername(@Nullable final String uname) {
+            username = uname;
+            return this;
+        }
+
+        @JsonProperty("webauthncredentials")
+        public Builder withWebAuthnCredentials(@Nullable final List<WebAuthnCredential> credentials) {
+            if (credentials != null) {
+                webAuthnCredentials = CollectionSupport.copyToList(credentials);
+            }
+            return this;
+        }
+        
+        @JsonAnySetter
+        public Builder withAdditionalProperty(@Nonnull final String name, @Nullable final Object value) {
+            // Only add properties with non-null values.
+            if (value != null) {
+                additionalProperties.put(name, value);
+            }
+            return this;
+        }
+
+        public User build() {
+            return new User(this);
+        }
+    }
+    
+
+}
diff --git a/idp-duo-api/src/main/java/net/shibboleth/idp/plugin/authn/duo/model/WebAuthnCredential.java b/idp-duo-api/src/main/java/net/shibboleth/idp/plugin/authn/duo/model/WebAuthnCredential.java
new file mode 100644
index 00000000..c36a71fd
--- /dev/null
+++ b/idp-duo-api/src/main/java/net/shibboleth/idp/plugin/authn/duo/model/WebAuthnCredential.java
@@ -0,0 +1,149 @@
+/*
+ * 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.model;
+
+import javax.annotation.Nullable;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder;
+
+/**
+ * A WebAuthn credential registered to a User.
+ */
+ at JsonInclude(JsonInclude.Include.NON_NULL)
+ at JsonDeserialize(builder = WebAuthnCredential.Builder.class)
+public class WebAuthnCredential {
+
+    /** Free-form label for the WebAuthn credential.*/
+    @JsonProperty("credential_name")
+    @Nullable private final String credentialName;
+    
+    /** The date the WebAuthn credential was registered in Duo.*/
+    @JsonProperty("date_added")
+    @Nullable private final Integer dateAdded;
+    
+    /** Indicates the type of WebAuthn credential.*/
+    @JsonProperty("label")
+    @Nullable private final String label;
+    
+    /** The WebAuthn credential's registration identifier.*/
+    @JsonProperty("webauthnkey")
+    @Nullable private final String webauthnkey;
+
+    /**
+     * 
+     * Constructor.
+     *
+     * @param builder the builder
+     */
+    private WebAuthnCredential(final Builder builder) {
+        this.credentialName = builder.credentialName;
+        this.dateAdded = builder.dateAdded;
+        this.label = builder.label;
+        this.webauthnkey = builder.webauthnkey;
+    }
+
+    /**
+     * Get the free-form label for the WebAuthn credential.
+     * 
+     * @return the credential name
+     */
+    @JsonProperty("credential_name")
+    @Nullable public String getCredentialName() {
+        return credentialName;
+    }
+
+    /**
+     * Get the date the WebAuthn credential was registered in Duo.
+     * 
+     * @return the date the credential was added
+     */
+    @JsonProperty("date_added")
+    @Nullable public Integer getDateAdded() {
+        return dateAdded;
+    }   
+
+    /**
+     * Get the type of WebAuthn credential.
+     * 
+     * @return the credential label
+     */
+    @JsonProperty("label")
+    @Nullable public String getLabel() {
+        return label;
+    }
+
+    /**
+     * Get the WebAuthn credential's registration identifier.
+     * 
+     * @return the credentials registration identifier
+     */
+    @JsonProperty("webauthnkey")
+    public String getWebauthnkey() {
+        return webauthnkey;
+    }
+
+    /**
+     * Get the builder for this class.
+     * 
+     * @return the builder for this class
+     */
+    public static Builder builder() {
+        return new Builder();
+    }
+
+    /** Builder for this class.*/
+    @JsonPOJOBuilder(buildMethodName = "build", withPrefix = "with")
+    public static final class Builder {
+        private String credentialName;
+        private Integer dateAdded;
+        private String label;
+        private String webauthnkey;
+
+        /** Constructor.*/
+        private Builder() {
+        }
+
+        @JsonProperty("credential_name")
+        public Builder withCredentialName(@Nullable final String name) {
+            credentialName = name;
+            return this;
+        }
+
+        @JsonProperty("date_added")
+        public Builder withDateAdded(@Nullable final Integer date) {
+            dateAdded = date;
+            return this;
+        }
+
+        @JsonProperty("label")
+        public Builder withLabel(@Nullable final String labelIn) {
+            label = labelIn;
+            return this;
+        }
+
+        @JsonProperty("webauthnkey")
+        public Builder withWebauthnkey(@Nullable final String key) {
+            webauthnkey = key;
+            return this;
+        }
+
+        public WebAuthnCredential build() {
+            return new WebAuthnCredential(this);
+        }
+    }
+}
diff --git a/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/DefaultDuoAdminClient.java b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/DefaultDuoAdminClient.java
new file mode 100644
index 00000000..466b0c97
--- /dev/null
+++ b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/DefaultDuoAdminClient.java
@@ -0,0 +1,333 @@
+/*
+ * 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.impl;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.apache.hc.client5.http.classic.HttpClient;
+import org.apache.hc.client5.http.protocol.HttpClientContext;
+import org.apache.hc.core5.http.ClassicHttpRequest;
+import org.apache.hc.core5.http.ClassicHttpResponse;
+import org.apache.hc.core5.http.HttpEntity;
+import org.apache.hc.core5.http.HttpStatus;
+import org.apache.hc.core5.http.io.support.ClassicRequestBuilder;
+import org.apache.hc.core5.net.URIBuilder;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.security.httpclient.HttpClientSecurityParameters;
+import org.opensaml.security.httpclient.HttpClientSecuritySupport;
+import org.slf4j.Logger;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import net.shibboleth.idp.authn.duo.DuoAuthAPI;
+import net.shibboleth.idp.authn.duo.DuoIntegration;
+import net.shibboleth.idp.plugin.authn.duo.DuoAdminClient;
+import net.shibboleth.idp.plugin.authn.duo.DuoAdminResponseWrapper;
+import net.shibboleth.idp.plugin.authn.duo.DuoException;
+import net.shibboleth.idp.plugin.authn.duo.model.User;
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.annotation.constraint.NonnullElements;
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
+import net.shibboleth.shared.annotation.constraint.ThreadSafeAfterInit;
+import net.shibboleth.shared.codec.EncodingException;
+import net.shibboleth.shared.component.AbstractIdentifiableInitializableComponent;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.logic.FunctionSupport;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * The default implementation of the {@link DuoAdminClient} for looking up information from the DuoAdmin API.
+ * 
+ * <p>This class is thread-safe and can be re-used once published.</p>
+ */
+ at ThreadSafeAfterInit
+public class DefaultDuoAdminClient extends AbstractIdentifiableInitializableComponent implements DuoAdminClient {    
+    
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(DefaultDuoAdminClient.class);
+    
+    /** HttpClient for contacting Duo. */
+    @NonnullAfterInit private HttpClient httpClient;
+
+    /** HTTP client security parameters. */
+    @Nullable private HttpClientSecurityParameters httpClientSecurityParameters;
+    
+    /** JSON object mapper. */
+    @NonnullAfterInit private ObjectMapper objectMapper;
+    
+    /** Lookup strategy for Duo integration for Admin API use. */
+    @Nonnull private Function<ProfileRequestContext, DuoIntegration> adminDuoIntegrationLookupStrategy;
+    
+    /** The location of the users admin endpoint.*/
+    @Nonnull @NotEmpty private String usersAdminEndpoint;
+    
+    /** Constructor.*/
+    public DefaultDuoAdminClient() {
+        adminDuoIntegrationLookupStrategy =  FunctionSupport.constant(null);
+        usersAdminEndpoint = "/admin/v1/users";
+    }
+    
+    /**
+     * Set the {@link HttpClient} to use for contacting Duo.
+     * 
+     * @param client HttpClient
+     */
+    public void setHttpClient(@Nonnull final HttpClient client) {
+        checkSetterPreconditions();
+        httpClient = Constraint.isNotNull(client, "HTTP client cannot be null");
+    }
+
+    /**
+     * Set the optional client security parameters.
+     * 
+     * @param params the new client security parameters
+     */
+    public void setHttpClientSecurityParameters(@Nullable final HttpClientSecurityParameters params) {
+        checkSetterPreconditions();
+        httpClientSecurityParameters = params;
+    }
+    
+    /**
+     * Set the JSON {@link ObjectMapper}.
+     * 
+     * @param mapper object mapper
+     */
+    public void setObjectMapper(@Nonnull final ObjectMapper mapper) {
+        checkSetterPreconditions();
+        objectMapper = Constraint.isNotNull(mapper, "Object mapper cannot be null");
+    }
+    
+    /**
+     * Set the lookup strategy to use to locate the Duo Admin API integration. 
+     * 
+     * @param strategy The lookup strategy to set.
+     */
+    public void setAdminDuoIntegrationLookupStrategy(@Nonnull 
+            final Function<ProfileRequestContext, DuoIntegration> strategy) {
+        checkSetterPreconditions();
+        adminDuoIntegrationLookupStrategy = Constraint.isNotNull(strategy,
+                "AdminDuoIntegrationLookup strategy can not be null");
+    }
+    
+    /**
+     * Set the /users admin endpoint.
+     * 
+     * @param endpoint The users admin endpoint to set.
+     */
+    public void setUsersAdminEndpoint(@Nonnull @NotEmpty final String endpoint) {
+        checkSetterPreconditions();
+        usersAdminEndpoint = Constraint.isNotEmpty(endpoint, "UserAdminEndpoint can not be null");
+    }
+
+    /** {@inheritDoc} */
+    @Override protected void doInitialize() throws ComponentInitializationException {
+        super.doInitialize();
+        
+        
+        if (httpClient == null) {
+            throw new ComponentInitializationException("HttpClient cannot be null");
+        }
+
+        if (objectMapper == null) {
+            throw new ComponentInitializationException("ObjectMapper cannot be null");
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public User getUser(@Nonnull final ProfileRequestContext context, final String username) throws DuoException {
+        try {
+            // prepare the request
+            final ClassicHttpRequest request = 
+                    buildRequest(context, usersAdminEndpoint, Map.of(DuoAuthAPI.DUO_USERNAME, username));
+            
+            // execute the request
+            final List<User> response = 
+                    doAPIRequest(request, new TypeReference<DuoAdminResponseWrapper<List<User>>>() {}).getResponse();
+            if (response.size() != 1) {
+                throw new DuoException("User API response did not contain a single user");
+            }
+            return response.get(0);
+
+        } catch (final Exception ex) {
+            throw new DuoException("Unable to to get User '"+username+"' from Duo's Admin API", ex);
+        }
+    }
+    
+    @Override
+    public <T extends DuoAdminResponseWrapper<?>> T retrieve(@Nonnull final ProfileRequestContext context,
+            @Nonnull @NotEmpty final String path, @Nullable @NonnullElements final Map<String, String> parameters,
+            @Nonnull final TypeReference<T> wrapperTypeRef) 
+                    throws DuoException {
+        try {
+            final ClassicHttpRequest request = buildRequest(context, path, parameters);
+            return doAPIRequest(request, wrapperTypeRef);
+        } catch (DuoException | IOException e) {
+            //Wrap the exception
+            throw new DuoException(e);
+        }
+    }
+    
+    
+    /**
+     * Check the component is active and if it is get the {@link DuoIntegration} to use.
+     * 
+     * @param prc the profile request context
+     * 
+     * @return the {@link DuoIntegration} to use
+     * 
+     * @throws DuoException
+     */
+    @Nonnull private DuoIntegration getIntegrationAndCheckComponentActive(
+            @Nonnull final ProfileRequestContext prc) throws DuoException {
+        
+        checkComponentActive();   
+        final DuoIntegration integration = adminDuoIntegrationLookupStrategy.apply(prc);
+        if (integration == null) {
+            throw new DuoException("Unable to locate Duo Integration.");
+        }
+        return integration;
+    }
+    
+    /**
+     * Build the HTTP request from the given path and parameters. 
+     * 
+     * <p>The {@link DuoIntegration} is located dynamically. The request is signed using the integration key and secret 
+     * key from the located integration. Similarly the integration's API host is used as the host component of the URI.
+     * </p>
+     * 
+     * @param context the profile request context use to locate the {@link DuoIntegration} to use
+     * @param path the path component of the API endpoint
+     * @param parameters a map of name value pairs to add to the HTTP request parameters
+     * 
+     * @return the built and signed HTTP request
+     * 
+     * @throws DuoException on error creating the request
+     * 
+     */
+    @Nonnull private ClassicHttpRequest buildRequest(@Nonnull final ProfileRequestContext context,
+            @Nonnull @NotEmpty final String path, @Nullable @NonnullElements final Map<String, String> parameters) 
+                    throws DuoException {
+        
+        final DuoIntegration integration = getIntegrationAndCheckComponentActive(context);
+        
+        try {
+            final URI uri = new URIBuilder().setScheme("https").setHost(integration.getAPIHost())
+                    .setPath(path).build();
+            final ClassicRequestBuilder rb =
+                    ClassicRequestBuilder.get().setUri(uri);
+            if (parameters != null) {
+                parameters.forEach(rb::addParameter);
+            }
+            
+            assert rb != null;            
+            DuoSupport.signRequest(rb, integration);
+            final ClassicHttpRequest request = rb.build();
+            assert request != null;            
+            return request;
+        } catch (final URISyntaxException | InvalidKeyException | NoSuchAlgorithmException | EncodingException ex) {
+            throw new DuoException("Unable to to get response from Duo Admin API", ex);
+        }
+        
+    }
+    
+ // Checkstyle: CyclomaticComplexity OFF
+    /**
+     * Performs a call to the Duo AdminAPI. Upon a successful call, the JSON response is mapped into the appropriate 
+     * type of {@link YYY}.
+     * 
+     * @param request the prepared HTTP request
+     * @param wrapperTypeRef the type of {@link DuoResponseWrapper} to use
+     * @param <T> the DuoResponse type being wrapped
+     * 
+     * @return a {@link DuoResponseWrapper}
+     * 
+     * @throws IOException on an I/O error
+     * @throws DuoException on a Duo-related error
+     */
+    @Nonnull private <T extends DuoAdminResponseWrapper<?>> T doAPIRequest(@Nonnull final ClassicHttpRequest request,
+            @Nonnull final TypeReference<T> wrapperTypeRef)
+                    throws DuoException, IOException {
+
+        // Make the request.
+        final HttpClientContext clientContext = HttpClientContext.create();
+        assert clientContext != null;
+        HttpClientSecuritySupport.marshalSecurityParameters(clientContext, httpClientSecurityParameters, true);
+        HttpClientSecuritySupport.addDefaultTLSTrustEngineCriteria(clientContext, request);
+        try (final ClassicHttpResponse httpResponse = httpClient.executeOpen(null, request, clientContext)) {
+            final String scheme = request.getScheme();
+            assert scheme != null;
+            HttpClientSecuritySupport.checkTLSCredentialEvaluated(clientContext, scheme);
+    
+            // Check the HTTP response code.
+            final int httpStatusCode = httpResponse.getCode();
+            if (httpStatusCode == HttpStatus.SC_BAD_REQUEST) {
+                try (final HttpEntity entity = httpResponse.getEntity()) {
+                    if (entity == null) {
+                        throw new IOException("Bad request status code (" + httpStatusCode + ") returned from Duo: "
+                                + (httpResponse.getReasonPhrase() != null ? httpResponse.getReasonPhrase() : "none"));
+                    }
+                    try (final InputStream httpContent = entity.getContent()) {
+                        final DuoFailureResponse msg = objectMapper.readValue(httpContent, DuoFailureResponse.class);
+                        final StringBuilder builder = new StringBuilder();
+                        builder.append(msg.getMessage() != null ? msg.getMessage() : "no message")
+                            .append(" (")
+                            .append(msg.getMessageDetail() != null ? msg.getMessageDetail() : "no detail")
+                            .append(")");
+                        throw new DuoException(builder.toString());
+                    }
+                }
+            }
+            if (httpStatusCode != HttpStatus.SC_OK) {
+                throw new IOException("Non-ok status code (" + httpStatusCode + ") returned from Duo: "
+                        + (httpResponse.getReasonPhrase() != null ? httpResponse.getReasonPhrase() : "none"));
+            }
+            
+            try (final HttpEntity entity = httpResponse.getEntity()) {
+                if (entity == null) {
+                    throw new IOException("No response body returned from Duo");
+                }
+        
+                // Parse the JSON response.
+                try (final InputStream content = entity.getContent()) {
+                    final T duoResponse = objectMapper.readValue(content, wrapperTypeRef);
+
+                    if (duoResponse == null) {
+                        throw new DuoException("Unable to parse JSON response");
+                    } else if (!"OK".equals(duoResponse.getStat())) {
+                        throw new DuoException("Unexpected status value in JSON response: " + duoResponse.getStat());
+                    }
+        
+                    return duoResponse;
+                }
+            }
+        }
+    }
+// Checkstyle: CyclomaticComplexity ON
+
+}
diff --git a/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/PasswordlessEnrolmentCheckCondition.java b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/PasswordlessEnrolmentCheckCondition.java
new file mode 100644
index 00000000..a78a090c
--- /dev/null
+++ b/idp-duo-impl/src/main/java/net/shibboleth/idp/plugin/authn/duo/impl/PasswordlessEnrolmentCheckCondition.java
@@ -0,0 +1,74 @@
+/*
+ * 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.impl;
+
+import java.util.function.BiPredicate;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+
+import net.shibboleth.idp.plugin.authn.duo.DuoException;
+import net.shibboleth.idp.plugin.authn.duo.model.User;
+import net.shibboleth.shared.annotation.ParameterName;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * A predicate which checks the enrolment status of a user against the Duo Admin APIs.  Accepts a profile request
+ * context tree and a username string.
+ */
+
+//TODO FIXME This is only an example
+
+public class PasswordlessEnrolmentCheckCondition implements BiPredicate<ProfileRequestContext,String> {
+    
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(PasswordlessEnrolmentCheckCondition.class);
+    
+    /** The admin client used to access the DuoAdmin API.*/
+    @Nonnull private final DefaultDuoAdminClient adminClient;
+    
+    public PasswordlessEnrolmentCheckCondition(@ParameterName(name="client") @Nonnull final DefaultDuoAdminClient client) {
+        adminClient = Constraint.isNotNull(client, "Duo Admin Client can not be null");
+    }    
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean test(@Nullable final ProfileRequestContext prc, @Nullable final String username) {
+        
+        if (prc == null || username == null) {
+            log.trace("Unable to checl passwordless enrollment status for '{}'", username);
+            return false;
+        }
+        
+        log.trace("Checking passwordless enrollment status for '{}'", username); 
+        try {
+            final User response = adminClient.getUser(prc, username);
+            log.trace("User '{}' isEnrolled '{}' and has '{}' credentials", response.getUserId(), response.isEnrolled(), 
+                    response.getWebAuthnCredentials().size());            
+            if (response.isEnrolled() == null) {
+                return false;
+            }
+            return response.isEnrolled();
+        } catch (final DuoException ex) {
+            log.warn("Duo AdminAPI request failed: '{}'", ex.getMessage(), ex);
+            return false;
+        }       
+    }
+
+}
diff --git a/idp-duo-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/DuoOIDC/duo-oidc-authn-beans.xml b/idp-duo-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/DuoOIDC/duo-oidc-authn-beans.xml
index 00981536..fa3bb838 100644
--- a/idp-duo-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/DuoOIDC/duo-oidc-authn-beans.xml
+++ b/idp-duo-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/authn/DuoOIDC/duo-oidc-authn-beans.xml
@@ -79,6 +79,15 @@
     </bean>
     <bean id="shibboleth.authn.DuoOIDC.Passwordless.DuoIntegrationStrategy" parent="shibboleth.Functions.Constant"
         c:target="#{getObject('shibboleth.authn.DuoOIDC.Passwordless.DuoIntegration')}" />
+        
+    <!-- Default Duo Admin API Integration -->
+    <bean id="shibboleth.authn.DuoOIDC.Admin.DuoIntegration" lazy-init="false"
+        class="net.shibboleth.idp.authn.duo.BasicDuoIntegration"
+        p:APIHost="%{idp.duo.oidc.admin.apiHost:none}"
+        p:integrationKey="%{idp.duo.oidc.admin.integrationKey:none}"
+        p:secretKey="%{idp.duo.oidc.admin.secretKey:none}"/>
+    <bean id="shibboleth.authn.DuoOIDC.Admin.DuoAdminIntegrationStrategy" parent="shibboleth.Functions.Constant"
+        c:target-ref="shibboleth.authn.DuoOIDC.Admin.DuoIntegration" />
 
     <!-- Default "optional" non-browser integration. -->
     <bean id="shibboleth.authn.DuoOIDC.NonBrowser.DuoIntegration" lazy-init="false"
@@ -161,7 +170,20 @@
         p:uppercase="%{idp.authn.oidc.uppercase:false}"
         p:trim="%{idp.authn.oidc.trim:true}"
         p:transforms="#{getObject('shibboleth.authn.DuoOIDC.Transforms')}" />
+    
+    <!-- Example passwordless condition that uses the admin API -->
+    <bean id="shibboleth.authn.DuoOIDC.Passwordless.Condition" 
+        class="net.shibboleth.idp.plugin.authn.duo.impl.PasswordlessEnrolmentCheckCondition"
+        c:_0="#{getObject('shibboleth.authn.DuoOIDC.Admin.Api.Client') ?: getObject('DefaultDuoAdminClient')}"/>
         
+                
+    <!-- Singleton, shared, Duo Admin API Beans -->
+    <bean id="DefaultDuoAdminClient" class="net.shibboleth.idp.plugin.authn.duo.impl.DefaultDuoAdminClient" scope="singleton"
+        p:objectMapper-ref="shibboleth.JSONObjectMapper"
+        p:httpClient="#{getObject('shibboleth.authn.DuoOIDC.Admin.HttpClient') ?: getObject('shibboleth.InternalHttpClient')}"
+        p:httpClientSecurityParameters="#{getObject('shibboleth.authn.DuoOIDC.NonBrowser.HttpClientSecurityParameters')}"
+        p:adminDuoIntegrationLookupStrategy-ref="shibboleth.authn.DuoOIDC.Admin.DuoAdminIntegrationStrategy"/>
+            
     <!-- Duo OIDC beans -->
     <bean id="PopulateDuoAuthenticationContext" scope="prototype"
         class="net.shibboleth.idp.plugin.authn.duo.impl.PopulateDuoAuthenticationContext"

-- 
To stop receiving notification emails like this one, please contact
the administrator of this repository.


More information about the commits mailing list