module Network.Gitit.Authentication.Github ( loginGithubUser
, getGithubUser
, GithubCallbackPars
, GithubLoginError
, ghUserMessage
, ghDetails) where
import Network.Gitit.Types
import Network.Gitit.Server
import Network.Gitit.State
import qualified Data.ByteString.Char8 as BS
import qualified Data.ByteString.Lazy as BSL
import Network.HTTP.Conduit
import Network.HTTP.Client.TLS
import Network.OAuth.OAuth2
import Control.Monad (liftM, mplus, mzero)
import Data.Maybe
import Data.Aeson
import Data.Text (Text, pack, unpack)
import Data.Text.Encoding (encodeUtf8)
import Control.Applicative
import Control.Monad.Trans (liftIO)
import Data.UUID (toString)
import Data.UUID.V4 (nextRandom)
import qualified Control.Exception as E
import Prelude
loginGithubUser :: OAuth2 -> Handler
loginGithubUser githubKey = do
state <- liftIO $ fmap toString nextRandom
key <- newSession (sessionDataGithubState state)
cfg <- getConfig
addCookie (MaxAge $ sessionTimeout cfg) (mkCookie "sid" (show key))
let usingOrg = isJust $ org $ githubAuth cfg
let scopes = "user:email" ++ if usingOrg then ",read:org" else ""
let url = authorizationUrl githubKey `appendQueryParam` [("state", BS.pack state), ("scope", BS.pack scopes)]
seeOther (BS.unpack url) $ toResponse ("redirecting to github" :: String)
data GithubLoginError = GithubLoginError { ghUserMessage :: String
, ghDetails :: Maybe String
}
getGithubUser :: GithubConfig
-> GithubCallbackPars
-> String
-> GititServerPart (Either GithubLoginError User)
getGithubUser ghConfig githubCallbackPars githubState =
withManagerSettings tlsManagerSettings getUserInternal
where
getUserInternal mgr =
liftIO $ do
let (Just state) = rState githubCallbackPars
if state == githubState
then do
let (Just code) = rCode githubCallbackPars
ifSuccess
"No access token found yet"
(fetchAccessToken mgr (oAuth2 ghConfig) (sToBS code))
(\at -> ifSuccess
"User Authentication failed"
(userInfo mgr at)
(\githubUser -> ifSuccess
("No email for user " ++ unpack (gLogin githubUser) ++ " returned by Github")
(mailInfo mgr at)
(\githubUserMail -> do
let gitLogin = gLogin githubUser
user <- mkUser (unpack gitLogin)
(unpack $ email $ head githubUserMail)
"none"
let mbOrg = org ghConfig
case mbOrg of
Nothing -> return $ Right user
Just githuborg -> ifSuccess
("Membership check of user " ++ unpack gitLogin ++ " to " ++ unpack githuborg ++ " failed")
(orgInfo gitLogin githuborg mgr at)
(\_ -> return $ Right user))))
else
return $ Left $
GithubLoginError ("The state sent to github is not the same as the state received: " ++ state ++ ", but expected sent state: " ++ githubState)
Nothing
ifSuccess errMsg failableAction successAction = E.catch
(do Right outcome <- failableAction
successAction outcome)
(\exception -> liftIO $ return $ Left $
GithubLoginError errMsg
(Just $ show (exception :: E.SomeException)))
data GithubCallbackPars = GithubCallbackPars { rCode :: Maybe String
, rState :: Maybe String }
deriving Show
instance FromData GithubCallbackPars where
fromData = do
vCode <- liftM Just (look "code") `mplus` return Nothing
vState <- liftM Just (look "state") `mplus` return Nothing
return GithubCallbackPars {rCode = vCode, rState = vState}
userInfo :: Manager -> AccessToken -> IO (OAuth2Result GithubUser)
userInfo mgr token = authGetJSON mgr token "https://api.github.com/user"
mailInfo :: Manager -> AccessToken -> IO (OAuth2Result [GithubUserMail])
mailInfo mgr token = authGetJSON mgr token "https://api.github.com/user/emails"
orgInfo :: Text -> Text -> Manager -> AccessToken -> IO (OAuth2Result BSL.ByteString)
orgInfo gitLogin githubOrg mgr token = do
let url = "https://api.github.com/orgs/" `BS.append` encodeUtf8 githubOrg `BS.append` "/members/" `BS.append` encodeUtf8 gitLogin
authGetBS mgr token url
data GithubUser = GithubUser { gLogin :: Text
} deriving (Show, Eq)
instance FromJSON GithubUser where
parseJSON (Object o) = GithubUser
<$> o .: "login"
parseJSON _ = mzero
data GithubUserMail = GithubUserMail { email :: Text
} deriving (Show, Eq)
instance FromJSON GithubUserMail where
parseJSON (Object o) = GithubUserMail
<$> o .: "email"
parseJSON _ = mzero
sToBS :: String -> BS.ByteString
sToBS = encodeUtf8 . pack