Browse Source

Update the client stack to support the client credentials grant

pull/1469/head
Kévin Chalet 4 years ago
parent
commit
b50a086862
  1. 46
      src/OpenIddict.Client/OpenIddictClientHandlers.cs
  2. 78
      src/OpenIddict.Client/OpenIddictClientService.cs

46
src/OpenIddict.Client/OpenIddictClientHandlers.cs

@ -156,8 +156,9 @@ public static partial class OpenIddictClientHandlers
throw new InvalidOperationException(SR.GetResourceString(SR.ID0309)); throw new InvalidOperationException(SR.GetResourceString(SR.ID0309));
} }
if (context.GrantType is not (GrantTypes.AuthorizationCode or GrantTypes.Implicit or if (context.GrantType is not (
GrantTypes.Password or GrantTypes.RefreshToken)) GrantTypes.AuthorizationCode or GrantTypes.ClientCredentials or
GrantTypes.Implicit or GrantTypes.Password or GrantTypes.RefreshToken))
{ {
throw new InvalidOperationException(SR.FormatID0310(context.GrantType)); 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. // 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 // 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 // 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. // 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) GrantTypes.AuthorizationCode or GrantTypes.Implicit when HasResponseType(ResponseTypes.Code)
=> true, => true,
// For resource owner password credentials and refresh token requests, always send a token request. // For client credentials, resource owner password credentials
GrantTypes.Password or GrantTypes.RefreshToken => true, // and refresh token requests, always send a token request.
GrantTypes.ClientCredentials or GrantTypes.Password or GrantTypes.RefreshToken => true,
_ => false _ => false
}; };
@ -1955,9 +1957,10 @@ public static partial class OpenIddictClientHandlers
GrantTypes.AuthorizationCode or GrantTypes.Implicit when HasResponseType(ResponseTypes.Code) GrantTypes.AuthorizationCode or GrantTypes.Implicit when HasResponseType(ResponseTypes.Code)
=> (true, true, false), => (true, true, false),
// An access token is always returned as part of resource // An access token is always returned as part of client credentials,
// owner password credentials and refresh token responses. // resource owner password credentials and refresh token responses.
GrantTypes.Password or GrantTypes.RefreshToken => (true, true, false), GrantTypes.ClientCredentials or GrantTypes.Password or GrantTypes.RefreshToken
=> (true, true, false),
_ => (false, false, false) _ => (false, false, false)
}; };
@ -1973,13 +1976,13 @@ public static partial class OpenIddictClientHandlers
GrantTypes.AuthorizationCode or GrantTypes.Implicit when HasResponseType(ResponseTypes.Code) && GrantTypes.AuthorizationCode or GrantTypes.Implicit when HasResponseType(ResponseTypes.Code) &&
context.StateTokenPrincipal!.HasScope(Scopes.OpenId) => (true, true, true), context.StateTokenPrincipal!.HasScope(Scopes.OpenId) => (true, true, true),
// The resource owner password credentials grant doesn't have an equivalent in // The client credentials and resource owner password credentials grants don't have
// OpenID Connect so an identity token is typically never returned when using it. // an equivalent in OpenID Connect so an identity token is typically never returned
// However, certain server implementations - like OpenIddict - allow returning it // when using them. However, certain server implementations (like OpenIddict)
// as a non-standard artifact. As such, the identity token is not considered required // allow returning it as a non-standard artifact. As such, the identity token
// but will always be validated using the same routine (except nonce validation) // is not considered required but will always be validated using the same routine
// if it is present in the token response. // (except nonce validation) if it is present in the token response.
GrantTypes.Password => (true, false, true), GrantTypes.ClientCredentials or GrantTypes.Password => (true, false, true),
// An identity token may or may not be returned as part of refresh token responses // 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, // 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) GrantTypes.AuthorizationCode or GrantTypes.Implicit when HasResponseType(ResponseTypes.Code)
=> (true, false, false), => (true, false, false),
// A refresh token may or may not be returned as part of resource owner password // A refresh token may or may not be returned as part of client credentials,
// credentials and refresh token responses depending on the policy adopted by the // resource owner password credentials and refresh token responses depending
// remote authorization server. As such, a refresh token is never considered // on the policy adopted by the remote authorization server. As such, a
// required for refresh token responses. // refresh token is never considered required for such token responses.
GrantTypes.Password or GrantTypes.RefreshToken => (true, false, false), GrantTypes.ClientCredentials or GrantTypes.Password or GrantTypes.RefreshToken
=> (true, false, false),
_ => (false, 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 // 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 // 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). // 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 // See https://datatracker.ietf.org/doc/html/draft-bradley-oauth-jwt-encoded-state-09

78
src/OpenIddict.Client/OpenIddictClientService.cs

@ -331,6 +331,84 @@ public class OpenIddictClientService
} }
} }
/// <summary>
/// Authenticates using the client credentials grant and resolves the corresponding tokens.
/// </summary>
/// <param name="registration">The client registration.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>The response and a merged principal containing the claims extracted from the tokens and userinfo response.</returns>
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<IOpenIddictClientDispatcher>();
var factory = scope.ServiceProvider.GetRequiredService<IOpenIddictClientFactory>();
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();
}
}
}
/// <summary> /// <summary>
/// Authenticates using the resource owner password credentials grant and resolves the corresponding tokens. /// Authenticates using the resource owner password credentials grant and resolves the corresponding tokens.
/// </summary> /// </summary>

Loading…
Cancel
Save