openiddict-samples icon indicating copy to clipboard operation
openiddict-samples copied to clipboard

Any samples for grant type client credentials and client assertions?

Open svasui123 opened this issue 2 years ago • 11 comments

Confirm you've already contributed to this project or that you sponsor it

  • [X] I confirm I'm a sponsor or a contributor

Version

3.x

Question

We are testing a Smart FHIR implementation with openiddict. I am stuck on the client assertion flow and would request any links to documentation or samples that implement or can help implement client assertion and client credentials.

svasui123 avatar May 26 '22 19:05 svasui123

private_key_jwt is already supported by the new OpenIddict client as it was required for the Apple SignIn provider but the OpenIddict server doesn't support it yet. Native support for client assertions is tracked by https://github.com/openiddict/openiddict-core/issues/1251.

If it's important for your company, let me know, it's something we can likely fund and implement for 4.0.

kevinchalet avatar May 27 '22 14:05 kevinchalet

It would definitely be of great value to anyone who is using Openiddict to comply with FHIR and OAUTH2 requirements. Assertion grant flows are 1 of the 4 vignettes that are tested. I will look at the samples and define a custom flow that handles assertion grants (I saw your comment about this flow technically being similar to the password flow). We are currently going through the certification process and this is the last hurdle.

Yes adding it to 4.0 would help.

svasui123 avatar May 27 '22 16:05 svasui123

Assertion grant flows are 1 of the 4 vignettes that are tested. I will look at the samples and define a custom flow that handles assertion grants (I saw your comment about this flow technically being similar to the password flow).

It's worth noting that client assertions != assertion grants: the former are generated to serve as client credentials while the second are generated to serve as an entity identity (typically a user).

kevinchalet avatar May 28 '22 20:05 kevinchalet

Noted. Thanks for this elaboration.

How do I override the default client credentials validation so that it allows to check for client assertions?

From: Kévin Chalet @.> Sent: Saturday, May 28, 2022 4:17 PM To: openiddict/openiddict-samples @.> Cc: svasui123 @.>; Author @.> Subject: Re: [openiddict/openiddict-samples] Any samples for grant type client credentials and client assertions? (Issue #193)

Assertion grant flows are 1 of the 4 vignettes that are tested. I will look at the samples and define a custom flow that handles assertion grants (I saw your comment about this flow technically being similar to the password flow).

It's worth noting that client assertions != assertion grants: the former are generated to serve as client credentials while the second are generated to serve as an entity identity (typically a user).

— Reply to this email directly, view it on GitHub https://github.com/openiddict/openiddict-samples/issues/193#issuecomment-1140324185 , or unsubscribe https://github.com/notifications/unsubscribe-auth/AZDMOETU4B7MJSNIZPVZ2MLVMJ5LJANCNFSM5XCFPXCA . You are receiving this because you authored the thread. https://github.com/notifications/beacon/AZDMOEVJCEMGWSTCU4IOT73VMJ5LJA5CNFSM5XCFPXCKYY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOIP37OWI.gif Message ID: @.*** @.***> >

svasui123 avatar Jun 01 '22 15:06 svasui123

If you can't/don't want to wait for built-in support, you'll need to replace these 2 handlers:

  • https://github.com/openiddict/openiddict-core/blob/eb8715d96440c05400b3613d37ca64385d79c042/src/OpenIddict.Server/OpenIddictServerHandlers.Exchange.cs#L498-L537: ValidateClientCredentialsParameters blocks the request if client_id and/or client_secret is/are missing. You'll want to replace it by an equivalent handler that supports the client_assertion/client_assertion_type parameters.

  • https://github.com/openiddict/openiddict-core/blob/eb8715d96440c05400b3613d37ca64385d79c042/src/OpenIddict.Server/OpenIddictServerHandlers.Exchange.cs#L928-L986: ValidateClientSecret only works with a client_secret so you'll need a custom handler that uses client_assertion/client_assertion_type.

It's not mandatory but you'll probably want to tweak the configuration document to indicate your server instance supports client assertions. For that, you'll need to add private_key_jwt (assuming you're using asymmetric signing keys) to HandleConfigurationRequestContext.TokenEndpointAuthenticationMethods.

See https://github.com/openiddict/openiddict-core/blob/eb8715d96440c05400b3613d37ca64385d79c042/src/OpenIddict.Server/OpenIddictServerHandlers.Discovery.cs#L491-L535 for the handler responsible for adding client_secret_basic/client_secret_post.

kevinchalet avatar Jun 01 '22 16:06 kevinchalet

We will use the built-in support as soon as it is available.

Having said that I think working with these handlers would help me better understand this extremely well done package.

From: Kévin Chalet @.> Sent: Wednesday, June 1, 2022 12:23 PM To: openiddict/openiddict-samples @.> Cc: svasui123 @.>; Author @.> Subject: Re: [openiddict/openiddict-samples] Any samples for grant type client credentials and client assertions? (Issue #193)

If you can't/don't want to wait for built-in support, you'll need to replace these 2 handlers:

  • https://github.com/openiddict/openiddict-core/blob/eb8715d96440c05400b3613d37ca64385d79c042/src/OpenIddict.Server/OpenIddictServerHandlers.Exchange.cs#L498-L537: ValidateClientCredentialsParameters blocks the request if client_id and/or client_secret is/are missing. You'll want to replace it by an equivalent handler that supports the client_assertion/client_assertion_type parameters.
  • https://github.com/openiddict/openiddict-core/blob/eb8715d96440c05400b3613d37ca64385d79c042/src/OpenIddict.Server/OpenIddictServerHandlers.Exchange.cs#L928-L986: ValidateClientSecret only works with a client_secret so you'll need a custom handler that uses client_assertion/client_assertion_type.

It's not mandatory but you'll probably want to tweak the configuration document to indicate your server instance supports client assertions. For that, you'll need to add private_key_jwt (assuming you're using asymmetric signing keys) to HandleConfigurationRequestContext.TokenEndpointAuthenticationMethods.

