Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Document servant-auth usage #38

Open
3noch opened this issue Aug 8, 2017 · 7 comments
Open

Document servant-auth usage #38

3noch opened this issue Aug 8, 2017 · 7 comments

Comments

@3noch
Copy link
Collaborator

3noch commented Aug 8, 2017

Copied from #36:

The tricky part about servant-auth is that it uses very specific types for the API spec. From the example:

type API =
 "login"
     :> ReqBody '[JSON] Login
     :> PostNoContent '[JSON] (Headers '[ Header "Set-Cookie" SetCookie
                                        , Header "Set-Cookie" SetCookie]
                                       NoContent)

The problem with this is that SetCookie (from cookie) needs some [orphan] instances that are supplied by servant-auth-server in order to work as servant header types. Clearly the client won't have access to these instances because the server library supplies them.

We have some options:

  1. Copy these orphans into our client-only code base and let the server and client share these instances separately. This seems particularly ugly.
  2. Figure out a way to share these instances between both server and client (likely by a change to servant-auth-server). But where would you put these? You could use a newtype over SetCookie to help?
  3. Parameterize your API spec on this type and specify SetCookie on the server and some nominal type (e.g. Text) on the client side. A slight variation of this would be to use CPP.

For option 3, the code could be:

type API setCookie =
 "login"
     :> ReqBody '[JSON] Login
     :> PostNoContent '[JSON] (Headers '[ Header "Set-Cookie" setCookie
                                        , Header "Set-Cookie" setCookie]
                                       NoContent)

-- Server
type ServerAPI = API SetCookie

-- Client
type ClientAPI = API Text

One advantage to option 3 is that the client code doesn't need to add a needless dependency on cookie. This really is a needless dependency because the client won't be benefiting from these cookies (or the types that capture them) in any way. In this case, that's actually the point as these must be HTTP-only cookies as well, which means the client can't use them. And since the SetCookie type is opaque (it says nothing about authentication in particular), there is really no benefit to keeping this information on the client.

A more aggressive option would be to parameterize the API type on the entire Header ... portion of the login endpoint.

I am looking for feedback before I decide to write up docs for this.

I should also mention that to practically use servant-auth in a Reflex-DOM app, some changes are needed to support alternative CSRF options in servant-auth-server. I've started these here: haskell-servant/servant-auth#54

From @imalsogreg:

I like options 2 and 3. Maybe a 4th option would be to send a PR to http-api-data and to standardize the ToHttpApiData and FromHttpApiData instances there? cookie's dependencies are very light, I don't see a problem compiling it with ghcjs.

@3noch
Copy link
Collaborator Author

3noch commented Aug 8, 2017

To do the more aggressive parameterization, one can use this:

type API loginHeaders =
 "login"
     :> ReqBody '[JSON] Login
     :> PostNoContent '[JSON] (loginHeaders $ User)

type family ($) (f :: k1) (x :: k2) = k3

-- Server
data LoginHeaders
type instance LoginHeaders $ x = Headers '[ Header "Set-Cookie" Auth.SetCookie
                                          , Header "Set-Cookie" Auth.SetCookie] x

type ServerAPI = API LoginHeaders

-- Client
data Id
type instance Id $ x = x

type ClientAPI = API Id

@JBetz
Copy link

JBetz commented Jul 7, 2018

Would really appreciate some documentation on this. @3noch, I'm trying to use your example for option 3 and haven't been able to get it working.

API and client definitions:

type API setCookie =
       "login"
         :> ReqBody '[JSON] Login
         :> Post '[JSON] (Headers '[ Header "Set-Cookie" setCookie
                                   , Header "Set-Cookie" setCookie]
                                   User)

data APIClient t m tag = APIClient
  { _login :: Dynamic t (Either Text Login) -> Event t () -> m (Event t (ReqResult () (Headers '[Header "Set-Cookie" Text, Header "Set-Cookie" Text] User)))
  }

apiClient :: forall t m tag. MonadWidget t m => APIClient t m tag
apiClient =
  let login = client layout m tag urlDyn
      layout = Proxy :: Proxy (API Text)
      m = Proxy :: Proxy m
      tag = Proxy :: Proxy ()
      urlDyn = constDyn url
  in APIClient login

Errors with:

Could not deduce Servant.Reflex.CookieAuthNotEnabled
        arising from a use of ‘client’
      from the context: MonadWidget t m
        bound by the type signature for:
                   apiClient :: MonadWidget t m => APIClient t m tag
        at frontend/src/Client/API.hs:39:1-65
      Possible fix:
        add Servant.Reflex.CookieAuthNotEnabled to the context of
          the type signature for:
            apiClient :: MonadWidget t m => APIClient t m tag

The type family approach gave the same result. Am I missing something?

@3noch
Copy link
Collaborator Author

3noch commented Jul 8, 2018

@JBetz servant-auth has changed a bit since I was using it actively, but have you tried the more aggressive parameterization version?

@JBetz
Copy link

JBetz commented Jul 8, 2018

@3noch Yes, and it yields the same error.

@JBetz
Copy link

JBetz commented Jul 9, 2018

Okay, the error went away after I added Cookie to the auths type parameter of my API type. I only had JWT in the list before, so it sounds like servant-reflex is relying on that to determine whether or not the API uses cookies or not, and with only JWT it was somehow ambiguous. That sortof makes sense, though it makes the API type a bit misleading since JWT and Cookie are supposed to represent two distinct forms of authentication, and in this case, it's really just one.

Worth noting in the docs, in any case.

@jappeace
Copy link
Contributor

Servant auth peope have moved the FromHttpData instance to http://hackage.haskell.org/package/http-api-data-0.3.10/docs/Web-HttpApiData.html#t:FromHttpApiData

Currently that doesn't build against reflex platform but it should be resolved eventually.

@danwdart
Copy link

Once logged in then, is there any way to do cross-domain ajax with credentials? Only since it's http-only and not sent with cross-domain requests by default, what flags are we going to need?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants