openiddict / openiddict-core

Flexible and versatile OAuth 2.0/OpenID Connect stack for .NET
https://openiddict.com/
Apache License 2.0
4.41k stars 515 forks source link

Introducing the OpenIddict client #1387

Closed kevinchalet closed 1 year ago

kevinchalet commented 2 years ago

Discussion for the Introducing the OpenIddict client blog post.

image

snow-jallen commented 2 years ago

So...is this available for use, or not until v4?

kevinchalet commented 2 years ago

It will ship as part of 4.0 but you can use the nightly builds to give it a try.

a-a-k commented 2 years ago

I have to ask you about other existing clients. Could you provide some sort of "versus table" to bring a tad more clarity for such a noob as me is? I read your explanation but didn't understand a lot.

dfreger commented 2 years ago

Do you have all client libraries as nuget packages?

kevinchalet commented 2 years ago

I have to ask you about other existing clients. Could you provide some sort of "versus table" to bring a tad more clarity for such a noob as me is? I read your explanation but didn't understand a lot.

@a-a-k definitely a good idea. It's likely something that will be done as part of the "aspnet-contrib OAuth 2.0 providers vs OpenIddict-based providers" comparison.

Do you have all client libraries as nuget packages?

Not sure what you mean exactly? The OpenIddict client nightly builds can be found on the MyGet repository. Chances are high they'll appear on NuGet in the next few weeks as part of the first release preview.

Bartmax commented 2 years ago

Wow! Awesome work! I'll take a look at soon as I can, especially interested on how this turns out with MAUI and Blazor.

xperiandri commented 2 years ago

Use Uno Platform on .NET 6 (MAUI runtime). MAUI is a dead end of evolution πŸ™‚

kevinchalet commented 2 years ago

@xperiandri wait, MAUI was released yesterday and it's already obsolete? :trollface:

xperiandri commented 2 years ago

Yes, it was obsolete at the idea stage πŸ˜„

codeaphex commented 2 years ago

Awesome I love openiddict, last time I tried to build "my" auth stack , it did burn me out on this topic a bit though. But I'm about to get into it again and I hope I could get some info about the points where I got stuck. Steps I did previously:

  1. Get general info about this whole topic and understand the different protocols and flows
  2. Find out best practices for public native client auth -> Auth Code with PKCE
  3. Find out how to store refresh tokens (somehow safely) on client side -> https://github.com/AzureAD/microsoft-authentication-extensions-for-dotnet I think MS itself uses this for its public apps and its a cross-plattform implementation.
  4. Fiddle with the MSAL client until I got stuck configuring it for non AD OAuth server
  5. Fiddle with IdentityModel client and try to hack the MSAL Token Cache Extension into it
  6. I think my last version was almost working, but I had concerns about the security of my implementation (aka hack)

So with this new client available my plan would be to integrate the token cache with the openiddict client.

  1. Are there security concerns about this I should consider?
  2. Could you provide some guidance on the implementation?
  3. Is there someone more security aware than myself who would take on this task?
kevinchalet commented 2 years ago

@codeaphex that's a very interesting scenario πŸ˜ƒ

As you likely already figured out, the OpenIddict client only handles the "acquisition" part so persisting/caching the resulting tokens is indeed your responsibility.

Currently, only ASP.NET Core and Katana are supported, so in most cases, you'll end up storing the tokens in the user cookies. Or if you use ASP.NET Core Identity, the UserManager.SetAuthenticationTokenAsync() API can also be leveraged to store tokens in the users database (it's up to you to enable column encryption tho').

For web apps, it's not a big deal, but should OpenIddict support mobile and desktop environments (like MAUI? 🀣) in the future, it's indeed a question that will need to be solved.