See https://github.com/openiddict/openiddict-core/blob/eb8715d96440c05400b3613d37ca64385d79c042/src/OpenIddict.Server/OpenIddictServerHandlers.Discovery.cs#L491-L535 for the handler responsible for adding client_secret_basic/client_secret_post.

— Reply to this email directly, view it on GitHub https://github.com/openiddict/openiddict-samples/issues/193#issuecomment-1143827420 , or unsubscribe https://github.com/notifications/unsubscribe-auth/AZDMOEUM5VE2SHDGE57MLADVM6FABANCNFSM5XCFPXCA . You are receiving this because you authored the thread. https://github.com/notifications/beacon/AZDMOEVOP6XT7CTTGURZJEDVM6FABA5CNFSM5XCFPXCKYY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOIQWWXXA.gif Message ID: @.*** @.***> >

svasui123 avatar Jun 01 '22 17:06 svasui123

I followed the instructions given in https://github.com/openiddict/openiddict-core/issues/736 to wire up an event handler. When I try to add options.AddEventHandler<HandleConfigurationRequestContext, MyEventHandler> (); under AddServer it is giving me an error. What is the new syntax?

svasui123 avatar Jun 01 '22 20:06 svasui123

Having said that I think working with these handlers would help me better understand this extremely well done package.

Haha, thanks for the kind words! 😃

What is the new syntax?

I'd recommend using the same approach as the one used for the built-in handlers:

public class YourCustomHandler : IOpenIddictServerHandler<...>
{
    /// <summary>
    /// Gets the default descriptor definition assigned to this handler.
    /// </summary>
    public static OpenIddictServerHandlerDescriptor Descriptor { get; }
        = OpenIddictServerHandlerDescriptor.CreateBuilder<...>()
            .UseSingletonHandler<YourCustomHandler>()
            .SetOrder(...)
            .SetType(OpenIddictServerHandlerType.Custom)
            .Build();

    /// <inheritdoc/>
    public ValueTask HandleAsync(... context)
    {
        ...
    }
}
options.AddEventHandler(YourCustomHandler.Descriptor);

Alternatively, you can also use inline handlers, as shown in https://documentation.openiddict.com/guides/index.html#events-model.

kevinchalet avatar Jun 02 '22 08:06 kevinchalet

Hi,

I am able to override the handlers for ValidateClientCredentialsParameters and the AttachClientAuthenticationMethods. When I try to override the ValidAteCLientSecret I get run time error:

System.InvalidOperationException: 'The core services must be registered when enabling the OpenIddict server feature. To register the OpenIddict core services, reference the 'OpenIddict.Core' package and call 'services.AddOpenIddict().AddCore()' from 'ConfigureServices'.

My Program.cs

builder.Services.AddOpenIddict()

// Register the OpenIddict core components.

.AddCore(options =>

{

    // Check if next line fixes the error.

    options.SetDefaultApplicationEntity<OpenIddictEntityFrameworkCoreApplication>();

    // Configure OpenIddict to use the Entity Framework Core stores and models.

    // Note: call ReplaceDefaultEntities() to replace the default OpenIddict entities.

   options.UseEntityFrameworkCore()

           .UseDbContext<ApplicationDbContext>()

           ;





    // Enable Quartz.NET integration.

    //options.UseQuartz();

})



// Register the OpenIddict server components.

.AddServer(options =>

{

    // Enable the authorization, logout, token and userinfo endpoints.

    options.AllowClientCredentialsFlow();

    options.SetTokenEndpointUris(builder.Configuration["OpenIddict:Endpoints:Token"]);



    Console.WriteLine("Config:" + builder.Configuration["OpenIddict:Endpoints:Token"]);



    // to enable revoke tokens

    // Encryption and signing of tokens

    options

    .AddEphemeralEncryptionKey()

    .AddEphemeralSigningKey()

    .DisableAccessTokenEncryption()

    ;



    // Mark the "email", "profile" and "roles" scopes as supported scopes.

    options.RegisterScopes(Scopes.Email, Scopes.Profile, Scopes.Roles, Scopes.OfflineAccess, Scopes.OpenId, "scp:fhirUser", "scp:patient/Patient.read");

    //options.RegisterClaims(Claims.Subject, Claims.Name, Claims.Username, "fhirUser");



    // Register the signing and encryption credentials.

   options.AddDevelopmentEncryptionCertificate()

    .AddDevelopmentSigningCertificate();



    // Register the ASP.NET Core host and configure the ASP.NET Core-specific options.

    options.UseAspNetCore()

    .EnableTokenEndpointPassthrough()

    ;

    options.AddEventHandler(MyValidateClientCredentialsParameters.Descriptor);

    options.AddEventHandler(MyValidateClientSecret.Descriptor);

    options.AddEventHandler(MyAttachClientAuthenticationMethods.Descriptor);



})    // Register the OpenIddict validation components.

