Zaid-Ajaj / Fable.Remoting

Type-safe communication layer (RPC-style) for F# featuring Fable and .NET Apps
https://zaid-ajaj.github.io/Fable.Remoting/
MIT License
273 stars 55 forks source link

Add resolve function for Authorization header #103

Closed mvsmal closed 5 years ago

mvsmal commented 5 years ago

I know that the recommended way for authentication/authorization in Fable.Remoting is to pass a token explicitly as a function argument. However after my talk about Fable.Remoting I got a lot of questions regarding this topic. So I decided that this functionality will be quite useful.

Currently you can add an Authorization header with Remoting.withAuthorizationHeader, but since it happens only once, the header must be constant. I added Remoting.withAuthorizationHeaderResolver function, which expects a function of type unit -> string to resolve the header on each request. This allows to get and apply the token dynamically with an ability to refresh it.

Zaid-Ajaj commented 5 years ago

Hello there Mikhail,

First of all, I want to thank you for the talk! I watched the questions section again, surely I expected some people to be confused about authorization and security but indeed there were just a lot of them and you still had the patience to explain everything.

As for resolving the authorization header on every request, this one was actually one of the "features" I removed from the last major release, along with many others (for example, retry logic) that should really be added by the developer using the library as needed.

Naturally, you will ask: "So how do you implement this with existing API?"

Resolving access token for authorization is itself a request that should be made before calling protocol functions. Let us assume the developer has such resolveAccessToken function:

let resolveAccessToken : unit -> Async<string> = (* implementation *)

Notice that the type unit -> Async<string> is more appropriate than just unit -> string because otherwise the request will have to be blocking. Sometimes, a user needs a client secret and refresh tokens to resolve the access token, in which case the signature will be

let resolveAccessToken : (clientSecret: string) -> (refreshToken: string) -> Async<string> = 
(* implementation *)

Are you starting to see why I removed this feature? there are too many possibilities that are application-specific and it is up to the user to decide.

Now, instead of creating a proxy for the protocol with a constant header for authorization, we will create a proxy factory that returns a new proxy after resolving the access token:

module Server

type IBookStore = { getByTitle: string -> Async<Book list> }

let resolveAccessToken : unit -> Async<string> = (* implementation *)

let createApiFromToken (accessToken: string) : IBookStore = 
  Remoting.createApi()
  |> Remoting.withAuthorizationHeader  accessToken
  |> Remoting.buildProxy<IBookStore>

let createApi() : Async<IBookStore> = 
  async {
      let! accessToken = resolveAccessToken()
      return createApiFromToken accessToken 
  }

Now from application code:

let getBooksByTitle (title: string) = 
  async {
     let! booksApi = Server.createApi()
     return! booksApi.getByTitle title 
  }

Here we are creating the new proxy for the book store API every time we want to use the proxy, which is totally fine, proxy creation is a very cheap operation and it only occurs every once in a while.

So the added complexity is simply one extra cheap function call and you are there. Maybe I should add this to the documentation as well, every time I think I covered everything, I am wrong again :smile:

Lastly, the error on the build servers seem because the drivers do not have executable permissions, you can ignore this for now, I will take a look later when time permits.

mvsmal commented 5 years ago

Thank you for a great explanation, Zaid. After it we can close this PR, it really doesn't make sense. However, as you mentioned, it would be awesome to add this example to the documentation, since it was not that obvious for me at first.

Zaid-Ajaj commented 5 years ago

Yes, I will definitely add more samples of code to the docs. Now that I think of it, it can still be done without an extra function call, something like this would work too I think:

let call (f: IBookStore -> Async<'T>) : Async<'T> = 
  async {
      let! accessToken = resolveAccessToken()
      let bookStore = createApiFromToken accessToken 
      return! f bookStore
  }

Then use it as follows:

let getBooksByTitle (title: string) = 
  async {
     let! books = Server.call (fun books -> books.getByTitle title)
     return books
  }

FP for the win! Just adding the sample code here so that I don't forget about it :smile: I will need to test this too to make sure everything work like one would expect.