I'm not familiar enough with ADAL/MSAL's internals to determine whether the cache part could be used without the acquisition one (the MSFT Identity team has never been really interested in making these scenarios possible but we can't really blame them, as they want to use their Azure products) but if you have a prototype, I'd definitely be interested in taking a look.

codeaphex commented 2 years ago

That implementation would be indeed for a MAUI app, maybe its not as dead as it seems to some 😝 (They just got a new/additional Product Manager, can there be multiple at MS?).
And yes support for mobile and desktop apps would be awesome, so much more fun building stuff with XAML and C# than Web tech. I should try UNO and Blazor though.

I just checked my implementation, will clean, update and upload it soon. It's just the OidcClient using the Duende TestServer with TokenCaching enabled on the console sample. The Cache Extension is ofc optimized for the MSAL client, but has an API for storing custom data: https://github.com/AzureAD/microsoft-authentication-extensions-for-dotnet/blob/387cef9a25d24342ea717c239646b6cf7e8f1b80/sample/ManualTestApp/ExampleUsage.cs#L110

kevinchalet commented 2 years ago

And yes support for mobile and desktop apps would be awesome, so much more fun building stuff with XAML and C# than Web tech.

FWIW, I started working on a prototype of an OpenIddict client host for MAUI and... well, I was expecting something easier πŸ˜ƒ

So, at this point, I'm not sure it's worth investing time on that, specially since the demand for an OpenIddict MAUI host seems quite low.

codeaphex commented 2 years ago

There seems to be some work around this topic https://docs.microsoft.com/en-us/dotnet/maui/platform-integration/communication/authentication?tabs=android
Looks like they got their own problems with it, since its not finished or limited for now, but it should be usable with any auth provider and 3rd party client. This should also take care of problems with multiple instances I guess, which looks to be indeed a plattform specific configuration. Maybe its time to look back into it soon, but I'm also wondering if a specific 3rd party client could provide advantages? πŸ˜ƒ

kevinchalet commented 2 years ago

IWebAuthenticator is a very simple stack that will only handle a tiny part of the authorization dance: you must generate and create the authorization request parameters yourself and it won’t handle the token request part for you. And there’s literally no validation (state or tokens) at all.

On WinUI, it acts as a wrapper around WebAuthenticationBroker, that uses its own frame thing instead of using the system browser. If your own server delegates user authentication to an external provider, chances are high some users will refuse to log in as you can’t even see the current URL of the navigated page.

It’s certainly not something I’d use myself 🀣

codeaphex commented 2 years ago

Oh gosh, why this needs to be so complicated... Couldn't there be a like a standard to handle something like that πŸ˜‚

kevinchalet commented 2 years ago

Well, in their defense, IWebAuthenticator is not an OAuth 2.0/OIDC client: it's just meant to handle the browser redirection part and is basically protocol-agnostic πŸ˜„

kevinchalet commented 2 years ago

I worked on a prototype of a MAUI host for the OpenIddict client that doesn't use MAUI's WebAuthenticator default implementation but directly leverages IBrowser - and lifecycle event handlers for the callback part - and I must admit it's surprisingly well integrated! It's actually much better than any other .NET UI I've ever worked on πŸ˜ƒ

The very cool part is that thanks to Project Reunion's AppInstance.GetInstances() APIs, we don't even need to make MAUI WinUI apps single-instanced for the OpenIddict integration to work correctly: it's able to redirect protocol activations to the correct app instance! πŸ˜ƒ

devenv_m8HvfPEd01

You can find the code and a sample here: https://github.com/openiddict/openiddict-core/pull/1480

Sadly, I was only able to test it on Windows/WinUI:

If it's something we want to productize, we'll need to find a way to add the remaining targets (i.e Android, iOS, macOS).

a-a-k commented 2 years ago

Sorry, I read your blog post twice and all this thread, but steel can't realize - may the new client be used for Xamarin forms?

kevinchalet commented 2 years ago

Sorry, I read your blog post twice and all this thread, but steel can't realize - may the new client be used for Xamarin forms?

Not natively as there's no OpenIddict/Xamarin Forms integration. Since Xamarin Forms was superseded by .NET MAUI, it's highly unlikely we'll ever have an official integration for Xamarin Forms.

nesheimroger commented 2 years ago

This might be slightly unrelated, but I have been looking over the code trying to figure out if this is a match for our project that is currently using ASOS and is in need of an upgrade. Looking at the sample code for clients it adds the core components and uses the entity framework stores. The way I look at it this should never be the case since its the servers job to communicate with the database, and the client should not have any knowledge of it what so ever. I noticed the sample client had a different connection string though; could it just be that the sample code is incorrect?

If the sample is correct, and you really need to "AddCore" on the client side, wouldn't it be better to separate the core parts the client and server needs to avoid some of the dependencies?

Also I noticed that stores and managers have functions for creating and updating applications, scopes, authorizations and tokens. I can understand the need for storing tokens and authorizations since its very much a core part of the solution with revocation etc, but for the other stores I believe read/validate-only version would be beneficial so you can offload the management of applications and custom scopes to another service/server that is not as "exposed".

Ideally I would like to see a complete split here allowing you to only have the oidc protocol specifics public, while the management and storage aspects of it can be set up in a secure zone with access to the database. Might be a bit old fashioned that way, but when security is the main concern it seems like the only acceptable way to go. Any thoughts here? Maybe something I missed that makes it possible?

kevinchalet commented 2 years ago

If the sample is correct, and you really need to "AddCore" on the client side, wouldn't it be better to separate the core parts the client and server needs to avoid some of the dependencies?

The sample is correct and the integration of the core managers is a deliberate design choice. By default, the OpenIddict client works in a stateful mode: it creates and persists state tokens the same exact way the server stack creates authorization codes or refresh tokens (with automatic redeeming, replay protection and reference identifiers), so reusing the same token manager instead of having a duplicate lookalike made the most sense. The OpenIddict client is expected to become as feature-rich as the server feature and both are part of the "OpenIddict stack" so having a shared persistence package is likely not unreasonable.

It's also worth noting that you can combine the client and server features in the same project (e.g to implement delegated authentication scenarios). In this case, sharing the same database and abstractions makes things easier as both features share the same tables/databases and you don't need to register things twice.

Also I noticed that stores and managers have functions for creating and updating applications, scopes, authorizations and tokens. I can understand the need for storing tokens and authorizations since its very much a core part of the solution with revocation etc, but for the other stores I believe read/validate-only version would be beneficial so you can offload the management of applications and custom scopes to another service/server that is not as "exposed".

For hybrid scenarios, you can create your own custom stores (just like you'd do with ASP.NET Core Identity).

Ideally I would like to see a complete split here allowing you to only have the oidc protocol specifics public, while the management and storage aspects of it can be set up in a secure zone with access to the database. Might be a bit old fashioned that way, but when security is the main concern it seems like the only acceptable way to go. Any thoughts here? Maybe something I missed that makes it possible?

We already have a story for that: the degraded (or ASOS-like) mode. For more information, visit https://kevinchalet.com/2020/02/18/creating-an-openid-connect-server-proxy-with-openiddict-3-0-s-degraded-mode/.

Shayan-To commented 2 years ago

@kevinchalet I'm reading blog posts and docs, and one thing is bothering me. I cannot pronounce OpenIddict. How should I pronounce it? Is it like open addict but open iddict instead?!

(Sorry, I didn't know where to ask this question, so I ended up posting it here.)

kevinchalet commented 2 years ago

How should I pronounce it? Is it like open addict but open iddict instead?!

Yep, that's the right way to pronounce it. As you likely guessed, the name is a play on words (OpenID + Addicted -> OpenIddict). It was originally intended to be a codename only, but the name was cool so I kept it for the final project name πŸ˜„

shao200 commented 1 year ago

var token = await HttpContext.GetTokenAsync( CookieAuthenticationDefaults.AuthenticationScheme, OpenIdConnectParameterNames.AccessToken); var t = await blogClient.GetBlogsAsync(token, ct); I got the token, but I can't access the API using the method below: public async Task<Blog[]> GetBlogsAsync(string token, CancellationToken ct) { _client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); var responseMessage = await _client.GetAsync("api/v2/blog/list", ct); responseMessage.EnsureSuccessStatusCode(); var stream = await responseMessage.Content.ReadAsStreamAsync(ct); return await JsonSerializer.DeserializeAsync<Blog[]>(stream, _options, ct); } In the zirku project in sample, too, the API is not accessible, depressed...

jaisriramjs commented 1 year ago

Hello Kavin, Can you give samples for asp.net core web api 6 and angular 11+ with angular-auth-oidc-client. can you please share any working source? thanks arun

bolenton commented 1 year ago

@kevinchalet, hey bud thanks for a cool library. I am trying to use the OpenIddictClientService in a .net Maui app with ABP backend. I am able to successfully get a token using _openIdClientService.AuthenticateWithPasswordAsync().. However I have 2 questions,

  1. Is there a way to pass cookies with this approach? (Abp uses __tenant cookies to know what tenant the user belongs to)
  2. When I log in using the OidcClient browser flow, I get AccessToken and RefreshToken. Is it possible to get the refresh token using OpenIddictClientService?
kevinchalet commented 1 year ago

Hey @bolenton,

Is there a way to pass cookies with this approach? (Abp uses __tenant cookies to know what tenant the user belongs to)

You can use the events model to tweak the HttpRequestMessage before it's sent. Here's an example that flows a custom "tenant" parameter as a __tenant cookie:

var (response, principal) = await service.AuthenticateWithPasswordAsync(
    issuer  : new Uri("http://localhost:58779/", UriKind.Absolute),
    username: email,
    password: password,
    parameters: new Dictionary<string, OpenIddictParameter>
    {
        ["tenant"] = "abc"
    });
services.AddOpenIddict()
    .AddClient(options =>
    {
        // ...

        options.AddEventHandler<PrepareTokenRequestContext>(builder => builder.UseInlineHandler(context =>
        {
            var request = context.Transaction.GetHttpRequestMessage() ??
                throw new InvalidOperationException("The HTTP request cannot be retrieved.");

            // Resolve the custom "tenant" parameter set when calling AuthenticateWithPasswordAsync().
            // If it can be found, send it via the Cookie header and remove it from the parameters list.
            var tenant = (string) context.Request["tenant"];
            if (!string.IsNullOrEmpty(tenant))
            {
                request.Headers.Add("Cookie", $"__tenant={tenant}");
                context.Request.RemoveParameter("tenant");
            }

            return default;
        }));
    });

That said, you should check with the ABP folks if there is not a better way to specify the tenant as the token endpoint is a typically API endpoint for which it's highly unusual to use cookies (not to say it's not a standard thing).

When I log in using the OidcClient browser flow, I get AccessToken and RefreshToken. Is it possible to get the refresh token using OpenIddictClientService?

Make sure you request the offline_access scope.

bolenton commented 1 year ago

Sorry, I meant to come back and provide feedback, I figured it out shortly after asking. It's actually interesting cause if I just pass __tenant as a parameter ABP accepts it without me adding the cookie code. So that's actually pretty cool!

Adding the offline_access scope also works like a charm. Thanks so much for your work @kevinchalet. Before finding the OpenIdClientService I was considering removing openIDDict and going back to idServer, since I didn't think all my use cases for mobile could be solved with OpenIdDict, but I'm very happy to report that I think I am good now and my use cases seem to all be covered so far.


Two more questions, I am building a Maui app and I would like to control the UI on the login screen that's the reason I am avoiding using the browser redirect method that was provided by ABP in the base Maui template. I know using the Resource owner password credentials flow is not as secure since we have to expose the username and password to a potentially nefarious client. I am hoping to remedy this by enforcing two-factor auth. Do you think that would be enough to help remedy this issue? Or should I just bite the bullet and redesign the webpage (eww) to look more custom and support the redirect flow?

In the future, if I decide to use other providers like Google and Twitter for auth, will the current browser redirect work for the most part once I provide the auth endpoint or will I need to create some different to support 3rd party auth? Sorry I Hope that question makes sense.