.AddValidation(options =>

{

    options.UseLocalServer();

    // Register the ASP.NET Core host.

    options.UseAspNetCore();

    //to cater to immediate access token revocation

    options.EnableTokenEntryValidation();

});

Code where it crashes:

   public class MyValidateClientSecret : ValidateClientSecret, IOpenIddictServerHandler<ValidateTokenRequestContext>

    {

        private readonly IOpenIddictApplicationManager _applicationManager;



        public MyValidateClientSecret() => throw new InvalidOperationException(OpenIddictResources.GetResourceString(OpenIddictResources.ID0016));



        public MyValidateClientSecret(IOpenIddictApplicationManager applicationManager)

            => _applicationManager = applicationManager ?? throw new ArgumentNullException(nameof(applicationManager));

Crashes on the above line

        /// <summary>

        /// Gets the default descriptor definition assigned to this handler.

        /// Turn off RequireCLientId

        /// </summary>

        public static new OpenIddictServerHandlerDescriptor Descriptor { get; }

            = OpenIddictServerHandlerDescriptor.CreateBuilder<ValidateTokenRequestContext>()

                //.AddFilter<RequireClientIdParameter>()

                .AddFilter<RequireDegradedModeDisabled>()

                .UseScopedHandler<MyValidateClientSecret>()

                .SetOrder(OpenIddictServerHandlers.Authentication.ValidateClientType.Descriptor.Order + 1_000)

                .SetType(OpenIddictServerHandlerType.Custom)

                .Build();

What am I missing?

Best Regards

Vasu

From: Kévin Chalet @.> Sent: Thursday, June 2, 2022 4:02 AM To: openiddict/openiddict-samples @.> Cc: svasui123 @.>; Author @.> Subject: Re: [openiddict/openiddict-samples] Any samples for grant type client credentials and client assertions? (Issue #193)

Having said that I think working with these handlers would help me better understand this extremely well done package.

Haha, thanks for the kind words! 😃

What is the new syntax?

I'd recommend using the same approach as the one used for the built-in handlers:

public class YourCustomHandler : IOpenIddictServerHandler<...>

{

/// <summary>

/// Gets the default descriptor definition assigned to this handler.

/// </summary>

public static OpenIddictServerHandlerDescriptor Descriptor { get; }

    = OpenIddictServerHandlerDescriptor.CreateBuilder<...>()

        .UseSingletonHandler<YourCustomHandler>()

        .SetOrder(...)

        .SetType(OpenIddictServerHandlerType.Custom)

        .Build();



/// <inheritdoc/>

public ValueTask HandleAsync(... context)

{

    ...

}

} options.AddEventHandler(YourCustomHandler.Descriptor);

Alternatively, you can also use inline handlers, as shown in https://documentation.openiddict.com/guides/index.html#events-model.

— Reply to this email directly, view it on GitHub https://github.com/openiddict/openiddict-samples/issues/193#issuecomment-1144568485 , or unsubscribe https://github.com/notifications/unsubscribe-auth/AZDMOESTNAI6CTEC3H27PWLVNBTARANCNFSM5XCFPXCA . You are receiving this because you authored the thread. https://github.com/notifications/beacon/AZDMOEQJJLQOB2QKXEA3GWLVNBTARA5CNFSM5XCFPXCKYY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOIQ4LVJI.gif Message ID: @.*** @.***> >

svasui123 avatar Jun 05 '22 17:06 svasui123

I suspect this could be related to the fact you're trying to subclass the built-in ValidateClientSecret handler, which is something you should never do.

Instead, create your own independent handler - that don't subclass ValidateClientSecret - and remove the built-in one by doing options.RemoveEventHandler(ValidateClientSecret.Descriptor).

kevinchalet avatar Jun 05 '22 18:06 kevinchalet

Hi Kevin,

This is what I ended up doing. I am currently working on adding an additional table to verify the jwksurl and store the public keys (if jwks url is not given then it will default to the stored key .. this is not recommended but is a requirement). Also messages are hard coded since we first wanted to get a working model and the documentation URI are incorrect.

Best Regards

Vasu

From: @.*** @.> Sent: Sunday, June 5, 2022 6:09 PM To: 'openiddict/openiddict-samples' @.>; 'openiddict/openiddict-samples' @.> Cc: 'Author' @.> Subject: RE: [openiddict/openiddict-samples] Any samples for grant type client credentials and client assertions? (Issue #193)

Thank you for your prompt response.

I was actually overriding the method by subclassing and using new to override.

I removed the subclassing and removed the built-in handlers. I am now able to see the flow into the custom handlers after overriding ValidateClientIdParameter to allow requests with grant type client credentials and clientassertion and clientassertiontype in the request.

I will keep you posted on the progress and share the code after implementing the client-assertion requirements.

From: Kévin Chalet @.*** @.> > Sent: Sunday, June 5, 2022 2:40 PM To: openiddict/openiddict-samples @. @.> > Cc: svasui123 @. @.> >; Author @. @.***> > Subject: Re: [openiddict/openiddict-samples] Any samples for grant type client credentials and client assertions? (Issue #193)

I suspect this could be related to the fact you're trying to subclass the built-in ValidateClientSecret handler, which is something you should never do.

Instead, create your own independent handler - that don't subclass ValidateClientSecret - and remove the built-in one by doing options.RemoveEventHandler(ValidateClientSecret.Descriptor).

— Reply to this email directly, view it on GitHub https://github.com/openiddict/openiddict-samples/issues/193#issuecomment-1146863255 , or unsubscribe https://github.com/notifications/unsubscribe-auth/AZDMOESFQM6ZZPQ4RU27K7LVNTYAFANCNFSM5XCFPXCA . You are receiving this because you authored the thread. https://github.com/notifications/beacon/AZDMOEUVICWKORB42SFUH6LVNTYAFA5CNFSM5XCFPXCKYY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOIRN35FY.gif Message ID: @.*** @.***> >

using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.ResponseCompression; using Microsoft.EntityFrameworkCore; using OpenIddict.EntityFrameworkCore.Models; using OpenIddict.Validation.AspNetCore; using SampleOverride; using SampleOverride.Data; using static OpenIddict.Abstractions.OpenIddictConstants; using static OpenIddict.Server.OpenIddictServerHandlers.Exchange; using static SampleOverride.MyEventHandler;

var builder = WebApplication.CreateBuilder(args); //Add compression builder.Services.Configure<GzipCompressionProviderOptions>(options => options.Level = System.IO.Compression.CompressionLevel.Fastest); builder.Services.AddResponseCompression(options => { options.EnableForHttps = true; options.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(new[] { "application/fhir+xml", "application/fhir+json" }); options.Providers.Add<GzipCompressionProvider>(); }); // Add services to the container. var connectionString = builder.Configuration.GetConnectionString("DefaultConnection"); builder.Services.AddDbContext<ApplicationDbContext>(options => { // Configure the context to use Microsoft SQL Server. options.UseSqlServer(connectionString);

// Register the entity sets needed by OpenIddict.
options.UseOpenIddict();

}); builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true) .AddEntityFrameworkStores<ApplicationDbContext>() .AddDefaultTokenProviders(); // Tell the Net stack to only use TLS1.2 //System.Net.ServicePointManager.SecurityProtocol = System.Net.SecurityProtocolType.Tls12;

//builder.Services.Configure<IdentityOptions>(options => //{ // // Configure Identity to use the same JWT claims as OpenIddict instead // // of the legacy WS-Federation claims it uses by default (ClaimTypes), // // which saves you from doing the mapping in your authorization controller. // options.ClaimsIdentity.UserNameClaimType = Claims.Name; // options.ClaimsIdentity.UserIdClaimType = Claims.Subject; // options.ClaimsIdentity.RoleClaimType = Claims.Role; // options.ClaimsIdentity.EmailClaimType = Claims.Email;

// options.SignIn.RequireConfirmedAccount = false; //});

builder.Services.AddOpenIddict()

// Register the OpenIddict core components.
.AddCore(options =>
{
    options.SetDefaultApplicationEntity<OpenIddictEntityFrameworkCoreApplication>();
    // Configure OpenIddict to use the Entity Framework Core stores and models.
    // Note: call ReplaceDefaultEntities() to replace the default OpenIddict entities.
    options.UseEntityFrameworkCore()
           .UseDbContext<ApplicationDbContext>()
           ;


    // Enable Quartz.NET integration.
    //options.UseQuartz();
})

// Register the OpenIddict server components.
.AddServer(options =>
{
    // Enable the authorization, logout, token and userinfo endpoints.
    options.AllowClientCredentialsFlow();
    options.SetTokenEndpointUris(builder.Configuration["OpenIddict:Endpoints:Token"]);

    Console.WriteLine("Config:" + builder.Configuration["OpenIddict:Endpoints:Token"]);

    // to enable revoke tokens
    // Encryption and signing of tokens
    options
    .AddEphemeralEncryptionKey()
    .AddEphemeralSigningKey()
    .DisableAccessTokenEncryption()
    ;

    // Mark the "email", "profile" and "roles" scopes as supported scopes.
    options.RegisterScopes(Scopes.Email, Scopes.Profile, Scopes.Roles, Scopes.OfflineAccess, Scopes.OpenId, "scp:fhirUser", "scp:patient/Patient.read");
    //options.RegisterClaims(Claims.Subject, Claims.Name, Claims.Username, "fhirUser");

    // Register the signing and encryption credentials.
    options.AddDevelopmentEncryptionCertificate()
    .AddDevelopmentSigningCertificate();

    // Register the ASP.NET Core host and configure the ASP.NET Core-specific options.
    options.UseAspNetCore()
    .EnableTokenEndpointPassthrough()
    ;
    options.RemoveEventHandler(OpenIddict.Server.OpenIddictServerHandlers.Exchange.ValidateClientIdParameter.Descriptor);
    options.RemoveEventHandler(OpenIddict.Server.OpenIddictServerHandlers.Exchange.ValidateClientCredentialsParameters.Descriptor);
    options.RemoveEventHandler(OpenIddict.Server.OpenIddictServerHandlers.Exchange.ValidateClientSecret.Descriptor);

    options.AddEventHandler(MyEventHandler.MyValidateClientIdParameter.Descriptor);
    options.AddEventHandler(MyEventHandler.MyValidateClientCredentialsParameters.Descriptor);
    options.AddEventHandler(MyEventHandler.MyValidateClientSecret.Descriptor);
    options.AddEventHandler(MyAttachClientAuthenticationMethods.Descriptor);

})    // Register the OpenIddict validation components.
.AddValidation(options =>
{
    options.UseLocalServer();
    // Register the ASP.NET Core host.
    options.UseAspNetCore();
    //to cater to immediate access token revocation
    options.EnableTokenEntryValidation();
});

builder.Services.AddRazorPages();

builder.Services.AddHostedService<SampleOverride.Worker>(); builder.Services.AddAuthentication(OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme);

var app = builder.Build();

// Configure the HTTP request pipeline. if (app.Environment.IsDevelopment()) { app.UseMigrationsEndPoint(); } else { app.UseExceptionHandler("/Error"); // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. app.UseHsts(); }

app.UseHttpsRedirection(); app.UseStaticFiles();

app.UseRouting(); app.UseAuthentication(); app.UseAuthorization();

app.UseEndpoints(endpoints => { endpoints.MapDefaultControllerRoute(); });

app.MapRazorPages();

app.Run();

using Microsoft.IdentityModel.Tokens; using OpenIddict.Abstractions; using OpenIddict.Server; using System.Diagnostics; using System.IdentityModel.Tokens.Jwt; using System.Net.Http.Headers; using System.Security.Cryptography; using static OpenIddict.Abstractions.OpenIddictConstants; using static OpenIddict.Server.OpenIddictServerEvents; using static OpenIddict.Server.OpenIddictServerHandlerFilters; using static OpenIddict.Server.OpenIddictServerHandlers.Discovery; using static OpenIddict.Server.OpenIddictServerHandlers.Exchange;

namespace SampleOverride { public class MyEventHandler { static string issval = string.Empty; public class MyAttachClientAuthenticationMethods : IOpenIddictServerHandler<HandleConfigurationRequestContext> { ///

/// Gets the default descriptor definition assigned to this handler. /// public static OpenIddictServerHandlerDescriptor Descriptor { get; } = OpenIddictServerHandlerDescriptor.CreateBuilder<HandleConfigurationRequestContext>() .UseSingletonHandler<MyAttachClientAuthenticationMethods>() .SetOrder(AttachResponseTypes.Descriptor.Order + 1_000) .SetType(OpenIddictServerHandlerType.Custom) .Build();

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

            if (context.IntrospectionEndpoint is not null)
            {
                context.IntrospectionEndpointAuthenticationMethods.Add(ClientAuthenticationMethods.ClientSecretBasic);
                context.IntrospectionEndpointAuthenticationMethods.Add(ClientAuthenticationMethods.ClientSecretPost);
            }

            if (context.RevocationEndpoint is not null)
            {
                context.RevocationEndpointAuthenticationMethods.Add(ClientAuthenticationMethods.ClientSecretBasic);
                context.RevocationEndpointAuthenticationMethods.Add(ClientAuthenticationMethods.ClientSecretPost);
            }

            if (context.TokenEndpoint is not null)
            {
                context.TokenEndpointAuthenticationMethods.Add(ClientAuthenticationMethods.ClientSecretBasic);
                context.TokenEndpointAuthenticationMethods.Add(ClientAuthenticationMethods.ClientSecretPost);
                context.TokenEndpointAuthenticationMethods.Add("private_key_jwt");
            }

            return default;
        }
    }
    /// <summary>
    /// Contains the logic responsible for rejecting token requests specifying an invalid client secret.
    /// Note: this handler is not used when the degraded mode is enabled.
    /// </summary>
    public  class MyValidateClientSecret :  IOpenIddictServerHandler<ValidateTokenRequestContext>
    {
        private readonly IOpenIddictApplicationManager _applicationManager;

        public MyValidateClientSecret() => throw new InvalidOperationException(OpenIddictResources.GetResourceString(OpenIddictResources.ID0016));

        public MyValidateClientSecret(IOpenIddictApplicationManager applicationManager)
            => _applicationManager = applicationManager ?? throw new ArgumentNullException(nameof(applicationManager));

        /// <summary>
        /// Gets the default descriptor definition assigned to this handler.
        /// Turn off RequireCLientId
        /// </summary>
        public static  OpenIddictServerHandlerDescriptor Descriptor { get; }
            = OpenIddictServerHandlerDescriptor.CreateBuilder<ValidateTokenRequestContext>()
                //.AddFilter<RequireClientIdParameter>()
                .AddFilter<RequireDegradedModeDisabled>()
                .UseScopedHandler<MyValidateClientSecret>()
                .SetOrder(OpenIddictServerHandlers.Authentication.ValidateClientType.Descriptor.Order + 1_000)
                .SetType(OpenIddictServerHandlerType.Custom)
                .Build();

        /// <inheritdoc/>
        public async ValueTask HandleAsync(ValidateTokenRequestContext context)
        {
            Console.WriteLine("In custom validate token");

            if (context is null)
            {
                throw new ArgumentNullException(nameof(context));
            }
            //vasu comment starts
            if (!string.IsNullOrEmpty(context.Request.ClientAssertion))
            {

                //get the iss from the assertion
                //verify if clientid exists.. if yes continue otherwise return error
                if (!string.IsNullOrEmpty(issval)) 
                {
                    var applicationn = await _applicationManager.FindByClientIdAsync(issval) ??
                        throw new InvalidOperationException(OpenIddictResources.GetResourceString(OpenIddictResources.ID0032));
                } 
                return;
            };
            //vasu changes end
            Debug.Assert(!string.IsNullOrEmpty(context.ClientId), OpenIddictResources.FormatID4000(Parameters.ClientId));

            var application = await _applicationManager.FindByClientIdAsync(context.ClientId) ??
                throw new InvalidOperationException(OpenIddictResources.GetResourceString(OpenIddictResources.ID0032));

            // If the application is a public client, don't validate the client secret.
            if (await _applicationManager.HasClientTypeAsync(application, ClientTypes.Public))
            {
                return;
            }

            Debug.Assert(!string.IsNullOrEmpty(context.ClientSecret), OpenIddict.Abstractions.OpenIddictResources.FormatID4000(Parameters.ClientSecret));

            if (!await _applicationManager.ValidateClientSecretAsync(application, context.ClientSecret))
            {
                context.Logger.LogInformation(OpenIddict.Abstractions.OpenIddictResources.GetResourceString(OpenIddict.Abstractions.OpenIddictResources.ID6085), context.ClientId);

                context.Reject(
                    error: Errors.InvalidClient,
                    description: OpenIddict.Abstractions.OpenIddictResources.GetResourceString(OpenIddict.Abstractions.OpenIddictResources.ID2055),
                    uri: OpenIddict.Abstractions.OpenIddictResources.FormatID8000(OpenIddict.Abstractions.OpenIddictResources.ID2055));

                return;
            }
        }
    }
    /// <summary>
    /// Contains the logic responsible for rejecting token requests that don't
    /// specify client credentials for the client credentials grant type.
    /// </summary>
    public class MyValidateClientCredentialsParameters : IOpenIddictServerHandler<ValidateTokenRequestContext>
    {
        /// <summary>
        /// Gets the default descriptor definition assigned to this handler.
        /// </summary>
        public static OpenIddictServerHandlerDescriptor Descriptor { get; }
            = OpenIddictServerHandlerDescriptor.CreateBuilder<ValidateTokenRequestContext>()
                .UseSingletonHandler<MyValidateClientCredentialsParameters>()
                .SetOrder(ValidateAuthorizationCodeParameter.Descriptor.Order + 1_000)
                .SetType(OpenIddictServerHandlerType.Custom)
                .Build();

        /// <inheritdoc/>
        public ValueTask HandleAsync(ValidateTokenRequestContext context)
        {
            if (context is null)
            {
                throw new ArgumentNullException(nameof(context));
            }
            //to be done
            Console.WriteLine("In custom validation");
            //check if client assertion and client credentials
            if (context.Request.IsClientCredentialsGrantType())
            {
                if (!string.IsNullOrEmpty(context.Request.ClientAssertion) && !string.IsNullOrEmpty(context.Request.ClientAssertionType))
                {
                    if (context.Request.ClientAssertionType != "urn:ietf:params:oauth:client-assertion-type:jwt-bearer")
                    {
                        context.Reject(
                            error: Errors.InvalidRequest,
                            description: "Client Assertion Type is not valid",
                            uri: OpenIddict.Abstractions.OpenIddictResources.FormatID8000(OpenIddict.Abstractions.OpenIddictResources.ID2057));
                        return default;
                    };
                    //base64decode the client assertion
                    //if the jwks_url is not given then use public key stored in db .. to implement
                    //if the jwks_url is given, get the public key for the given keyid in the client assertion
                    string tokenStr = context.Request.ClientAssertion;
                    string[] tokenParts = tokenStr.Split('.');
                    if (tokenParts.Length != 3)
                    {
                        context.Reject(
                            error: Errors.InvalidRequest,
                            description: "Client Assertion is not valid",
                            uri: OpenIddict.Abstractions.OpenIddictResources.FormatID8000(OpenIddict.Abstractions.OpenIddictResources.ID2057));
                        return default;
                    };
                    Dictionary<string, string> Tags = new Dictionary<string, string>();
                    Tags = GetTokenInfo(context.Request.ClientAssertion);
                    issval = Tags.FirstOrDefault(x => x.Key == "iss").Value;
                   
                    var handler = new JwtSecurityTokenHandler();

                    if (!handler.CanReadToken(issval))
                    {
                        context.Reject(
                            error: Errors.InvalidRequest,
                            description: "Iss is not valid",
                            uri: OpenIddict.Abstractions.OpenIddictResources.FormatID8000(OpenIddict.Abstractions.OpenIddictResources.ID2057));
                        return default;

                    };
                    //UpdateAppSetting.UpdateJson(issval);
                    var subval = Tags.FirstOrDefault(x => x.Key == "sub").Value;
                    if (issval != subval)
                    {
                        context.Reject(
                            error: Errors.InvalidRequest,
                            description: "Iss and Sub are not the same",
                            uri: OpenIddict.Abstractions.OpenIddictResources.FormatID8000(OpenIddict.Abstractions.OpenIddictResources.ID2057));
                        return default;

                    };

                    Tags = GetTokenInfo(issval);
                    var jwksuri = Tags.FirstOrDefault(x => x.Key == "jwks_url").Value;
                    HttpResponseMessage response;

                    using (var client = new HttpClient())
                    {
                        client.BaseAddress = new Uri(jwksuri);
                        client.DefaultRequestHeaders.Accept.Clear();
                        client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
                        //GET Method
                        response = client.GetAsync(jwksuri, CancellationToken.None).Result;
                        if (!response.IsSuccessStatusCode)
                        {
                            Console.WriteLine("Internal server Error");
                        }
                    }

                    var jsoncontent = response.Content;
                    Dictionary<string, string> dictval;
                    var data = jsoncontent.ReadAsStringAsync().Result;
                    //.ToString();
                    var jwk = JsonWebKeySet.Create(data);
                    var seckeys = new List<SecurityKey>();
                    seckeys = GetSecurityKeys(jwk);

                    TokenValidationParameters validationParameters = new TokenValidationParameters
                    {
                        ValidateIssuer = true,
                        ValidateAudience = true,
                        ValidateLifetime = false,
                        ValidateIssuerSigningKey = true,
                        //this should be the clientid
                        ValidIssuer = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InJlZ2lzdHJhdGlvbi10b2tlbiJ9.eyJqd2tzX3VybCI6Imh0dHA6Ly8xMC4xNS4yNTIuNzMvaW5mZXJuby8ud2VsbC1rbm93bi9qd2tzLmpzb24iLCJhY2Nlc3NUb2tlbnNFeHBpcmVJbiI6MTUsImlhdCI6MTU5NzQxMzE5NX0.q4v4Msc74kN506KTZ0q_minyapJw0gwlT6M_uiL73S4",
                        //this hsould be our end point for the 
                        ValidAudience = "https://localost:7048/bulk-token",
                        IssuerSigningKeys = seckeys,
                        CryptoProviderFactory = new CryptoProviderFactory()
                        {
                            CacheSignatureProviders = false
                        }
                    };
                    try
                    {
                        handler = new JwtSecurityTokenHandler();
                        handler.ValidateToken(tokenStr, validationParameters, out var validatedSecurityToken);
                        var jwtSecurityToken = handler.ReadJwtToken(tokenStr);
                        var TokenInfo = new Dictionary<string, string>();

                        var claims = jwtSecurityToken.Claims.ToList();

                        foreach (var claim in claims)
                        {
                            TokenInfo.Add(claim.Type, claim.Value);
                            Console.WriteLine(claim.Type + " : " + claim.Value);
                        }
                    }
                    catch (SecurityTokenInvalidIssuerException issex)
                    {
                        Console.WriteLine(issex.Message.ToString());
                        Console.WriteLine("Issuer error");
                    }
                    catch (Exception ex)
                    {
                        //switch 
                        //    case when ex.Message.Contains("IDX10516")
                        //        : Console.WriteLine(ex.Message.ToString());
                        //default:
                        Console.WriteLine(ex.Message.ToString());

                        Console.WriteLine("sign error");
                    }

                    return default;
                }
             }

            //if client assetion, get the payolad and get the aud or sub for the client_id
            //set the client_id
            //continue to next step

            // Reject grant_type=client_credentials requests missing the client credentials.
            // See https://tools.ietf.org/html/rfc6749#section-4.4.1 for more information.
            if (context.Request.IsClientCredentialsGrantType() && (string.IsNullOrEmpty(context.Request.ClientId) ||
                                                                   string.IsNullOrEmpty(context.Request.ClientSecret)))
            {
                context.Reject(
                    error: Errors.InvalidRequest,
                    description: OpenIddict.Abstractions.OpenIddictResources.FormatID2057(Parameters.ClientId, Parameters.ClientSecret),
                    uri: OpenIddict.Abstractions.OpenIddictResources.FormatID8000(OpenIddict.Abstractions.OpenIddictResources.ID2057));

                return default;
            }
            return default;
        }
        public static byte[] FromBase64Url(string base64Url)
        {
            string padded = base64Url.Length % 4 == 0
                ? base64Url : base64Url + "====".Substring(base64Url.Length % 4);
            string base64 = padded.Replace("_", "/")
                                  .Replace("-", "+");
            return Convert.FromBase64String(base64);
        }
        private static Dictionary<string, string> GetTokenInfo(string token)
        {
            var TokenInfo = new Dictionary<string, string>();
            
            var handler = new JwtSecurityTokenHandler();
           
            //check for valid token true or false
            if (handler.CanReadToken(token))
            {
                var jwtSecurityToken = handler.ReadJwtToken(token);

                var claims = jwtSecurityToken.Claims.ToList();
               
                foreach (var claim in claims)
                {
                    TokenInfo.Add(claim.Type, claim.Value);
                }

            }
            return TokenInfo;
        }
        private List<SecurityKey> GetSecurityKeys(JsonWebKeySet jsonWebKeySet)
        {
            var keys = new List<SecurityKey>();

            foreach (var key in jsonWebKeySet.Keys)
            {
                if (key.Kty == "EC")
                {
                    ECDsa eckey = ECDsa.Create(new ECParameters
                    {
                        Curve = ECCurve.NamedCurves.nistP384,
                        Q = new ECPoint
                        {
                            X = FromBase64Url(key.X),
                            Y = FromBase64Url(key.Y)
                        }

                    });
                    var eckeys = new ECDsaSecurityKey(eckey)
                    {
                        KeyId = key.Kid
                    };

                    keys.Add(eckeys);
                }
                else if (key.Kty == "RSA")
                {
                    var e = FromBase64Url(key.E);
                    var n = FromBase64Url(key.N);

                    var rsakey = new RsaSecurityKey(new RSAParameters { Exponent = e, Modulus = n })
                    {
                        KeyId = key.Kid
                    };

                    keys.Add(rsakey);
                }
                else
                {
                    throw new NotImplementedException("Unknown key type for token validation");
                }
            }

            return keys;
        }

    }


    /// <summary>
    /// Contains the logic responsible for rejecting token requests that don't specify a client identifier.
    /// </summary>
    public class MyValidateClientIdParameter :  IOpenIddictServerHandler<ValidateTokenRequestContext>
    {
        /// <summary>
        /// Gets the default descriptor definition assigned to this handler.
        /// </summary>
        public static OpenIddictServerHandlerDescriptor Descriptor { get; }
            = OpenIddictServerHandlerDescriptor.CreateBuilder<ValidateTokenRequestContext>()
                .UseSingletonHandler<MyValidateClientIdParameter>()
                .SetOrder(ValidateGrantType.Descriptor.Order + 1_000)
                .SetType(OpenIddictServerHandlerType.Custom)
                .Build();

        /// <inheritdoc/>
        public ValueTask HandleAsync(ValidateTokenRequestContext context)
        {
            Console.WriteLine("In custom validate clientid");
            if (context is null)
            {
                throw new ArgumentNullException(nameof(context));
            }

            //vsi added flow to allow client assertion and grant type client_credentials
            if (context.Request.IsClientCredentialsGrantType() && (!string.IsNullOrEmpty(context.Request.ClientAssertion) && !string.IsNullOrEmpty(context.Request.ClientAssertionType)))
            {
                return default;
            }

            if (!string.IsNullOrEmpty(context.ClientId))
            {
                return default;
            }

            // At this stage, reject the token request unless the client identification requirement was disabled.
            // Independently of this setting, also reject grant_type=authorization_code requests that don't specify
            // a client_id, as the client identifier MUST be sent by the client application in the request body
            // if it cannot be inferred from the client authentication method (e.g the username when using basic).
            // See https://tools.ietf.org/html/rfc6749#section-4.1.3 for more information.
            if (!context.Options.AcceptAnonymousClients || context.Request.IsAuthorizationCodeGrantType())
            {
                context.Logger.LogInformation(OpenIddict.Abstractions.OpenIddictResources.GetResourceString(OpenIddict.Abstractions.OpenIddictResources.ID6077), Parameters.ClientId);

                context.Reject(
                    error: Errors.InvalidClient,
                    description: OpenIddict.Abstractions.OpenIddictResources.FormatID2029(Parameters.ClientId),
                    uri: OpenIddict.Abstractions.OpenIddictResources.FormatID8000(OpenIddict.Abstractions.OpenIddictResources.ID2029));

                return default;
            }

            return default;
        }
    }

}

}

