[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