{-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE CPP #-} {-# LANGUAGE PackageImports #-} --------------------------------------------------------- -- | -- Module : Web.Authenticate.OpenId -- Copyright : Michael Snoyman -- License : BSD3 -- -- Maintainer : Michael Snoyman -- Stability : Unstable -- Portability : portable -- -- Provides functionality for being an OpenId consumer. -- --------------------------------------------------------- module Web.Authenticate.OpenId ( Identifier (..) , getForwardUrl , authenticate , AuthenticateException (..) ) where import Network.HTTP.Wget import Text.HTML.TagSoup import Numeric (showHex) #if MIN_VERSION_transformers(0,2,0) import "transformers" Control.Monad.IO.Class #else import "transformers" Control.Monad.Trans #endif import Data.Data import Control.Failure hiding (Error) import Control.Exception import Control.Monad (liftM) -- | An openid identifier (ie, a URL). newtype Identifier = Identifier { identifier :: String } deriving (Eq, Show) data Error v = Error String | Ok v instance Monad Error where return = Ok Error s >>= _ = Error s Ok v >>= f = f v fail s = Error s -- | Returns a URL to forward the user to in order to login. getForwardUrl :: (MonadIO m, Failure WgetException m) => String -- ^ The openid the user provided. -> String -- ^ The URL for this application\'s complete page. -> m String -- ^ URL to send the user to. getForwardUrl openid complete = do bodyIdent <- wget openid [] [] server <- getOpenIdVar "server" bodyIdent let delegate = maybe openid id $ getOpenIdVar "delegate" bodyIdent return $ constructUrl server [ ("openid.mode", "checkid_setup") , ("openid.identity", delegate) , ("openid.return_to", complete) ] getOpenIdVar :: Monad m => String -> String -> m String getOpenIdVar var content = do let tags = parseTags content let secs = sections (~== ("")) tags secs' <- mhead secs secs'' <- mhead secs' return $ fromAttrib "href" secs'' where mhead [] = fail $ "Variable not found: openid." ++ var -- FIXME mhead (x:_) = return x constructUrl :: String -> [(String, String)] -> String constructUrl url [] = url constructUrl url args = url ++ "?" ++ queryString args where queryString [] = error "queryString with empty args cannot happen" queryString [first] = onePair first queryString (first:rest) = onePair first ++ "&" ++ queryString rest onePair (x, y) = urlEncode x ++ "=" ++ urlEncode y -- | Handle a redirect from an OpenID provider and check that the user -- logged in properly. If it was successfully, 'return's the openid. -- Otherwise, 'failure's an explanation. authenticate :: (MonadIO m, Failure WgetException m, Failure AuthenticateException m) => [(String, String)] -> m Identifier authenticate req = do -- FIXME check openid.mode == id_res (not cancel) authUrl <- getAuthUrl req content <- wget authUrl [] [] let isValid = contains "is_valid:true" content if isValid then Identifier `liftM` alookup "openid.identity" req else failure $ AuthenticateException content alookup :: (Failure AuthenticateException m, Monad m) => String -> [(String, String)] -> m String alookup k x = case lookup k x of Just k -> return k Nothing -> failure $ MissingOpenIdParameter k data AuthenticateException = AuthenticateException String | MissingOpenIdParameter String deriving (Show, Typeable) instance Exception AuthenticateException getAuthUrl :: (MonadIO m, Failure AuthenticateException m, Failure WgetException m) => [(String, String)] -> m String getAuthUrl req = do identity <- alookup "openid.identity" req idContent <- wget identity [] [] helper idContent where helper idContent = do server <- getOpenIdVar "server" idContent dargs <- mapM makeArg [ "assoc_handle", "sig", "signed", "identity", "return_to" ] let sargs = [("openid.mode", "check_authentication")] return $ constructUrl server $ dargs ++ sargs makeArg s = do let k = "openid." ++ s v <- alookup k req return (k, v) contains :: String -> String -> Bool contains [] _ = True contains _ [] = False contains needle haystack = begins needle haystack || (contains needle $ tail haystack) begins :: String -> String -> Bool begins [] _ = True begins _ [] = False begins (x:xs) (y:ys) = x == y && begins xs ys urlEncode :: String -> String urlEncode = concatMap urlEncodeChar urlEncodeChar :: Char -> String urlEncodeChar x | safeChar (fromEnum x) = return x | otherwise = '%' : showHex (fromEnum x) "" safeChar :: Int -> Bool safeChar x | x >= fromEnum 'a' && x <= fromEnum 'z' = True | x >= fromEnum 'A' && x <= fromEnum 'Z' = True | x >= fromEnum '0' && x <= fromEnum '9' = True | otherwise = False