WIP: Make it general purpose with easy integration#2
WIP: Make it general purpose with easy integration#2eskimor wants to merge 31 commits intoobsidiansystems:masterfrom
Conversation
and made it suitable for both backend routes and frontend routes.
introduced Common module and did some cleanup.
for consistency.
ryantrinkle
left a comment
There was a problem hiding this comment.
I think this package can be considerably simplified. In particular, I think the logic regarding local storage can (and should) be eliminated, with the user providing the means for persistence and/or communication.
backend/Obelisk/OAuth/Backend.hs
Outdated
| module Obelisk.OAuth.Backend | ||
| (getOAuthToken) where | ||
|
|
||
| import Data.Functor.Identity |
There was a problem hiding this comment.
Please don't add spaces here.
backend/Obelisk/OAuth/Backend.hs
Outdated
| import Data.Functor.Sum | ||
| import qualified Data.Text as T | ||
| import qualified Data.Text.Encoding as T | ||
| import Network.HTTP.Client (Request (..), |
There was a problem hiding this comment.
Please don't do 2D formatting in source code
common/Obelisk/OAuth/Config.hs
Outdated
| -> Maybe (r (R OAuthRoute)) | ||
| -> m (OAuthConfigPublic r) | ||
| getOAuthConfigPublic p@(OAuthProvider provider) mr = liftIO $ do | ||
| providerUri <- getTextConfigRequired $ "config/common/oauth/" <> provider <> "/uri" |
There was a problem hiding this comment.
You're using the scheme "config/common/oauth/" <> provider <> "/blah" in a lot of places; if any of those places fail to agree, you will have a bug. You should try to centralize this decision so that it is made in exactly one place.
common/Obelisk/OAuth/Config.hs
Outdated
| providerUri <- getTextConfigRequired $ "config/common/oauth/" <> provider <> "/uri" | ||
| mRedirectBase <- getTextConfig $ "config/common/route" | ||
| mScope <- getTextConfig $ "config/common/oauth" <> provider <> "/scope" | ||
| clientId <- getTextConfigRequired $ "config/common/oauth/" <> provider <> "/client-id" |
There was a problem hiding this comment.
I'm skeptical of the approach of using many separate files; wouldn't it be easier to use a single JSON file? This is really a stylistic decision, but i'm curious if there's a specific reason you prefer this way.
There was a problem hiding this comment.
Non really. I just got the impression this was the current Obelisk way of doing configs. A single JSON file is probably better.
There was a problem hiding this comment.
Makes sense. Let's go with the JSON file approach.
common/Obelisk/OAuth/Config.hs
Outdated
| { _oAuthConfig_responseType = AuthorizationResponseType_Code | ||
| , _oAuthConfig_provider = p | ||
| , _oAuthConfig_providerUri = providerUri | ||
| , _oAuthConfig_redirectUri = (,) <$> mRedirectBase <*> mr |
There was a problem hiding this comment.
Shouldn't these always agree on whether they're Just or Nothing? If one's missing and the other isn't, shouldn't that be a "parse" failure?
There was a problem hiding this comment.
Note: It is (much) better to be strict with parsing this kind of stuff. We want to blow up as soon as possible if the configs are invalid.
There was a problem hiding this comment.
That's a good point. Although there is only one case that should be a parse error: When mr is Just, but mRedirectBase is Nothing (which is unlikely). The other three cases are actually valid. Will fix it nonetheless.
| -- on some value derived from the current time. | ||
| genOAuthState :: MonadIO m => m OAuthState | ||
| genOAuthState = liftIO $ do | ||
| let fastRandom nr = maybe (getEntropy nr) pure =<< getHardwareEntropy nr |
There was a problem hiding this comment.
Why are we getting entropy this way? Why not just getEntropy?
There was a problem hiding this comment.
I just wanted it as random as possible (that way was suggested in the entropy docs) - it should not really matter though.
There was a problem hiding this comment.
That way was suggested in the entropy docs for a specific use case, not as the default. Not everyone trusts the hardware RNG - this was the topic of extensive debate in the Linux kernel developer community. getEntropy will get cryptographic-quality entropy in the standard way for the given system (usually /dev/urandom on linux), so that's what we should generally do. Crypto needs to be kept simple.
There was a problem hiding this comment.
Yeah I know that debate. Sure I will go with just getEntropy!
frontend/ChangeLog.md
Outdated
|
|
||
| ## 0.1.0.0 -- YYYY-mm-dd | ||
|
|
||
| * First version. Released on an unsuspecting world. |
There was a problem hiding this comment.
I think this is generated by cabal; let's just delete the whole file.
frontend/Obelisk/OAuth/Frontend.hs
Outdated
| makeReflexLenses ''OAuthFrontend | ||
|
|
||
|
|
||
| type OAuthConstraints t m = |
There was a problem hiding this comment.
Let's not have "Constraints" in the name of a constraint synonym. (See Joel Spolsky's article on why "Hungarian" notation is bad)
There was a problem hiding this comment.
I don't really see the problem in this case, also what should I name it then?
There was a problem hiding this comment.
HasOAuth or SupportsOAuth or something like that would be good. Just, generally, the name of a thing shouldn't include the type of the thing (or, in this case, the kind of the thing).
There was a problem hiding this comment.
Thanks - I like SupportsOAuth :-)
frontend/Obelisk/OAuth/Frontend.hs
Outdated
| mOldState <- getItemStorage sessionStorage StoreOAuth_State | ||
| let | ||
| onParamsErr = fmapMaybe (fmap (checkState mOldState =<<) . getParams) onRoute | ||
| performEvent_ $ removeItemStorage sessionStorage StoreOAuth_State <$ onParamsErr |
There was a problem hiding this comment.
I don't think it makes sense for this package to worry about how the oauth-related data is persisted. Let's let the caller specify whatever's needed to store this, e.g. in local storage or somewhere else.
There was a problem hiding this comment.
Also, can you explain what's going on here with removing the storage item and such?
There was a problem hiding this comment.
Well, as we redirect the user to github - we need to store the generated state somewhere, so we have it when the user comes back. I picked session storage as it is ideal for this purpose.
I remove it from storage mostly for good measure to ensure any kind of replay attacks are not possible. It should not be necessary as session storage is transient anyway.
Letting the user specify what's needed to store this is most likely overkill, also customization could be added later on when the need arises in a backwards compatible way. (Assuming users don't do a full pattern match on the config of course.
There was a problem hiding this comment.
I believe the risk of the user using the same key in session storage for something else is pretty low, although we should probably add to the documentation what keys are used in session storage by default. - Will do!
There was a problem hiding this comment.
I know I'm late to this, but I have a couple of questions:
a) What if I want to use cookies instead of session storage?
b) Is there a strong reason to not support storing token in cookies?
There was a problem hiding this comment.
Hi @srid!
Where the state token is stored is considered an implementation detail, I don't see why one would care where it is stored (ok prerender ...) - as long as obelisk-oauth works as expected.
Also I am much in favor of implementing #4 which would do away with any kind of storage anyway and would also have a couple of other benefits as well (no page reloads, no lost state, no broken Requester workflow, ...)
Storing the access token (as oppposed to the state, that's just an internal security measure) in cookies should be fine over https. I don't like cookies in general because of their automatic sending semantics, which makes a bunch of attacks possible in the first place and it can be argued whether it is a good idea to send security sensitive information automatically on each request. On the other hand, this is standard practice for a lot of websites, you just need to be careful when you grant access based on that automatically transmitted token in order to prevent CSRF attacks and such.
Storing it in cookies so that you can avoid prerender should be fine.
| {-| Internal module for handling local and session storage. | ||
|
|
||
| TODO: This should really go into some general purpose package and be exposed | ||
| there. |
Without a default implementation this would reduce the usefulness of the package and would also increase the already quite big interface. By providing a default implementation though, this could also be added later on in a backwards compatible way if needed without sacrificing ease of use in the common case. I can also add this right a way if you think it is worthwhile of course. If so, how should the interface look like? I was thinking of a record providing functions for storing and retrieval put in the Config. |
Frontend is now based on Requester which might have been a stupid idea. As sending and receiving Events is a pretty bad abstraction if you need to map responses to requests.
(Does not yet typecheck.)
| , command_getToken | ||
| ) where | ||
|
|
||
| import Control.Monad.Free (Free (..), liftF, MonadFree (..)) |
There was a problem hiding this comment.
What are the pros/cons of using Free versus asking the user to provide a record of functions?
There was a problem hiding this comment.
I'll get back to this, have to get the contract sharing going now.
There was a problem hiding this comment.
@ryantrinkle Sorry for the delay, I really had to get the contract sharing going. I thought using Requester would make sense here and needed a data structure in for Request that allows me to capture some context at request time and transfer it to response time. I realized that a Free monad (as any other monad) allows me to do that.
Especially with things like deriving via I believe a more pleasant API could be provided with a generic monad satisfying some type class like MonadStore/MonadOAuth or something.
Or of course I could also ditch Requester all together and just go with passing in a record of functions and use performEvent.
With deriving via the MonadStore approach could actually be very convenient to use - but probably overkill for this library.
All those approaches are pretty much equivalent, it's a matter of taste and of ergonomics and where we want to go in general with library design. As of now, I would not really go with Free as I don't see any real benefits of this approach.
By the way, I came to the conclusion that Request should almost always be a monad, at least when used in libraries. Would you agree on that or did I get something wrong?
No description provided.