Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -108,8 +108,8 @@ public ValueTask HandleAsync(HandleConfigurationResponseContext context)
// types is amended to include the known supported types for the providers that require it.

if (context.Registration.ProviderType is
ProviderTypes.Apple or ProviderTypes.FaceIt or
ProviderTypes.LinkedIn or ProviderTypes.QuickBooksOnline)
ProviderTypes.AlibabaCloud or ProviderTypes.Apple or ProviderTypes.FaceIt or
ProviderTypes.LinkedIn or ProviderTypes.QuickBooksOnline)
{
context.Configuration.GrantTypesSupported.Add(GrantTypes.AuthorizationCode);
context.Configuration.GrantTypesSupported.Add(GrantTypes.RefreshToken);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -357,23 +357,23 @@ public ValueTask HandleAsync(ExtractTokenResponseContext context)
context.Response.RefreshToken = null;
}

// Note: Alibaba Cloud and Exact Online returns a non-standard "expires_in"
// parameter formatted as a string instead of a numeric type.
if (context.Registration.ProviderType is ProviderTypes.AlibabaCloud or ProviderTypes.ExactOnline &&
long.TryParse((string?) context.Response[Parameters.ExpiresIn],
NumberStyles.Integer, CultureInfo.InvariantCulture, out long value))
{
context.Response.ExpiresIn = value;
}

// Note: Deezer doesn't return a standard "expires_in" parameter
// but returns an equivalent "expires" integer parameter instead.
if (context.Registration.ProviderType is ProviderTypes.Deezer)
else if (context.Registration.ProviderType is ProviderTypes.Deezer)
{
context.Response[Parameters.ExpiresIn] = context.Response["expires"];
context.Response["expires"] = null;
}

// Note: Exact Online returns a non-standard "expires_in"
// parameter formatted as a string instead of a numeric type.
else if (context.Registration.ProviderType is ProviderTypes.ExactOnline &&
long.TryParse((string?) context.Response[Parameters.ExpiresIn],
NumberStyles.Integer, CultureInfo.InvariantCulture, out long value))
{
context.Response.ExpiresIn = value;
}

// Note: Huawei returns a non-standard "error" parameter as a numeric value, which is not allowed
// by OpenIddict (that requires a string). Huawei also returns a non-standard "sub_error" parameter
// that contains additional error information, with which the error code can demonstrate a specific
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ public static class Revocation
* Revocation request preparation:
*/
MapNonStandardRequestParameters.Descriptor,
AttachNonStandardParameters.Descriptor,
OverrideHttpMethod.Descriptor,
AttachBearerAccessToken.Descriptor,

Expand Down Expand Up @@ -68,6 +69,40 @@ public ValueTask HandleAsync(PrepareRevocationRequestContext context)
}
}

/// <summary>
/// Contains the logic responsible for attaching non-standard
/// parameters to the request for the providers that require it.
/// </summary>
public sealed class AttachNonStandardParameters : IOpenIddictClientHandler<PrepareRevocationRequestContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<PrepareRevocationRequestContext>()
.UseSingletonHandler<AttachNonStandardParameters>()
.SetOrder(AttachHttpParameters<PrepareRevocationRequestContext>.Descriptor.Order - 250)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();

/// <inheritdoc/>
public ValueTask HandleAsync(PrepareRevocationRequestContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}

