{-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE DeriveDataTypeable #-} --------------------------------------------------------- -- -- Module : Yesod.Helpers.Auth -- Copyright : Michael Snoyman -- License : BSD3 -- -- Maintainer : Michael Snoyman -- Stability : Stable -- Portability : portable -- -- Authentication through the authentication package. -- --------------------------------------------------------- module Yesod.Helpers.Auth ( authHandler , YesodAuth (..) , maybeIdentifier , authIdentifier , displayName ) where import Web.Encodings import qualified Web.Authenticate.Rpxnow as Rpxnow import qualified Web.Authenticate.OpenId as OpenId import Yesod import Control.Monad.Attempt import qualified Data.ByteString.Char8 as B8 import Data.Maybe (fromMaybe) import qualified Network.Wai import Data.Typeable (Typeable) import Control.Exception (Exception, SomeException (..)) class YesodApproot a => YesodAuth a where -- | The following breaks DRY, but I cannot think of a better solution -- right now. -- -- The root relative to the application root. Should not begin with a slash -- and should end with one. authRoot :: a -> String authRoot _ = "auth/" -- | Absolute path to the default login path. defaultLoginPath :: a -> String defaultLoginPath a = approot a ++ authRoot a ++ "openid/" rpxnowApiKey :: a -> Maybe String rpxnowApiKey _ = Nothing onRpxnowLogin :: Rpxnow.Identifier -> Handler a () onRpxnowLogin _ = return () getFullAuthRoot :: YesodAuth y => Handler y String getFullAuthRoot = do y <- getYesod ar <- getApproot return $ ar ++ authRoot y data AuthResource = Check | Logout | Openid | OpenidForward | OpenidComplete | LoginRpxnow deriving (Show, Eq, Enum, Bounded) rc :: HasReps x => Handler y x -> Handler y ChooseRep rc = fmap chooseRep authHandler :: YesodAuth y => Verb -> [String] -> Handler y ChooseRep authHandler Get ["check"] = rc authCheck authHandler Get ["logout"] = rc authLogout authHandler Get ["openid"] = rc authOpenidForm authHandler Get ["openid", "forward"] = rc authOpenidForward authHandler Get ["openid", "complete"] = rc authOpenidComplete -- two different versions of RPX protocol apparently, so just accepting all -- verbs authHandler _ ["login", "rpxnow"] = rc rpxnowLogin authHandler _ _ = notFound data OIDFormReq = OIDFormReq (Maybe String) (Maybe String) instance ConvertSuccess OIDFormReq Html where convertSuccess (OIDFormReq Nothing _) = cs "" convertSuccess (OIDFormReq (Just s) _) = Tag "p" [("class", "message")] $ cs s someParam :: (Monad m, RequestReader m) => ParamType -> (RawRequest -> ParamName -> [ParamValue]) -> ParamName -> m ParamValue someParam pt paramList pn = do rr <- getRawRequest case paramList rr pn of [x] -> return x x -> invalidParams [((pt, pn, x), SomeException ExpectedSingleParam)] data ExpectedSingleParam = ExpectedSingleParam deriving (Show, Typeable) instance Exception ExpectedSingleParam getParam :: (Monad m, RequestReader m) => ParamName -> m ParamValue getParam = someParam GetParam getParams authOpenidForm :: Yesod y => Handler y ChooseRep authOpenidForm = do rr <- getRawRequest case getParams rr "dest" of [] -> return () (x:_) -> addCookie 120 "DEST" x let html = HtmlList [ case getParams rr "message" of [] -> HtmlList [] (m:_) -> Tag "p" [("class", "message")] $ cs m , Tag "form" [("method", "get"), ("action", "forward/")] $ HtmlList [ Tag "label" [("for", "openid")] $ cs "OpenID: " , EmptyTag "input" [("type", "text"), ("id", "openid"), ("name", "openid")] , EmptyTag "input" [("type", "submit"), ("value", "Login")] ] ] applyLayout' "Log in via OpenID" html authOpenidForward :: YesodAuth y => Handler y () authOpenidForward = do oid <- getParam "openid" authroot <- getFullAuthRoot let complete = authroot ++ "/openid/complete/" res <- runAttemptT $ OpenId.getForwardUrl oid complete attempt (\err -> redirect RedirectTemporary $ "/auth/openid/?message=" ++ encodeUrl (show err)) (redirect RedirectTemporary) res authOpenidComplete :: YesodApproot y => Handler y () authOpenidComplete = do ar <- getApproot rr <- getRawRequest let gets' = rawGetParams rr let dest = case cookies rr "DEST" of [] -> ar (x:_) -> x res <- runAttemptT $ OpenId.authenticate gets' let onFailure err = redirect RedirectTemporary $ "/auth/openid/?message=" ++ encodeUrl (show err) let onSuccess (OpenId.Identifier ident) = do deleteCookie "DEST" header authCookieName ident redirect RedirectTemporary dest attempt onFailure onSuccess res rpxnowLogin :: YesodAuth y => Handler y () rpxnowLogin = do ay <- getYesod let ar = approot ay apiKey <- case rpxnowApiKey ay of Just x -> return x Nothing -> notFound rr <- getRawRequest let token = case getParams rr "token" ++ postParams rr "token" of [] -> failure MissingToken (x:_) -> x let dest = case postParams rr "dest" of [] -> case getParams rr "dest" of [] -> ar ("":_) -> ar (('#':rest):_) -> rest (s:_) -> s (d:_) -> d ident <- Rpxnow.authenticate apiKey token onRpxnowLogin ident header authCookieName $ Rpxnow.identifier ident header authDisplayName $ getDisplayName ident redirect RedirectTemporary dest data MissingToken = MissingToken deriving (Show, Typeable) instance Exception MissingToken -- | Get some form of a display name, defaulting to the identifier. getDisplayName :: Rpxnow.Identifier -> String getDisplayName (Rpxnow.Identifier ident extra) = helper choices where choices = ["verifiedEmail", "email", "displayName", "preferredUsername"] helper [] = ident helper (x:xs) = case lookup x extra of Nothing -> helper xs Just y -> y authCheck :: Yesod y => Handler y ChooseRep authCheck = do ident <- maybeIdentifier dn <- displayName applyLayoutJson "Authentication Status" $ cs [ ("identifier", fromMaybe "" ident) , ("displayName", fromMaybe "" dn) ] authLogout :: YesodAuth y => Handler y () authLogout = do deleteCookie authCookieName rr <- getRawRequest ar <- getApproot let dest = case cookies rr "DEST" of [] -> ar (x:_) -> x deleteCookie "DEST" redirect RedirectTemporary dest -- | Gets the identifier for a user if available. maybeIdentifier :: (Functor m, Monad m, RequestReader m) => m (Maybe String) maybeIdentifier = do rr <- getRawRequest return $ fmap cs $ lookup (B8.pack authCookieName) $ rawSession rr -- | Gets the display name for a user if available. displayName :: (Functor m, Monad m, RequestReader m) => m (Maybe String) displayName = do rr <- getRawRequest return $ fmap cs $ lookup (B8.pack authDisplayName) $ rawSession rr -- | Gets the identifier for a user. If user is not logged in, redirects them -- to the login page. authIdentifier :: YesodAuth y => Handler y String authIdentifier = do mi <- maybeIdentifier ar <- getApproot case mi of Nothing -> do rp <- requestPath let dest = ar ++ rp lp <- defaultLoginPath `fmap` getYesod addCookie 120 "DEST" dest redirect RedirectTemporary lp Just x -> return x -- | Determinge the path requested by the user (ie, the path info). This -- includes the query string. requestPath :: (Functor m, Monad m, RequestReader m) => m String requestPath = do env <- parseEnv let q = case B8.unpack $ Network.Wai.queryString env of "" -> "" q'@('?':_) -> q' q' -> '?' : q' return $! dropSlash (B8.unpack $ Network.Wai.pathInfo env) ++ q where dropSlash ('/':x) = x dropSlash x = x