Hello,

Jason, thanks for the testing and the report!


On Tue, 6 Dec 2016 07:37:04 -0600
Jason Harrer <jazzyeagl...@gmail.com> wrote:

> [...]
> 
> George is already signed up *and* signed in to Snowdrift.coop and goes to
> Discourse for the first time.  George will still see a button at the top
> that says "Log in".  In order for George to post as George, he still needs
> to click the "Log in" button in Discourse.  When he clicks the "Log in"
> button, the SSO logic will happen and George will then be logged in without
> having to enter any additional username or password information.  (yay!)
>  At this point, I'm looking into trying to figure out if there's a way for
> George to simply be logged in to Discourse without ever having to click
> "Log in".

That would be Discourse's responsibility. Basically, it would be possible if
once you load the Discourse instance website in your browser, either it
automatically redirects to SSO login (but that's bad because some users just
want to read the conversation, not log in), or a piece of Javascript somehow
sends a request to Snowdrift asynchronously. Not sure if it can happen without
custom modifications to Snowdrift. I don't know much JS, someone with JS
experience could perhaps know more.

> Sidney is already signed up for Snowdrift.coop, but she is not currently
> logged in to Snowdrift.coop.  Sidney goes to Discourse and sees the same
> screen as George did, with the "Log in" button at the top.  Sidney clicks
> on "Log in", and the sso logic redirects her to the Snowdrift.coop login
> page.  She enters her username and password, and clicks the "Log in" button
> on the Snowdrift.coop auth page, and she is redirected to...  her
> Dashboard.   When Sidney manually goes back to Discourse, she still has a
> "Log in" button on the Discourse page.  When she clicks "Log in" *this* time,
> the sso logic works.
> 
> The Snowdrift login page should have been redirected back to the sso
> handler, which would have then redirect back to Discourse with the
> authentication information (assuming we can keep the GET strings
> transferred throughout the various redirects).  I tried looking at Bryan's
> auth code, but apparently my Haskell skillset is not advanced enough to
> fully follow along.  I'm not sure what to do to make the modification.  If
> anyone can help with this, I would greatly appreciate it.

That's indeed an issue with Bryan's auth code. Yesod has a "final destination"
feature which lets you send the user a web page while specifying where to
redirect after that, and that sent page's handler can use the info to actually
redirect. That feature originally exists for the auth system: If you load a
page that requires login, you're redirected to login with the "final
destination" being the page from which you came. That way, after login you are
redirected back to where you were.

Bryan's code, which replaces YesodAuth, doesn't have that part implemented, or
it's still buggy.

