diff --git a/src/OpenIddict.Client/OpenIddictClientHandlers.cs b/src/OpenIddict.Client/OpenIddictClientHandlers.cs
index fab977d0..c4351614 100644
--- a/src/OpenIddict.Client/OpenIddictClientHandlers.cs
+++ b/src/OpenIddict.Client/OpenIddictClientHandlers.cs
@@ -156,8 +156,9 @@ public static partial class OpenIddictClientHandlers
throw new InvalidOperationException(SR.GetResourceString(SR.ID0309));
}
- if (context.GrantType is not (GrantTypes.AuthorizationCode or GrantTypes.Implicit or
- GrantTypes.Password or GrantTypes.RefreshToken))
+ if (context.GrantType is not (
+ GrantTypes.AuthorizationCode or GrantTypes.ClientCredentials or
+ GrantTypes.Implicit or GrantTypes.Password or GrantTypes.RefreshToken))
{
throw new InvalidOperationException(SR.FormatID0310(context.GrantType));
}
@@ -400,7 +401,7 @@ public static partial class OpenIddictClientHandlers
// Retrieve the client definition using the authorization server stored in the state token.
//
// Note: there's no guarantee that the state token was not replaced by a malicious actor
- // by a state token meant to be used with a different authorization server as part of a
+ // with a state token meant to be used with a different authorization server as part of a
// mix-up attack where the state token and the authorization code or access/identity tokens
// wouldn't match. To mitigate this, additional defenses are added later by other handlers.
@@ -1517,8 +1518,9 @@ public static partial class OpenIddictClientHandlers
GrantTypes.AuthorizationCode or GrantTypes.Implicit when HasResponseType(ResponseTypes.Code)
=> true,
- // For resource owner password credentials and refresh token requests, always send a token request.
- GrantTypes.Password or GrantTypes.RefreshToken => true,
+ // For client credentials, resource owner password credentials
+ // and refresh token requests, always send a token request.
+ GrantTypes.ClientCredentials or GrantTypes.Password or GrantTypes.RefreshToken => true,
_ => false
};
@@ -1955,9 +1957,10 @@ public static partial class OpenIddictClientHandlers
GrantTypes.AuthorizationCode or GrantTypes.Implicit when HasResponseType(ResponseTypes.Code)
=> (true, true, false),
- // An access token is always returned as part of resource
- // owner password credentials and refresh token responses.
- GrantTypes.Password or GrantTypes.RefreshToken => (true, true, false),
+ // An access token is always returned as part of client credentials,
+ // resource owner password credentials and refresh token responses.
+ GrantTypes.ClientCredentials or GrantTypes.Password or GrantTypes.RefreshToken
+ => (true, true, false),
_ => (false, false, false)
};
@@ -1973,13 +1976,13 @@ public static partial class OpenIddictClientHandlers
GrantTypes.AuthorizationCode or GrantTypes.Implicit when HasResponseType(ResponseTypes.Code) &&
context.StateTokenPrincipal!.HasScope(Scopes.OpenId) => (true, true, true),
- // The resource owner password credentials grant doesn't have an equivalent in
- // OpenID Connect so an identity token is typically never returned when using it.
- // However, certain server implementations - like OpenIddict - allow returning it
- // as a non-standard artifact. As such, the identity token is not considered required
- // but will always be validated using the same routine (except nonce validation)
- // if it is present in the token response.
- GrantTypes.Password => (true, false, true),
+ // The client credentials and resource owner password credentials grants don't have
+ // an equivalent in OpenID Connect so an identity token is typically never returned
+ // when using them. However, certain server implementations (like OpenIddict)
+ // allow returning it as a non-standard artifact. As such, the identity token
+ // is not considered required but will always be validated using the same routine
+ // (except nonce validation) if it is present in the token response.
+ GrantTypes.ClientCredentials or GrantTypes.Password => (true, false, true),
// An identity token may or may not be returned as part of refresh token responses
// depending on the policy adopted by the remote authorization server. As such,
@@ -2005,11 +2008,12 @@ public static partial class OpenIddictClientHandlers
GrantTypes.AuthorizationCode or GrantTypes.Implicit when HasResponseType(ResponseTypes.Code)
=> (true, false, false),
- // A refresh token may or may not be returned as part of resource owner password
- // credentials and refresh token responses depending on the policy adopted by the
- // remote authorization server. As such, a refresh token is never considered
- // required for refresh token responses.
- GrantTypes.Password or GrantTypes.RefreshToken => (true, false, false),
+ // A refresh token may or may not be returned as part of client credentials,
+ // resource owner password credentials and refresh token responses depending
+ // on the policy adopted by the remote authorization server. As such, a
+ // refresh token is never considered required for such token responses.
+ GrantTypes.ClientCredentials or GrantTypes.Password or GrantTypes.RefreshToken
+ => (true, false, false),
_ => (false, false, false)
};
@@ -4093,7 +4097,7 @@ public static partial class OpenIddictClientHandlers
// Store the identity of the authorization server in the state token principal to allow
// resolving it when handling the authorization callback. Note: additional security checks
- // are generally required to ensure the state token was not replaced by a state token
+ // are generally required to ensure the state token was not replaced with a state token
// meant to be used with a different authorization server (e.g using the "iss" parameter).
//
// See https://datatracker.ietf.org/doc/html/draft-bradley-oauth-jwt-encoded-state-09
diff --git a/src/OpenIddict.Client/OpenIddictClientService.cs b/src/OpenIddict.Client/OpenIddictClientService.cs
index a9402ca7..04e046ea 100644
--- a/src/OpenIddict.Client/OpenIddictClientService.cs
+++ b/src/OpenIddict.Client/OpenIddictClientService.cs
@@ -331,6 +331,84 @@ public class OpenIddictClientService
}
}
+ ///
+ /// Authenticates using the client credentials grant and resolves the corresponding tokens.
+ ///
+ /// The client registration.
+ /// The that can be used to abort the operation.
+ /// The response and a merged principal containing the claims extracted from the tokens and userinfo response.
+ public async ValueTask<(OpenIddictResponse Response, ClaimsPrincipal Principal)> AuthenticateWithClientCredentialsAsync(
+ OpenIddictClientRegistration registration, CancellationToken cancellationToken = default)
+ {
+ if (registration is null)
+ {
+ throw new ArgumentNullException(nameof(registration));
+ }
+
+ var configuration = await registration.ConfigurationManager.GetConfigurationAsync(default) ??
+ throw new InvalidOperationException(SR.GetResourceString(SR.ID0140));
+
+ if (configuration.TokenEndpoint is not { IsAbsoluteUri: true } ||
+ !configuration.TokenEndpoint.IsWellFormedOriginalString())
+ {
+ throw new InvalidOperationException(SR.FormatID0301(Metadata.TokenEndpoint));
+ }
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ // Note: this service is registered as a singleton service. As such, it cannot
+ // directly depend on scoped services like the validation provider. To work around
+ // this limitation, a scope is manually created for each method to this service.
+ var scope = _provider.CreateScope();
+
+ // Note: a try/finally block is deliberately used here to ensure the service scope
+ // can be disposed of asynchronously if it implements IAsyncDisposable.
+ try
+ {
+ var dispatcher = scope.ServiceProvider.GetRequiredService();
+ var factory = scope.ServiceProvider.GetRequiredService();
+ var transaction = await factory.CreateTransactionAsync();
+
+ var context = new ProcessAuthenticationContext(transaction)
+ {
+ Configuration = configuration,
+ GrantType = GrantTypes.ClientCredentials,
+ Issuer = registration.Issuer,
+ Registration = registration
+ };
+
+ await dispatcher.DispatchAsync(context);
+
+ if (context.IsRejected)
+ {
+ throw new OpenIddictExceptions.GenericException(
+ SR.FormatID0319(context.Error, context.ErrorDescription, context.ErrorUri),
+ context.Error, context.ErrorDescription, context.ErrorUri);
+ }
+
+ Debug.Assert(context.TokenResponse is not null, SR.GetResourceString(SR.ID4007));
+
+ // Create a composite principal containing claims resolved from the
+ // backchannel identity token and the userinfo token, if available.
+ return (context.TokenResponse, CreatePrincipal(
+ context.BackchannelIdentityTokenPrincipal,
+ context.UserinfoTokenPrincipal));
+ }
+
+ finally
+ {
+ if (scope is IAsyncDisposable disposable)
+ {
+ await disposable.DisposeAsync();
+ }
+
+ else
+ {
+ scope.Dispose();
+ }
+ }
+ }
+
///
/// Authenticates using the resource owner password credentials grant and resolves the corresponding tokens.
///