svasui123 avatar Jun 16 '22 21:06 svasui123

Hi Kevin,

Is there a new version with the private_key_jwt included?

Best Regards

Vasu

From: Kévin Chalet @.> Sent: Monday, August 22, 2022 6:47 PM To: openiddict/openiddict-samples @.> Cc: svasui123 @.>; Author @.> Subject: Re: [openiddict/openiddict-samples] Any samples for grant type client credentials and client assertions? (Issue #193)

Closed #193 https://github.com/openiddict/openiddict-samples/issues/193 as completed.

— Reply to this email directly, view it on GitHub https://github.com/openiddict/openiddict-samples/issues/193#event-7236846555 , or unsubscribe https://github.com/notifications/unsubscribe-auth/AZDMOETISCTRXYUT2QEPJE3V2P7PVANCNFSM5XCFPXCA . You are receiving this because you authored the thread. https://github.com/notifications/beacon/AZDMOETMBLMWZY7DB3JDUG3V2P7PVA5CNFSM5XCFPXCKYY3PNVWWK3TUL52HS4DFWZEXG43VMVCXMZLOORHG65DJMZUWGYLUNFXW5KTDN5WW2ZLOORPWSZGPAAAAAANPLGB5W.gif Message ID: @.*** @.***> >

svasui123 avatar Aug 23 '22 17:08 svasui123

Hey Vasu,

Support for client authentication assertions in the server stack is tracked by https://github.com/openiddict/openiddict-core/issues/1251. 4.0 (whose 3rd preview shipped yesterday) will mostly focus on the whole new client stack so it's likely something that will need to wait a bit (maybe 5.0 or 6.0?).

Cheers.

kevinchalet avatar Aug 23 '22 17:08 kevinchalet

For the record, client assertions support is part of the 5.0 preview1 release that shipped today: https://kevinchalet.com/2023/10/20/introducing-native-applications-per-client-token-lifetimes-and-client-assertions-support-in-openiddict-5-0-preview1/

kevinchalet avatar Oct 20 '23 19:10 kevinchalet

Thank you. We will check this.

From: Kévin Chalet @.> Sent: Friday, October 20, 2023 3:59 PM To: openiddict/openiddict-samples @.> Cc: svasui123 @.>; Author @.> Subject: Re: [openiddict/openiddict-samples] Any samples for grant type client credentials and client assertions? (Issue #193)

For the record, client assertions support is part of the 5.0 preview1 release that shipped today: https://kevinchalet.com/2023/10/20/introducing-native-applications-per-client-token-lifetimes-and-client-assertions-support-in-openiddict-5-0-preview1/

— Reply to this email directly, view it on GitHub https://github.com/openiddict/openiddict-samples/issues/193#issuecomment-1773322749 , or unsubscribe https://github.com/notifications/unsubscribe-auth/AZDMOERH4TZW3UD3HMNPRN3YALJZPAVCNFSM5XCFPXCKU5DIOJSWCZC7NNSXTN2JONZXKZKDN5WW2ZLOOQ5TCNZXGMZTEMRXGQ4Q . You are receiving this because you authored the thread. https://github.com/notifications/beacon/AZDMOEX63XQOO442IXKR26TYALJZPA5CNFSM5XCFPXCKYY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGONGZMD7I.gif Message ID: @.***>

svasui123 avatar Oct 24 '23 15:10 svasui123