So, Bryan, waiting for your input on this. For now, I suppose it's not super
critical, but it definitely will annoy and confuse users once the Discourse
instance starts getting filled with people and messages (or is it alreday? I
didn't check).

-- fr33

> Thanks!!
> 
> - Jason
> 
> On Mon, Dec 5, 2016 at 4:11 PM, <fr33domlo...@riseup.net> wrote:
> 
> > From: fr33domlover <fr33domlo...@rel4tion.org>
> >
> > ---
> >  website/Snowdrift.cabal          |  8 ++++
> >  website/config/routes            |  2 +
> >  website/config/settings.yml      |  3 ++
> >  website/src/Application.hs       |  1 +
> >  website/src/Discourse.hs         | 86 ++++++++++++++++++++++++++++++
> > ++++++++++
> >  website/src/Handler/Discourse.hs | 49 +++++++++++++++++++++++
> >  website/src/Settings.hs          |  2 +
> >  7 files changed, 151 insertions(+)
> >  create mode 100644 website/src/Discourse.hs
> >  create mode 100644 website/src/Handler/Discourse.hs
> >
> > diff --git a/website/Snowdrift.cabal b/website/Snowdrift.cabal
> > index 9c1664c..8eca05d 100644
> > --- a/website/Snowdrift.cabal
> > +++ b/website/Snowdrift.cabal
> > @@ -40,6 +40,7 @@ library
> >          Avatar
> >          Application
> >          Css
> > +        Discourse
> >          Email
> >          Foundation
> >          Import
> > @@ -49,6 +50,7 @@ library
> >          Settings.StaticFiles
> >          Handler
> >          Handler.Dashboard
> > +        Handler.Discourse
> >          Handler.PaymentInfo
> >          Handler.Pledge
> >          Handler.Project
> > @@ -70,10 +72,12 @@ library
> >          , crowdmatch
> >          -- Other
> >          , aeson                  >= 0.6     && < 0.12
> > +        , base64-bytestring
> >          , blaze-html
> >          , bytestring             >= 0.9     && < 0.11
> >          , classy-prelude         >= 0.10.2
> >          , classy-prelude-yesod   >= 0.10.2
> > +        , cryptonite
> >          , data-default
> >          , errors
> >          , esqueleto
> > @@ -82,8 +86,10 @@ library
> >          , formattable
> >          , hjsmin                 >= 0.1
> >          , http-client
> > +        , http-types
> >          , lens
> >          , libravatar
> > +        , memory
> >          , mime-mail
> >          , monad-logger           >= 0.3     && < 0.4
> >          , nonce
> > @@ -99,6 +105,7 @@ library
> >          , text                   >= 0.11    && < 2.0
> >          , time
> >          , titlecase
> > +        , transformers
> >          , unordered-containers
> >          , wai
> >          , wai-extra              >= 3.0     && < 3.1
> > @@ -122,6 +129,7 @@ library
> >          RecordWildCards
> >          ScopedTypeVariables
> >          TemplateHaskell
> > +        TupleSections
> >          TypeFamilies
> >          ViewPatterns
> >
> > diff --git a/website/config/routes b/website/config/routes
> > index 9e4e079..30fdef8 100644
> > --- a/website/config/routes
> > +++ b/website/config/routes
> > @@ -26,6 +26,8 @@
> >  /p/snowdrift SnowdriftProjectR GET
> >  /pledge/snowdrift PledgeSnowdriftR POST DELETE
> >
> > +/discourse/sso DiscourseR GET
> > +
> >  -- ## Backward compatibility routes
> >
> >  -- Prevents breakage of external links to the old wiki. See
> > diff --git a/website/config/settings.yml b/website/config/settings.yml
> > index 3c30c0f..61ff751 100644
> > --- a/website/config/settings.yml
> > +++ b/website/config/settings.yml
> > @@ -44,3 +44,6 @@ send-email: "_env:SD_EMAILS:false"
> >  # chreekat for assistance.
> >  stripe-secret-key: "_env:STRIPE_SECRET_KEY:"
> >  stripe-publishable-key: "_env:STRIPE_PUBLISHABLE_KEY:"
> > +
> > +# Discourse SSO
> > +discourse-sso-secret: ""
> > diff --git a/website/src/Application.hs b/website/src/Application.hs
> > index 8ccd6b2..761d3f8 100644
> > --- a/website/src/Application.hs
> > +++ b/website/src/Application.hs
> > @@ -35,6 +35,7 @@ import qualified Yesod.GitRev as G
> >
> >  import Handler
> >  import Handler.Dashboard
> > +import Handler.Discourse
> >  import Handler.PaymentInfo
> >  import Handler.Pledge
> >  import Handler.Project
> > diff --git a/website/src/Discourse.hs b/website/src/Discourse.hs
> > new file mode 100644
> > index 0000000..83106bb
> > --- /dev/null
> > +++ b/website/src/Discourse.hs
> > @@ -0,0 +1,86 @@
> > +module Discourse where
> > +
> > +import Prelude
> > +
> > +import Control.Monad.Trans.Except
> > +import Crypto.Hash.Algorithms (SHA256)
> > +import Crypto.MAC.HMAC
> > +import Data.ByteArray.Encoding
> > +import Data.ByteString (ByteString)
> > +import Data.Maybe (catMaybes)
> > +import Data.Text (Text, pack)
> > +import Data.Text.Encoding (encodeUtf8, decodeUtf8')
> > +import Network.HTTP.Types.URI (renderSimpleQuery, parseSimpleQuery)
> > +
> > +import qualified Data.ByteString as B (drop)
> > +import qualified Data.ByteString.Base64 as B64 (decodeLenient)
> > +
> > +import Model
> > +
> > +-- | Information we send back to Discourse once the user logs in through
> > our
> > +-- UI.
> > +data UserInfo = UserInfo
> > +    { ssoEmail     :: Text
> > +    , ssoId        :: UserId
> > +    , ssoUsername  :: Maybe Text
> > +    , ssoFullName  :: Maybe Text
> > +    , ssoAvatarUrl :: Maybe Text
> > +    , ssoBio       :: Maybe Text
> > +    }
> > +
> > +-- | Type restricted convenience wrapper that computes our HMAC.
> > +hmacSHA256 :: ByteString -> ByteString -> HMAC SHA256
> > +hmacSHA256 = hmac
> > +
> > +-- | Given secret known in advance and payload given in the query,
> > compute the
> > +-- HMAC-SHA256, to which Discourse refers as the signature.
> > +generateSig
> > +    :: ByteString -- ^ Secret
> > +    -> ByteString -- ^ Base64 encoded payload
> > +    -> ByteString
> > +generateSig secret payload =
> > +    convertToBase Base16 $ hmacGetDigest $ hmacSHA256 secret payload
> > +
> > +-- | This validates the payloads's authenticity (i.e. make sure it's
> > really our
> > +-- trusted local Discourse instance) by using the signature as a proof
> > that it
> > +-- knows the SSO secret. This is done by verifying that the HMAC-SHA256
> > of the
> > +-- secret and the payload is identical to the signature.
> > +validateSig
> > +    :: ByteString -- ^ SSO secret, same one you specify in Discourse
> > settings
> > +    -> ByteString -- ^ Base64 encoded payload sent by Discourse in the
> > query
> > +    -> ByteString -- ^ Signature sent by Discourse in the query
> > +    -> Bool       -- ^ Whether the computed sig and one passed are
> > identical
> > +validateSig secret payload signature = generateSig secret payload ==
> > signature
> > +
> > +-- | Get the nonce and the return URL from the payload by decoding from
> > Base64
> > +-- and extracting the parameter values.
> > +--
> > +-- We use lenient decoding here because Discourse doesn't seem to add the
> > +-- necessary padding for strictly by-the-spec Base64 encoding.
> > +parsePayload :: ByteString -> Either String (ByteString, Text)
> > +parsePayload b = runExcept $ do
> > +    let params = parseSimpleQuery $ B64.decodeLenient b
> > +    nonce <- maybe (throwE "Nonce is missing") return $ lookup "nonce"
> > params
> > +    burl <- maybe (throwE "URL is missing") return $
> > +        lookup "return_sso_url" params
> > +    let mapLeft f (Left x)  = Left $ f x
> > +        mapLeft _ (Right x) = Right x
> > +    url <- except $ mapLeft show $ decodeUtf8' burl
> > +    return (nonce, url)
> > +
> > +-- | Compute Base64 encoded payload to send back to Discourse after login
> > +userInfoPayload
> > +    :: ByteString -- ^ Raw nonce string we extracted from input payload
> > +    -> UserInfo   -- ^ Info about the user we pass back to Discourse
> > +    -> ByteString
> > +userInfoPayload nonce uinfo =
> > +    let query = catMaybes $ map (\ (name, mval) -> fmap (name,) mval)
> > +            [ ("nonce"      , Just nonce)
> > +            , ("email"      , Just $ encodeUtf8 $ ssoEmail uinfo)
> > +            , ("external_id", Just $ encodeUtf8 $ pack $ show $ ssoId
> > uinfo)
> > +            , ("username"   , fmap encodeUtf8 $ ssoUsername uinfo)
> > +            , ("name"       , fmap encodeUtf8 $ ssoFullName uinfo)
> > +            , ("avatar_url" , fmap encodeUtf8 $ ssoAvatarUrl uinfo)
> > +            , ("bio"        , fmap encodeUtf8 $ ssoBio uinfo)
> > +            ]
> > +    in  convertToBase Base64 $ renderSimpleQuery False query
> > diff --git a/website/src/Handler/Discourse.hs b/website/src/Handler/
> > Discourse.hs
> > new file mode 100644
> > index 0000000..4d69a68
> > --- /dev/null
> > +++ b/website/src/Handler/Discourse.hs
> > @@ -0,0 +1,49 @@
> > +module Handler.Discourse (getDiscourseR) where
> > +
> > +import Import
> > +import Control.Lens
> > +import Control.Monad.Trans.Except
> > +import Avatar
> > +import Discourse
> > +
> > +getDiscourseR :: Handler Html
> > +getDiscourseR = do
> > +    result <- runExceptT $ do
> > +        -- Extract payload param
> > +        mpayload <- lift $ fmap encodeUtf8 <$> lookupGetParam "sso"
> > +        payload <- maybe (throwE "No payload") return mpayload
> > +        -- Extract sig param
> > +        msig <- lift $ fmap encodeUtf8 <$> lookupGetParam "sig"
> > +        sig <- maybe (throwE "No sig") return msig
> > +        -- Get SSO secret from settings
> > +        secret <- lift $ getsYesod $ appDiscourseSsoSecret . appSettings
> > +        -- Verify signature
> > +        unless (validateSig secret payload sig) $ throwE "Signature is
> > invalid"
> > +        -- Extract nonce and return URL from payload
> > +        (nonce, baseUrl) <- case parsePayload payload of
> > +            Left err -> throwE $ "Payload invalid: " <> pack err
> > +            Right p  -> return p
> > +        -- Perform authentication and fetch user info
> > +        Entity uid u <- lift requireAuth
> > +        avatar <-
> > +            lift $ getUserAvatar (StaticR img_default_avatar_png) (Just u)
> > +        let uinfo = UserInfo
> > +                { ssoEmail     = u ^. userEmail
> > +                , ssoId        = uid
> > +                -- TODO no better option right now...
> > +                , ssoUsername  = Nothing
> > +                -- TODO no better option right now...
> > +                , ssoFullName  = Nothing
> > +                , ssoAvatarUrl = Just avatar
> > +                -- TODO could link to Snowdrift user page
> > +                , ssoBio       = Nothing
> > +                }
> > +        -- Compute new payload and sig
> > +            uinfoPayload = userInfoPayload nonce uinfo
> > +            uinfoSig = generateSig secret uinfoPayload
> > +        -- Send them back to Discourse
> > +        let params = [("sso", uinfoPayload), ("sig", uinfoSig)]
> > +        return $ baseUrl <> decodeUtf8 (renderSimpleQuery True params)
> > +    case result of
> > +        Left err  -> invalidArgs [err]
> > +        Right url -> redirect url
> > diff --git a/website/src/Settings.hs b/website/src/Settings.hs
> > index 74254f7..dd6f270 100644
> > --- a/website/src/Settings.hs
> > +++ b/website/src/Settings.hs
> > @@ -51,6 +51,7 @@ data AppSettings = AppSettings
> >      -- ^ Whether to send emails
> >      , appStripeSecretKey        :: StripeKey
> >      , appStripePublishableKey   :: StripeKey
> > +    , appDiscourseSsoSecret     :: ByteString
> >      }
> >
> >  instance FromJSON AppSettings where
> > @@ -76,6 +77,7 @@ instance FromJSON AppSettings where
> >          appSendMail               <- o .:? "send-email"       .!= not
> > runningDevelopment
> >          appStripePublishableKey   <- StripeKey . encodeUtf8 <$> o .:
> > "stripe-publishable-key"
> >          appStripeSecretKey        <- StripeKey . encodeUtf8 <$> o .:
> > "stripe-secret-key"
> > +        appDiscourseSsoSecret     <- encodeUtf8 <$> o .:
> > "discourse-sso-secret"
> >
> >          return AppSettings {..}
> >
> > --
> > 1.9.1
> >
> > _______________________________________________
> > Dev mailing list
> > Dev@lists.snowdrift.coop
> > https://lists.snowdrift.coop/mailman/listinfo/dev
> >  

Attachment: pgpPNgTnXhiK_.pgp
Description: OpenPGP digital signature

_______________________________________________
Dev mailing list
Dev@lists.snowdrift.coop
https://lists.snowdrift.coop/mailman/listinfo/dev

Reply via email to