{-# LANGUAGE OverloadedStrings, ScopedTypeVariables #-}
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 Network.Gitit.Util
import Network.Gitit.Framework
import qualified Data.ByteString.Char8 as BS
import qualified Data.ByteString.Lazy as BSL
import qualified URI.ByteString as URI
import Network.HTTP.Conduit
import Network.OAuth.OAuth2
import Network.OAuth.OAuth2.TokenRequest as OA
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 -> Params -> Handler
loginGithubUser githubKey params = do
state <- liftIO $ fmap toString nextRandom
base' <- getWikiBase
let destination = pDestination params `orIfNull` (base' ++ "/")
key <- newSession $ sessionDataGithubStateUrl state destination
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 = appendQueryParams [("state", BS.pack state), ("scope", BS.pack scopes)] $ authorizationUrl githubKey
seeOther (BS.unpack (URI.serializeURIRef' 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 = liftIO $
newManager 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) (ExchangeToken $ pack code))
(\at -> ifSuccess
"User Authentication failed"
(userInfo mgr (accessToken at))
(\githubUser -> ifSuccess
("No email for user " ++ unpack (gLogin githubUser) ++ " returned by Github")
(mailInfo mgr (accessToken at))
(\githubUserMail -> do
let gitLogin = gLogin githubUser
user <- mkUser (unpack gitLogin)
(unpack $ email $ head (filter primary githubUserMail))
"none"
let mbOrg = org ghConfig
case mbOrg of
Nothing -> return $ Right user
Just githuborg -> ifSuccess
("Membership check failed: the user " ++ unpack gitLogin ++ " is required to be a member of the organization " ++ unpack githuborg ++ ".")
(orgInfo gitLogin githuborg mgr (accessToken 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 OA.Errors GithubUser)
userInfo mgr token = authGetJSON mgr token $ githubUri "/user"
mailInfo :: Manager -> AccessToken -> IO (OAuth2Result OA.Errors [GithubUserMail])
mailInfo mgr token = authGetJSON mgr token $ githubUri "/user/emails"
orgInfo :: Text -> Text -> Manager -> AccessToken -> IO (OAuth2Result OA.Errors BSL.ByteString)
orgInfo gitLogin githubOrg mgr token = do
let url = githubUri $ "/orgs/" `BS.append` encodeUtf8 githubOrg `BS.append` "/members/" `BS.append` encodeUtf8 gitLogin
authGetBS mgr token url
type UriPath = BS.ByteString
githubUri :: UriPath -> URI.URI
githubUri p = URI.URI { URI.uriScheme = URI.Scheme "https"
, URI.uriAuthority = Just $ URI.Authority Nothing (URI.Host "api.github.com") Nothing
, URI.uriPath = p
, URI.uriQuery = URI.Query []
, URI.uriFragment = Nothing }
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
, primary :: Bool
} deriving (Show, Eq)
instance FromJSON GithubUserMail where
parseJSON (Object o) = GithubUserMail
<$> o .: "email"
<*> o .: "primary"
parseJSON _ = mzero