OpenAPITools / openapi-generator

OpenAPI Generator allows generation of API client libraries (SDK generation), server stubs, documentation and configuration automatically given an OpenAPI Spec (v2, v3)
https://openapi-generator.tech
Apache License 2.0
21.35k stars 6.46k forks source link

How to set the use of "credentials" with csharp or csharp-dotnet2 client? #187

Open JFCote opened 6 years ago

JFCote commented 6 years ago
Description

This is not a bug, it is only a question.

I'm a common user of different typescript clients like jQuery and Fetch. In those clients, when I create the client, I can specify via the options that I want to use the credentials in the session. A typical use case is that there is a function called "login" in the api and after that, if in the session we have the credentials, keep them and you have now access to other calls.

For example, in typescript-jquery:

this.brandApi = new BrandApi(path, null, {
            xhrFields: {"withCredentials": true}
        });

Or in typescript-fetch, I pass this in the options:

{credentials: "include"}

How are we supposed to do that in the csharp and csharp-dotnet2 client generators?

I'm tagging c# technical committee. This could be added in a FAQ somewhere too :) @mandrean (2017/08) @jimschubert (2017/09)

jimschubert commented 6 years ago

This is an interesting question. So I'll explain it as best as I can.

The csharp and csharp-dotnet2 generators were written against the OpenAPI 2.0 spec, which doesn't specifically call out any cookie support (3.x spec now offers parameter object in: cookie). They do, however, expose the underlying client implementation which gives you access to headers/cookies/etc directly from any given request.

One argument against cookies in a REST API is that they quietly apply state to something that's generally meant to be stateless. I could go either way on that argument… if you're using a cookie for passing some token around with some expiration time then I don't really see how that's any different from something like a JWT token or another type of Authorization header value. Then again, if you're using a cookie to track details about the last call a client made then that can become a problem. I'd personally stay away from cookies, but I understand that this isn't always possible in the real world when you have an API to consume and that API forces you to use cookies.

Anyway, I've created a working client/server example for you to check out: https://github.com/jimschubert/cookies-issue-187

I've generated this from OAI's petstore-minimal.json OpenAPI v2.0 example:

openapi-generator-cli -g aspnetcore \
    -i https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/examples/v2.0/json/petstore-minimal.json \
    -o server

openapi-generator-cli generate -g csharp \
    -i https://raw.githubusercontent.com/OAI/OpenAPI-Specification/master/examples/v2.0/json/petstore-minimal.json \
    -o client

The single GET /api/pets endpoint creates a new cookie on a header if you're a new requestor:

$ curl -v http://localhost:5000/api/pets
*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 5000 (#0)
> GET /api/pets HTTP/1.1
> Host: localhost:5000
> User-Agent: curl/7.54.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Date: Fri, 01 Jun 2018 02:29:46 GMT
< Content-Type: application/json; charset=utf-8
< Server: Kestrel
< Transfer-Encoding: chunked
< Set-Cookie: session-id=ce9ec4a398e246eba17dcfb713f89302; path=/; samesite=lax; httponly
<
* Connection #0 to host localhost left intact
[{"id":1,"name":"Jim","tag":"Person"},{"id":2,"name":"Socks","tag":"Cat"},{"id":3,"name":"Fido","tag":"Dog"},{"id":4,"name":"Pookie","tag":"Snake"}]

And if you tell it you have a cookie already, it'll set that same cookie on the response:

curl --cookie 'session-id=fc1467d9d76144c4beeb49646d7cc76a' -v http://localhost:5000/api/pets
*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 5000 (#0)
> GET /api/pets HTTP/1.1
> Host: localhost:5000
> User-Agent: curl/7.54.0
> Accept: */*
> Cookie: session-id=fc1467d9d76144c4beeb49646d7cc76a
>
< HTTP/1.1 200 OK
< Date: Fri, 01 Jun 2018 02:30:39 GMT
< Content-Type: application/json; charset=utf-8
< Server: Kestrel
< Transfer-Encoding: chunked
< Set-Cookie: session-id=fc1467d9d76144c4beeb49646d7cc76a; path=/; samesite=lax; httponly
<
* Connection #0 to host localhost left intact
[{"id":1,"name":"Jim","tag":"Person"},{"id":2,"name":"Socks","tag":"Cat"},{"id":3,"name":"Fido","tag":"Dog"},{"id":4,"name":"Pookie","tag":"Snake"}]

The client then implements an extension on the ApiClient partial class which does similar work (set cookie if it exists, log cookie state on request/response).

using System;
using System.Linq;
using RestSharp;

namespace Org.OpenAPITools.Client
{
    public partial class ApiClient
    {
        private static String CookieKey = "session-id";
        private String cookie = null;
        partial void InterceptRequest(IRestRequest request)
        {
            if (cookie == null) return;
            Console.WriteLine("Add cookie to request: " + cookie);
            request.AddCookie(CookieKey, cookie);
        }

        partial void InterceptResponse(IRestRequest request, IRestResponse response)
        {
            var setCookie = response.Cookies.First(c => c.Name == CookieKey);
            if (setCookie != null)
            {
                cookie = setCookie.Value;
                Console.WriteLine("Cookie on response: " + cookie);
            }
        }
    }
}

You can use this as a starting point for tracking cookies between requests. For full details on running the two projects, check out its README.

I didn't go too in depth in the README about how this differs from what you do in xhr/fetch from the browser side. Those 'credentials' options are a little different from a C# client because they're reading/writing from their cookie persistence (the browser or in-memory cookie jar) one thread at a time. You can do "multi-threading" in JavaScript using web workers or service workers, but these have limited access to certain browser storage (can't access DOM/window/document for example). In a multi-threaded client, it can sometimes be tricky to get thread-safety right (as in, right for most use cases). If we were to do this using a thread-safe container, for example, this would add overhead for clients that may run on commodity hardware. Then, there's the issue of getting thread-safe implementations wrong and causing deadlocks… that's obviously not ideal.

So we generally fall back to two solutions: the client code is open for extension using partial methods. This means that you can create your own file implementing the partial class to extend, then provide your own implementation of a partial method. You can then use the ignore file to prevent your extension file from being overwritten if there's concern that the name may be reused by client code. Now, you can regenerate client code as-needed without removing your extensions (such as cookie support). The second solution is that we provide for the client templates to be fully customized. This is a larger effort from a consumer's perspective, but may offer the biggest bang. A consumer may have an in-house built REST client that supports cookies in a request/response flow, for which they'd like to generate code from an OpenAPI spec. It might take an hour or two to switch the existing C# template over to a customized one, but it's fully worth it in this case.

I hope that answers your question and provides a good example for moving forward with your implementation.

JFCote commented 6 years ago

Thanks @jimschubert

Wow, I didn't expect an answer so good! I will definitely check out everything you say in this answer and see what is the best way in our use case.

Thanks a lot!

jimschubert commented 6 years ago

No problem! I figured that I'd answer as thoroughly as possible, since others probably have similar questions about this use case.

JFCote commented 6 years ago

@jimschubert Should this question be moved to the wiki since it might help others? If we let it open in the issue list, it will stay there forever.

jimschubert commented 6 years ago

I'm fine with moving it to a wiki/tutorial space. I probably won't have the time to create that document for a few weeks, though.