{-# LANGUAGE FlexibleContexts #-} -------------------------------------------------------------------------------- -- | -- Module : Network.OpenID.Authentication -- Copyright : (c) Trevor Elliott, 2008 -- License : BSD3 -- -- Maintainer : Trevor Elliott -- Stability : -- Portability : -- module Network.OpenID.Authentication ( -- * Types CheckIdMode(..) -- * Authentication , authenticationURI , verifyAuthentication ) where -- friends import Codec.Binary.Base64 import Network.OpenID.Association.Manager import Network.OpenID.HTTP import Network.OpenID.Types import Network.OpenID.Utils import qualified Data.Digest.OpenSSL.AlternativeHMAC as HMAC -- libraries import Data.List import MonadLib import Network.HTTP import Network.URI import Numeric import qualified Data.ByteString as BS -------------------------------------------------------------------------------- -- Types data CheckIdMode = Immediate | Setup instance Show CheckIdMode where show Immediate = "checkid_immediate" show Setup = "checkid_setup" instance Read CheckIdMode where readsPrec _ s | "checkid_immediate" `isPrefixOf` s = [(Immediate, drop 17 s)] | "checkid_setup" `isPrefixOf` s = [(Setup, drop 13 s)] | otherwise = [] -------------------------------------------------------------------------------- -- Utilities -- | Get the mac hash type macHash :: AssocType -> HMAC.CryptoHashFunction macHash HmacSha1 = HMAC.sha1 macHash HmacSha256 = HMAC.sha256 -- | Parse a provider within the Result monad parseProvider' :: ExceptionM m Error => String -> m Provider parseProvider' = maybe err return . parseProvider where err = raise (Error "unable to parse openid.op_endpoint") -- | Get the signed fields from a set of parameters getSignedFields :: ExceptionM m Error => [String] -> Params -> m Params getSignedFields ks ps = maybe err return (mapM lkp ks) where err = raise (Error "not all signed parameters present") lkp k = (,) k `fmap` lookup ("openid." ++ k) ps -------------------------------------------------------------------------------- -- Authentication -- | Generate an authentication URL. The params field allows you to -- | specify any extensions, for example, AttributeExchange. authenticationURI :: AssociationManager am => am -- ^ Your pre-established assocations -> CheckIdMode -- ^ Use this if you want to try to -- use OpenID's Immediate mode. Some -- providers won't ever let this -- mode succeed, whereas some won't -- even prompt the user in Setup -- mode and go straight for the -- redirect. It is safe to use the -- Setup mode only. -> Provider -- ^ The OpenID provider's (e.g -- Google or Yahoo) OpenID URI -> Identifier -- ^ The identity URI you are trying -- to verify. Please note that a -- number of providers no longer -- encode their services' usernames -- into the URI. -> ReturnTo -- ^ After the user verifies that -- they are indeed "them" with the -- OpenID provider, where should -- said provider redirect them? -> Maybe Params -- ^ Additional params for OpenID -- extensions. You can use this to -- verify a user's email using -- Attribute Extensions. -> Maybe Realm -> URI authenticationURI am mode prov ident rt mb_exts mb_realm = addParams params (providerURI prov) where params = [ ("openid.ns", openidNS) , ("openid.mode", show mode) , ("openid.claimed_id", getIdentifier ident) , ("openid.identity", getIdentifier ident) , ("openid.return_to", rt) ] ++ ah ++ maybe [] id mb_exts ++ maybe [] (\r -> [("openid.realm", r)]) mb_realm ah = case findAssociation am prov of Nothing -> [] Just a -> [("openid.assoc_handle", assocHandle a)] -- | Verify a signature on a set of params. verifyAuthentication :: (Monad m, AssociationManager am) => am -> Params -> ReturnTo -> Resolver m -> m (Either Error ()) verifyAuthentication am ps rto resolve = runExceptionT $ do u <- lookupParam "openid.return_to" ps unless (u == rto) $ raise $ Error $ "invalid return path: " ++ u prov <- parseProvider' =<< lookupParam "openid.op_endpoint" ps case findAssociation am prov of Nothing -> verifyDirect ps prov resolve Just assoc -> verifyWithAssociation ps assoc -- | Verify an assertion directly verifyDirect :: Monad m => Params -> Provider -> Resolver m -> ExceptionT Error m () verifyDirect ps prov resolve = do let body = formatParams $ ("openid.mode","check_authentication") : filter (\p -> fst p /= "openid.mode") ps ersp <- lift $ resolve Request { rqURI = providerURI prov , rqMethod = POST , rqHeaders = [ Header HdrContentLength $ show $ length body , Header HdrContentType "application/x-www-form-urlencoded" ] , rqBody = body } withResponse ersp $ \rsp -> do let rps = parseDirectResponse (rspBody rsp) case lookup "is_valid" rps of Just "true" -> return () _ -> raise (Error "invalid authentication request") -- | Verify with an association verifyWithAssociation :: Monad m => Params -> Association -> ExceptionT Error m () verifyWithAssociation ps a = do mode <- lookupParam "openid.mode" ps unless (mode == "id_res") $ raise $ Error $ "unexpected openid.mode: " ++ mode sigParams <- breaks (',' ==) `fmap` lookupParam "openid.signed" ps sig <- decode `fmap` lookupParam "openid.sig" ps sps <- getSignedFields sigParams ps let h = macHash (assocType a) msg = map (toEnum . fromEnum) (formatDirectParams sps) mc = HMAC.unsafeHMAC h (BS.pack (assocMacKey a)) (BS.pack msg) sig' = case readHex mc of [(x,"")] -> unroll x _ -> [] unless (sig' == sig) $ raise $ Error "invalid signature"