diff --git a/gen/OpenIddict.Client.WebIntegration.Generators/OpenIddictClientWebIntegrationGenerator.cs b/gen/OpenIddict.Client.WebIntegration.Generators/OpenIddictClientWebIntegrationGenerator.cs
index ca877ff7..69abb53b 100644
--- a/gen/OpenIddict.Client.WebIntegration.Generators/OpenIddictClientWebIntegrationGenerator.cs
+++ b/gen/OpenIddict.Client.WebIntegration.Generators/OpenIddictClientWebIntegrationGenerator.cs
@@ -862,6 +862,7 @@ using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using OpenIddict.Client;
using static OpenIddict.Client.WebIntegration.OpenIddictClientWebIntegrationConstants;
+using static OpenIddict.Extensions.OpenIddictHelpers;
namespace OpenIddict.Client.WebIntegration;
diff --git a/shared/OpenIddict.Extensions/Helpers/OpenIddictHelpers.cs b/shared/OpenIddict.Extensions/Helpers/OpenIddictHelpers.cs
index ba666793..f340b18c 100644
--- a/shared/OpenIddict.Extensions/Helpers/OpenIddictHelpers.cs
+++ b/shared/OpenIddict.Extensions/Helpers/OpenIddictHelpers.cs
@@ -178,6 +178,18 @@ internal static class OpenIddictHelpers
=> new(source ?? throw new ArgumentNullException(nameof(source)), comparer);
#endif
+ ///
+ /// Computes an absolute URI from the specified and URIs.
+ /// Note: if the URI is already absolute, it is directly returned.
+ ///
+ /// The left part.
+ /// The right part.
+ /// An absolute URI from the specified and .
+ /// is not an absolute URI.
+ [return: NotNullIfNotNull(nameof(right))]
+ public static Uri? CreateAbsoluteUri(Uri? left, string? right)
+ => CreateAbsoluteUri(left, !string.IsNullOrEmpty(right) ? new Uri(right, UriKind.RelativeOrAbsolute) : null);
+
///
/// Computes an absolute URI from the specified and URIs.
/// Note: if the URI is already absolute, it is directly returned.
diff --git a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Userinfo.cs b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Userinfo.cs
index f4e9f173..3dbd2aab 100644
--- a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Userinfo.cs
+++ b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.Userinfo.cs
@@ -389,6 +389,10 @@ public static partial class OpenIddictClientWebIntegrationHandlers
=> new(context.Response["data"]?.GetNamedParameters() ??
throw new InvalidOperationException(SR.FormatID0334("data"))),
+ // Nextcloud returns a nested "data" object that is itself nested in a "ocs" node.
+ ProviderTypes.Nextcloud => new(context.Response["ocs"]?["data"]?.GetNamedParameters() ??
+ throw new InvalidOperationException(SR.FormatID0334("ocs/data"))),
+
// ServiceChannel returns a nested "UserProfile" object.
ProviderTypes.ServiceChannel => new(context.Response["UserProfile"]?.GetNamedParameters() ??
throw new InvalidOperationException(SR.FormatID0334("UserProfile"))),
diff --git a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.cs b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.cs
index ee769934..0b5396bd 100644
--- a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.cs
+++ b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationHandlers.cs
@@ -1039,6 +1039,10 @@ public static partial class OpenIddictClientWebIntegrationHandlers
// Mailchimp returns the username as a custom "accountname" node:
ProviderTypes.Mailchimp => (string?) context.UserinfoResponse?["accountname"],
+ // Nextcloud returns the username as a custom "displayname" or "display-name" node:
+ ProviderTypes.Nextcloud => (string?) context.UserinfoResponse?["displayname"] ??
+ (string?) context.UserinfoResponse?["display-name"],
+
// Notion returns the username as a custom "bot/owner/user/name" node but
// requires a special capability to access this node, that may not be present:
ProviderTypes.Notion => (string?) context.UserinfoResponse?["bot"]?["owner"]?["user"]?["name"],
@@ -1087,11 +1091,11 @@ public static partial class OpenIddictClientWebIntegrationHandlers
=> (string?) context.UserinfoResponse?["username"],
// These providers return the user identifier as a custom "id" node:
- ProviderTypes.Basecamp or ProviderTypes.Deezer or ProviderTypes.Discord or
- ProviderTypes.Facebook or ProviderTypes.GitHub or ProviderTypes.Harvest or
- ProviderTypes.Kroger or ProviderTypes.Lichess or ProviderTypes.Twitter or
- ProviderTypes.Patreon or ProviderTypes.Reddit or ProviderTypes.Smartsheet or
- ProviderTypes.Spotify or ProviderTypes.SubscribeStar
+ ProviderTypes.Basecamp or ProviderTypes.Deezer or ProviderTypes.Discord or
+ ProviderTypes.Facebook or ProviderTypes.GitHub or ProviderTypes.Harvest or
+ ProviderTypes.Kroger or ProviderTypes.Lichess or ProviderTypes.Nextcloud or
+ ProviderTypes.Patreon or ProviderTypes.Reddit or ProviderTypes.Smartsheet or
+ ProviderTypes.Spotify or ProviderTypes.SubscribeStar or ProviderTypes.Twitter
=> (string?) context.UserinfoResponse?["id"],
// Bitbucket returns the user identifier as a custom "uuid" node:
diff --git a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationProviders.xml b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationProviders.xml
index a0d99abf..9b21c617 100644
--- a/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationProviders.xml
+++ b/src/OpenIddict.Client.WebIntegration/OpenIddictClientWebIntegrationProviders.xml
@@ -633,6 +633,29 @@
+
+
+
+
+
+
+
+
+
+
+
+
+