// Alibaba Cloud requires attaching the "client_id" parameter to revocation requests.
if (context.Registration.ProviderType is ProviderTypes.AlibabaCloud)
{
context.Request.ClientId = context.Registration.ClientId;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: OpenIddict is expected to take care of sending the client credentials automatically (client_id/client_secret/client_assertion). Were you seeing an error without that custom handler?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In my recollection, the first time I invoked the token revocation, it returned an error saying the request was missing the client_id parameter. So, I immediately tried adding this handler. After that, the error message changed to indicating that only refresh_token could be revoked.

Strangely, after removing the handler, I haven't been able to reproduce the original error. Furthermore, after adjusting the console sample, the token revocation request succeeded. Therefore, given the current situation, I'll remove it for now.

image

Copy link
Member

@kevinchalet kevinchalet Feb 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe the first time you tried, your client was confidential and was assigned a client secret?

When OpenIddict doesn't find an explicit revocation_endpoint_auth_methods_supported node in the server configuration, it uses client_secret_basic - i.e the default method - if both a client identifier and client secret are expected to be sent. In that case, the client credentials are not sent as regular OAuth 2.0 parameters but are sent using the HTTP WWW-Authenticate header. If no client secret is configured, the client_id is sent alone as a regular OAuth 2.0 parameter.

Can you please check the type of client you configured with Alibaba's portal?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you please check the type of client you configured with Alibaba's portal?

I tested both WebApp and NativeApp, and both can be revoked successfully.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tested both WebApp and NativeApp, and both can be revoked successfully.

After changing to web app, you updated your OpenIddict registration to call SetClientSecret(...), right?

Anyway, I took a look at the docs and they don't mention basic authentication support.
Just in case, we should probably tweak the returned configuration response to add client_secret_post to both token_endpoint_auth_methods_supported and revocation_endpoint_auth_methods_supported so that client credentials are always sent as part of the request form.

Can you please give it a try?

/// <summary>
/// Contains the logic responsible for amending the supported client
/// authentication methods for the providers that require it.
/// </summary>
public sealed class AmendClientAuthenticationMethods : IOpenIddictClientHandler<HandleConfigurationResponseContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<HandleConfigurationResponseContext>()
.UseSingletonHandler<AmendClientAuthenticationMethods>()
.SetOrder(ExtractTokenEndpointClientAuthenticationMethods.Descriptor.Order + 500)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(HandleConfigurationResponseContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
// Apple implements a non-standard client authentication method for its endpoints that
// is inspired by the standard private_key_jwt method but doesn't use the standard
// client_assertion/client_assertion_type parameters. Instead, the client assertion
// must be sent as a "dynamic" client secret using client_secret_post. Since the logic
// is the same as private_key_jwt, the configuration is amended to assume Apple supports
// private_key_jwt and an event handler is responsible for populating the client_secret
// parameter using the client assertion once it has been generated by OpenIddict.
if (context.Registration.ProviderType is ProviderTypes.Apple)
{
context.Configuration.RevocationEndpointAuthMethodsSupported.Add(
ClientAuthenticationMethods.PrivateKeyJwt);
context.Configuration.TokenEndpointAuthMethodsSupported.Add(
ClientAuthenticationMethods.PrivateKeyJwt);
}
// Atlassian doesn't return a "revocation_endpoint_auth_methods_supported" node in its
// server configuration but only supports the "client_secret_post" authentication method.
else if (context.Registration.ProviderType is ProviderTypes.Atlassian)
{
context.Configuration.RevocationEndpointAuthMethodsSupported.Add(
ClientAuthenticationMethods.ClientSecretPost);
}
// Google doesn't properly implement the device authorization grant, doesn't support
// basic client authentication for the device authorization endpoint and returns
// a generic "invalid_request" error when using "client_secret_basic" instead of
// sending the client identifier in the request form. To work around this limitation,
// "client_secret_post" is listed as the only supported client authentication method.
else if (context.Registration.ProviderType is ProviderTypes.Google)
{
context.Configuration.DeviceAuthorizationEndpointAuthMethodsSupported.Clear();
context.Configuration.DeviceAuthorizationEndpointAuthMethodsSupported.Add(
ClientAuthenticationMethods.ClientSecretPost);
}
// Huawei doesn't support sending the client credentials using basic authentication when
// using the device authorization grant, making basic authentication the default authentication
// method. To work around this compliance issue, "client_secret_post" is manually added here.
else if (context.Registration.ProviderType is ProviderTypes.Huawei)
{
context.Configuration.DeviceAuthorizationEndpointAuthMethodsSupported.Add(
ClientAuthenticationMethods.ClientSecretPost);
}
// LinkedIn doesn't support sending the client credentials using basic authentication but
// doesn't return a "token_endpoint_auth_methods_supported" node containing alternative
// authentication methods, making basic authentication the default authentication method.
// To work around this compliance issue, "client_secret_post" is manually added here.
else if (context.Registration.ProviderType is ProviderTypes.LinkedIn)
{
context.Configuration.TokenEndpointAuthMethodsSupported.Add(
ClientAuthenticationMethods.ClientSecretPost);
}
// Pro Santé Connect lists private_key_jwt as a supported client authentication method but
// only supports client_secret_basic/client_secret_post and tls_client_auth and plans to
// remove secret-based authentication support in late 2024 to force clients to use mTLS.
else if (context.Registration.ProviderType is ProviderTypes.ProSantéConnect)
{
context.Configuration.DeviceAuthorizationEndpointAuthMethodsSupported.Remove(
ClientAuthenticationMethods.PrivateKeyJwt);
context.Configuration.IntrospectionEndpointAuthMethodsSupported.Remove(
ClientAuthenticationMethods.PrivateKeyJwt);
context.Configuration.RevocationEndpointAuthMethodsSupported.Remove(
ClientAuthenticationMethods.PrivateKeyJwt);
context.Configuration.TokenEndpointAuthMethodsSupported.Remove(
ClientAuthenticationMethods.PrivateKeyJwt);
}
return default;
}
}

Copy link
Contributor Author

@gehongyan gehongyan Feb 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After changing to web app, you updated your OpenIddict registration to call SetClientSecret(...), right?

Actually, I added registrations for both application types in my test code to conduct these tests.

Test code
.AddAlibabaCloud(options =>
    options
        .SetRegion("CN")
        .SetProviderName("AliyunWeb")
        .SetProviderDisplayName("Aliyun Web")
        .SetClientId("******************")
        .SetClientSecret("***********************************************")
        .SetAccessType("offline")
        // .SetPrompt("admin_consent")
        .SetRedirectUri("callback/login/aliyun-web")
        .AddScopes("aliuid", "profile"))
// Aliyun NativeApp
.AddAlibabaCloud(options =>
    options
        .SetRegion("CN")
        .SetProviderName("AliyunNative")
        .SetProviderDisplayName("Aliyun Native")
        .SetClientId("******************")
        .SetClientSecret("***********************************************")
        // .SetPrompt("admin_consent")
        .SetRedirectUri("callback/login/aliyun-native")
        .AddScopes("aliuid", "profile"))
// Alibaba Cloud WebApp
.AddAlibabaCloud(options =>
    options
        .SetProviderName("AlibabaWeb")
        .SetProviderDisplayName("Alibaba Web")
        .SetClientId("******************")
        .SetClientSecret("***********************************************")
        .SetRedirectUri("callback/login/alibabacloud-web")
        .SetAccessType("offline")
        // .SetPrompt("admin_consent")
        .AddScopes("aliuid", "profile"))
// Alibaba Cloud NativeApp
.AddAlibabaCloud(options =>
    options
        .SetProviderName("AlibabaNative")
        .SetProviderDisplayName("Alibaba Native")
        .SetClientId("******************")
        .SetClientSecret("***********************************************")
        // .SetPrompt("admin_consent")
        .SetRedirectUri("callback/login/alibabacloud-native")
        .AddScopes("aliuid", "profile"))

Just in case, we should probably tweak the returned configuration response.

Sounds good. Completed. All of the registrations above still work well.

}

return default;
}
}

/// <summary>
/// Contains the logic responsible for overriding the HTTP method for the providers that require it.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,27 @@
</Environment>
</Provider>

<!--
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
█ ▄▄▀██ ████▄ ▄██ ▄▄▀█ ▄▄▀██ ▄▄▀█ ▄▄▀████ ▄▄▀██ █████ ▄▄▄ ██ ██ ██ ▄▄▀██
█ ▀▀ ██ █████ ███ ▄▄▀█ ▀▀ ██ ▄▄▀█ ▀▀ ████ █████ █████ ███ ██ ██ ██ ██ ██
█ ██ ██ ▀▀ █▀ ▀██ ▀▀ █ ██ ██ ▀▀ █ ██ ████ ▀▀▄██ ▀▀ ██ ▀▀▀ ██▄▀▀▄██ ▀▀ ██
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
-->

<Provider Name="AlibabaCloud" DisplayName="Alibaba Cloud (Aliyun)" Id="078caf87-3c5b-46aa-a8af-31e7cb2f4b7a"
Documentation="https://www.alibabacloud.com/help/en/ram/user-guide/oauth-management/">
<!--
Note: Alibaba Cloud serves global users, but it is known as Aliyun in China, which has a separate issuer and domain.
-->

<Environment Issuer="https://oauth.{(settings.Region?.ToUpperInvariant() is 'CN' ? 'aliyun' : 'alibabacloud')}.com/"
ConfigurationEndpoint="https://oauth.{(settings.Region?.ToUpperInvariant() is 'CN' ? 'aliyun' : 'alibabacloud')}.com/.well-known/openid-configuration" />

<Setting PropertyName="Region" ParameterName="region" Type="String" Required="false" DefaultValue="Global"
Description="The Alibaba Cloud (Aliyun) service region ('Global' for the global Alibaba Cloud by default, or can be set to 'CN' for Aliyun)" />
</Provider>

<!--
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
█ ▄▄▀██ ▄▀▄ █ ▄▄▀██ ▄▄▄ ██ ▄▄▄ ██ ▀██ ██
Expand Down