Add 'yesod-auth/' from commit 'fe498e3dac01bfc999cad33b90a2b1b397785178'
git-subtree-dir: yesod-auth git-subtree-mainline:a7df7531dcgit-subtree-split:fe498e3dac
This commit is contained in:
commit
cd5ee0fb12
4
yesod-auth/.gitignore
vendored
Normal file
4
yesod-auth/.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
dist
|
||||
*.swp
|
||||
auth2.db3
|
||||
client_session_key.aes
|
||||
25
yesod-auth/LICENSE
Normal file
25
yesod-auth/LICENSE
Normal file
@ -0,0 +1,25 @@
|
||||
The following license covers this documentation, and the source code, except
|
||||
where otherwise indicated.
|
||||
|
||||
Copyright 2010, Michael Snoyman. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS "AS IS" AND ANY EXPRESS OR
|
||||
IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
|
||||
EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
||||
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
|
||||
NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
|
||||
OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||||
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
|
||||
OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
|
||||
ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
0
yesod-auth/README
Normal file
0
yesod-auth/README
Normal file
8
yesod-auth/Setup.lhs
Executable file
8
yesod-auth/Setup.lhs
Executable file
@ -0,0 +1,8 @@
|
||||
#!/usr/bin/env runhaskell
|
||||
|
||||
> module Main where
|
||||
> import Distribution.Simple
|
||||
> import System.Cmd (system)
|
||||
|
||||
> main :: IO ()
|
||||
> main = defaultMain
|
||||
211
yesod-auth/Yesod/Auth.hs
Normal file
211
yesod-auth/Yesod/Auth.hs
Normal file
@ -0,0 +1,211 @@
|
||||
{-# LANGUAGE QuasiQuotes, TypeFamilies, TemplateHaskell #-}
|
||||
{-# LANGUAGE FlexibleContexts #-}
|
||||
{-# LANGUAGE FlexibleInstances #-}
|
||||
{-# LANGUAGE MultiParamTypeClasses #-}
|
||||
{-# LANGUAGE RankNTypes #-}
|
||||
{-# LANGUAGE CPP #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# OPTIONS_GHC -fno-warn-orphans #-}
|
||||
module Yesod.Auth
|
||||
( -- * Subsite
|
||||
Auth
|
||||
, AuthPlugin (..)
|
||||
, AuthRoute (..)
|
||||
, getAuth
|
||||
, YesodAuth (..)
|
||||
-- * Plugin interface
|
||||
, Creds (..)
|
||||
, setCreds
|
||||
-- * User functions
|
||||
, maybeAuthId
|
||||
, maybeAuth
|
||||
, requireAuthId
|
||||
, requireAuth
|
||||
) where
|
||||
|
||||
#include "qq.h"
|
||||
|
||||
import Control.Monad (when)
|
||||
import Control.Monad.Trans.Class (lift)
|
||||
import Control.Monad.Trans.Maybe
|
||||
|
||||
import Data.Aeson
|
||||
import Data.Text.Encoding (decodeUtf8With)
|
||||
import Data.Text.Encoding.Error (lenientDecode)
|
||||
import Data.Text (Text)
|
||||
import qualified Data.Text as T
|
||||
import qualified Data.Map as Map
|
||||
|
||||
import Language.Haskell.TH.Syntax hiding (lift)
|
||||
|
||||
import qualified Network.Wai as W
|
||||
import Text.Hamlet (html)
|
||||
|
||||
import Yesod.Core
|
||||
import Yesod.Persist
|
||||
import Yesod.Json
|
||||
import Yesod.Auth.Message (AuthMessage, defaultMessage)
|
||||
import qualified Yesod.Auth.Message as Msg
|
||||
import Yesod.Form (FormMessage)
|
||||
|
||||
data Auth = Auth
|
||||
|
||||
type Method = Text
|
||||
type Piece = Text
|
||||
|
||||
data AuthPlugin m = AuthPlugin
|
||||
{ apName :: Text
|
||||
, apDispatch :: Method -> [Piece] -> GHandler Auth m ()
|
||||
, apLogin :: forall s. (Route Auth -> Route m) -> GWidget s m ()
|
||||
}
|
||||
|
||||
getAuth :: a -> Auth
|
||||
getAuth = const Auth
|
||||
|
||||
-- | User credentials
|
||||
data Creds m = Creds
|
||||
{ credsPlugin :: Text -- ^ How the user was authenticated
|
||||
, credsIdent :: Text -- ^ Identifier. Exact meaning depends on plugin.
|
||||
, credsExtra :: [(Text, Text)]
|
||||
}
|
||||
|
||||
class (Yesod m, SinglePiece (AuthId m), RenderMessage m FormMessage) => YesodAuth m where
|
||||
type AuthId m
|
||||
|
||||
-- | Default destination on successful login, if no other
|
||||
-- destination exists.
|
||||
loginDest :: m -> Route m
|
||||
|
||||
-- | Default destination on successful logout, if no other
|
||||
-- destination exists.
|
||||
logoutDest :: m -> Route m
|
||||
|
||||
getAuthId :: Creds m -> GHandler s m (Maybe (AuthId m))
|
||||
|
||||
authPlugins :: [AuthPlugin m]
|
||||
|
||||
-- | What to show on the login page.
|
||||
loginHandler :: GHandler Auth m RepHtml
|
||||
loginHandler = defaultLayout $ do
|
||||
setTitleI Msg.LoginTitle
|
||||
tm <- lift getRouteToMaster
|
||||
mapM_ (flip apLogin tm) authPlugins
|
||||
|
||||
renderAuthMessage :: m
|
||||
-> [Text] -- ^ languages
|
||||
-> AuthMessage -> Text
|
||||
renderAuthMessage _ _ = defaultMessage
|
||||
|
||||
mkYesodSub "Auth"
|
||||
[ ClassP ''YesodAuth [VarT $ mkName "master"]
|
||||
]
|
||||
#define STRINGS *Texts
|
||||
[QQ(parseRoutes)|
|
||||
/check CheckR GET
|
||||
/login LoginR GET
|
||||
/logout LogoutR GET POST
|
||||
/page/#Text/STRINGS PluginR
|
||||
|]
|
||||
|
||||
credsKey :: Text
|
||||
credsKey = "_ID"
|
||||
|
||||
-- | FIXME: won't show up till redirect
|
||||
setCreds :: YesodAuth m => Bool -> Creds m -> GHandler s m ()
|
||||
setCreds doRedirects creds = do
|
||||
y <- getYesod
|
||||
maid <- getAuthId creds
|
||||
case maid of
|
||||
Nothing ->
|
||||
when doRedirects $ do
|
||||
case authRoute y of
|
||||
Nothing -> do rh <- defaultLayout $ addHtml [QQ(html)| <h1>Invalid login |]
|
||||
sendResponse rh
|
||||
Just ar -> do setMessageI Msg.InvalidLogin
|
||||
redirect RedirectTemporary ar
|
||||
Just aid -> do
|
||||
setSession credsKey $ toSinglePiece aid
|
||||
when doRedirects $ do
|
||||
setMessageI Msg.NowLoggedIn
|
||||
redirectUltDest RedirectTemporary $ loginDest y
|
||||
|
||||
getCheckR :: YesodAuth m => GHandler Auth m RepHtmlJson
|
||||
getCheckR = do
|
||||
creds <- maybeAuthId
|
||||
defaultLayoutJson (do
|
||||
setTitle "Authentication Status"
|
||||
addHtml $ html' creds) (json' creds)
|
||||
where
|
||||
html' creds =
|
||||
[QQ(html)|
|
||||
<h1>Authentication Status
|
||||
$maybe _ <- creds
|
||||
<p>Logged in.
|
||||
$nothing
|
||||
<p>Not logged in.
|
||||
|]
|
||||
json' creds =
|
||||
Object $ Map.fromList
|
||||
[ (T.pack "logged_in", Bool $ maybe False (const True) creds)
|
||||
]
|
||||
|
||||
getLoginR :: YesodAuth m => GHandler Auth m RepHtml
|
||||
getLoginR = setUltDestReferer >> loginHandler
|
||||
|
||||
getLogoutR :: YesodAuth m => GHandler Auth m ()
|
||||
getLogoutR = setUltDestReferer >> postLogoutR -- FIXME redirect to post
|
||||
|
||||
postLogoutR :: YesodAuth m => GHandler Auth m ()
|
||||
postLogoutR = do
|
||||
y <- getYesod
|
||||
deleteSession credsKey
|
||||
redirectUltDest RedirectTemporary $ logoutDest y
|
||||
|
||||
handlePluginR :: YesodAuth m => Text -> [Text] -> GHandler Auth m ()
|
||||
handlePluginR plugin pieces = do
|
||||
env <- waiRequest
|
||||
let method = decodeUtf8With lenientDecode $ W.requestMethod env
|
||||
case filter (\x -> apName x == plugin) authPlugins of
|
||||
[] -> notFound
|
||||
ap:_ -> apDispatch ap method pieces
|
||||
|
||||
-- | Retrieves user credentials, if user is authenticated.
|
||||
maybeAuthId :: YesodAuth m => GHandler s m (Maybe (AuthId m))
|
||||
maybeAuthId = do
|
||||
ms <- lookupSession credsKey
|
||||
case ms of
|
||||
Nothing -> return Nothing
|
||||
Just s -> return $ fromSinglePiece s
|
||||
|
||||
maybeAuth :: ( YesodAuth m
|
||||
, Key val ~ AuthId m
|
||||
, PersistBackend (YesodDB m (GGHandler s m IO))
|
||||
, PersistEntity val
|
||||
, YesodPersist m
|
||||
) => GHandler s m (Maybe (Key val, val))
|
||||
maybeAuth = runMaybeT $ do
|
||||
aid <- MaybeT $ maybeAuthId
|
||||
a <- MaybeT $ runDB $ get aid
|
||||
return (aid, a)
|
||||
|
||||
requireAuthId :: YesodAuth m => GHandler s m (AuthId m)
|
||||
requireAuthId = maybeAuthId >>= maybe redirectLogin return
|
||||
|
||||
requireAuth :: ( YesodAuth m
|
||||
, Key val ~ AuthId m
|
||||
, PersistBackend (YesodDB m (GGHandler s m IO))
|
||||
, PersistEntity val
|
||||
, YesodPersist m
|
||||
) => GHandler s m (Key val, val)
|
||||
requireAuth = maybeAuth >>= maybe redirectLogin return
|
||||
|
||||
redirectLogin :: Yesod m => GHandler s m a
|
||||
redirectLogin = do
|
||||
y <- getYesod
|
||||
setUltDest'
|
||||
case authRoute y of
|
||||
Just z -> redirect RedirectTemporary z
|
||||
Nothing -> permissionDenied "Please configure authRoute"
|
||||
|
||||
instance YesodAuth m => RenderMessage m AuthMessage where
|
||||
renderMessage = renderAuthMessage
|
||||
48
yesod-auth/Yesod/Auth/BrowserId.hs
Normal file
48
yesod-auth/Yesod/Auth/BrowserId.hs
Normal file
@ -0,0 +1,48 @@
|
||||
{-# LANGUAGE QuasiQuotes #-}
|
||||
{-# LANGUAGE CPP #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
module Yesod.Auth.BrowserId
|
||||
( authBrowserId
|
||||
) where
|
||||
|
||||
import Yesod.Auth
|
||||
import Web.Authenticate.BrowserId
|
||||
import Data.Text (Text)
|
||||
import Yesod.Core
|
||||
import Text.Hamlet (hamlet)
|
||||
import Control.Monad.IO.Class (liftIO)
|
||||
|
||||
#include "qq.h"
|
||||
|
||||
pid :: Text
|
||||
pid = "browserid"
|
||||
|
||||
complete :: AuthRoute
|
||||
complete = PluginR pid []
|
||||
|
||||
authBrowserId :: YesodAuth m
|
||||
=> Text -- ^ audience
|
||||
-> AuthPlugin m
|
||||
authBrowserId audience = AuthPlugin
|
||||
{ apName = pid
|
||||
, apDispatch = \m ps ->
|
||||
case (m, ps) of
|
||||
("GET", [assertion]) -> do
|
||||
memail <- liftIO $ checkAssertion audience assertion
|
||||
case memail of
|
||||
Nothing -> error "Invalid assertion"
|
||||
Just email -> setCreds True Creds
|
||||
{ credsPlugin = pid
|
||||
, credsIdent = email
|
||||
, credsExtra = []
|
||||
}
|
||||
(_, []) -> badMethod
|
||||
_ -> notFound
|
||||
, apLogin = \toMaster -> do
|
||||
addScriptRemote browserIdJs
|
||||
addHamlet [QQ(hamlet)|
|
||||
<p>
|
||||
<a href="javascript:navigator.id.getVerifiedEmail(function(a){if(a)document.location='@{toMaster complete}/'+a});">
|
||||
<img src="https://browserid.org/i/sign_in_green.png">
|
||||
|]
|
||||
}
|
||||
34
yesod-auth/Yesod/Auth/Dummy.hs
Normal file
34
yesod-auth/Yesod/Auth/Dummy.hs
Normal file
@ -0,0 +1,34 @@
|
||||
{-# LANGUAGE QuasiQuotes #-}
|
||||
{-# LANGUAGE CPP #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
-- | Provides a dummy authentication module that simply lets a user specify
|
||||
-- his/her identifier. This is not intended for real world use, just for
|
||||
-- testing.
|
||||
module Yesod.Auth.Dummy
|
||||
( authDummy
|
||||
) where
|
||||
|
||||
#include "qq.h"
|
||||
|
||||
import Yesod.Auth
|
||||
import Yesod.Form (runInputPost, textField, ireq)
|
||||
import Yesod.Handler (notFound)
|
||||
import Text.Hamlet (hamlet)
|
||||
import Yesod.Widget (addHamlet)
|
||||
|
||||
authDummy :: YesodAuth m => AuthPlugin m
|
||||
authDummy =
|
||||
AuthPlugin "dummy" dispatch login
|
||||
where
|
||||
dispatch "POST" [] = do
|
||||
ident <- runInputPost $ ireq textField "ident"
|
||||
setCreds True $ Creds "dummy" ident []
|
||||
dispatch _ _ = notFound
|
||||
url = PluginR "dummy" []
|
||||
login authToMaster =
|
||||
addHamlet [QQ(hamlet)|
|
||||
<form method="post" action="@{authToMaster url}">
|
||||
\Your new identifier is:
|
||||
<input type="text" name="ident">
|
||||
<input type="submit" value="Dummy Login">
|
||||
|]
|
||||
276
yesod-auth/Yesod/Auth/Email.hs
Normal file
276
yesod-auth/Yesod/Auth/Email.hs
Normal file
@ -0,0 +1,276 @@
|
||||
{-# LANGUAGE QuasiQuotes, TypeFamilies #-}
|
||||
{-# LANGUAGE CPP #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE FlexibleContexts #-}
|
||||
module Yesod.Auth.Email
|
||||
( -- * Plugin
|
||||
authEmail
|
||||
, YesodAuthEmail (..)
|
||||
, EmailCreds (..)
|
||||
, saltPass
|
||||
-- * Routes
|
||||
, loginR
|
||||
, registerR
|
||||
, setpassR
|
||||
) where
|
||||
|
||||
#include "qq.h"
|
||||
|
||||
import Network.Mail.Mime (randomString)
|
||||
import Yesod.Auth
|
||||
import System.Random
|
||||
import Control.Monad (when)
|
||||
import Control.Applicative ((<$>), (<*>))
|
||||
import Data.Digest.Pure.MD5
|
||||
import qualified Data.Text.Lazy as T
|
||||
import qualified Data.Text as TS
|
||||
import Data.Text.Lazy.Encoding (encodeUtf8)
|
||||
import Data.Text (Text)
|
||||
import qualified Crypto.PasswordStore as PS
|
||||
import qualified Data.Text.Encoding as DTE
|
||||
|
||||
import Yesod.Form
|
||||
import Yesod.Handler
|
||||
import Yesod.Content
|
||||
import Yesod.Widget
|
||||
import Yesod.Core
|
||||
import Control.Monad.IO.Class (liftIO)
|
||||
import qualified Yesod.Auth.Message as Msg
|
||||
|
||||
loginR, registerR, setpassR :: AuthRoute
|
||||
loginR = PluginR "email" ["login"]
|
||||
registerR = PluginR "email" ["register"]
|
||||
setpassR = PluginR "email" ["set-password"]
|
||||
|
||||
verify :: Text -> Text -> AuthRoute -- FIXME
|
||||
verify eid verkey = PluginR "email" ["verify", eid, verkey]
|
||||
|
||||
type Email = Text
|
||||
type VerKey = Text
|
||||
type VerUrl = Text
|
||||
type SaltedPass = Text
|
||||
type VerStatus = Bool
|
||||
|
||||
-- | Data stored in a database for each e-mail address.
|
||||
data EmailCreds m = EmailCreds
|
||||
{ emailCredsId :: AuthEmailId m
|
||||
, emailCredsAuthId :: Maybe (AuthId m)
|
||||
, emailCredsStatus :: VerStatus
|
||||
, emailCredsVerkey :: Maybe VerKey
|
||||
}
|
||||
|
||||
class (YesodAuth m, SinglePiece (AuthEmailId m)) => YesodAuthEmail m where
|
||||
type AuthEmailId m
|
||||
|
||||
addUnverified :: Email -> VerKey -> GHandler Auth m (AuthEmailId m)
|
||||
sendVerifyEmail :: Email -> VerKey -> VerUrl -> GHandler Auth m ()
|
||||
getVerifyKey :: AuthEmailId m -> GHandler Auth m (Maybe VerKey)
|
||||
setVerifyKey :: AuthEmailId m -> VerKey -> GHandler Auth m ()
|
||||
verifyAccount :: AuthEmailId m -> GHandler Auth m (Maybe (AuthId m))
|
||||
getPassword :: AuthId m -> GHandler Auth m (Maybe SaltedPass)
|
||||
setPassword :: AuthId m -> SaltedPass -> GHandler Auth m ()
|
||||
getEmailCreds :: Email -> GHandler Auth m (Maybe (EmailCreds m))
|
||||
getEmail :: AuthEmailId m -> GHandler Auth m (Maybe Email)
|
||||
|
||||
-- | Generate a random alphanumeric string.
|
||||
randomKey :: m -> IO Text
|
||||
randomKey _ = do
|
||||
stdgen <- newStdGen
|
||||
return $ TS.pack $ fst $ randomString 10 stdgen
|
||||
|
||||
authEmail :: YesodAuthEmail m => AuthPlugin m
|
||||
authEmail =
|
||||
AuthPlugin "email" dispatch $ \tm ->
|
||||
[QQ(whamlet)|
|
||||
<form method="post" action="@{tm loginR}">
|
||||
<table>
|
||||
<tr>
|
||||
<th>_{Msg.Email}
|
||||
<td>
|
||||
<input type="email" name="email">
|
||||
<tr>
|
||||
<th>_{Msg.Password}
|
||||
<td>
|
||||
<input type="password" name="password">
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<input type="submit" value=_{Msg.LoginViaEmail}>
|
||||
<a href="@{tm registerR}">I don't have an account
|
||||
|]
|
||||
where
|
||||
dispatch "GET" ["register"] = getRegisterR >>= sendResponse
|
||||
dispatch "POST" ["register"] = postRegisterR >>= sendResponse
|
||||
dispatch "GET" ["verify", eid, verkey] =
|
||||
case fromSinglePiece eid of
|
||||
Nothing -> notFound
|
||||
Just eid' -> getVerifyR eid' verkey >>= sendResponse
|
||||
dispatch "POST" ["login"] = postLoginR >>= sendResponse
|
||||
dispatch "GET" ["set-password"] = getPasswordR >>= sendResponse
|
||||
dispatch "POST" ["set-password"] = postPasswordR >>= sendResponse
|
||||
dispatch _ _ = notFound
|
||||
|
||||
getRegisterR :: YesodAuthEmail master => GHandler Auth master RepHtml
|
||||
getRegisterR = do
|
||||
toMaster <- getRouteToMaster
|
||||
defaultLayout $ do
|
||||
setTitleI Msg.RegisterLong
|
||||
addWidget
|
||||
[QQ(whamlet)|
|
||||
<p>_{Msg.EnterEmail}
|
||||
<form method="post" action="@{toMaster registerR}">
|
||||
<label for="email">_{Msg.Email}
|
||||
<input type="email" name="email" width="150">
|
||||
<input type="submit" value=_{Msg.Register}>
|
||||
|]
|
||||
|
||||
postRegisterR :: YesodAuthEmail master => GHandler Auth master RepHtml
|
||||
postRegisterR = do
|
||||
y <- getYesod
|
||||
email <- runInputPost $ ireq emailField "email"
|
||||
mecreds <- getEmailCreds email
|
||||
(lid, verKey) <-
|
||||
case mecreds of
|
||||
Just (EmailCreds lid _ _ (Just key)) -> return (lid, key)
|
||||
Just (EmailCreds lid _ _ Nothing) -> do
|
||||
key <- liftIO $ randomKey y
|
||||
setVerifyKey lid key
|
||||
return (lid, key)
|
||||
Nothing -> do
|
||||
key <- liftIO $ randomKey y
|
||||
lid <- addUnverified email key
|
||||
return (lid, key)
|
||||
render <- getUrlRender
|
||||
tm <- getRouteToMaster
|
||||
let verUrl = render $ tm $ verify (toSinglePiece lid) verKey
|
||||
sendVerifyEmail email verKey verUrl
|
||||
defaultLayout $ do
|
||||
setTitleI Msg.ConfirmationEmailSentTitle
|
||||
addWidget
|
||||
[QQ(whamlet)| <p>_{Msg.ConfirmationEmailSent email} |]
|
||||
|
||||
getVerifyR :: YesodAuthEmail m
|
||||
=> AuthEmailId m -> Text -> GHandler Auth m RepHtml
|
||||
getVerifyR lid key = do
|
||||
realKey <- getVerifyKey lid
|
||||
memail <- getEmail lid
|
||||
case (realKey == Just key, memail) of
|
||||
(True, Just email) -> do
|
||||
muid <- verifyAccount lid
|
||||
case muid of
|
||||
Nothing -> return ()
|
||||
Just _uid -> do
|
||||
setCreds False $ Creds "email" email [("verifiedEmail", email)] -- FIXME uid?
|
||||
toMaster <- getRouteToMaster
|
||||
setMessageI Msg.AddressVerified
|
||||
redirect RedirectTemporary $ toMaster setpassR
|
||||
_ -> return ()
|
||||
defaultLayout $ do
|
||||
setTitleI Msg.InvalidKey
|
||||
addWidget
|
||||
[QQ(whamlet)| <p>_{Msg.InvalidKey} |]
|
||||
|
||||
postLoginR :: YesodAuthEmail master => GHandler Auth master ()
|
||||
postLoginR = do
|
||||
(email, pass) <- runInputPost $ (,)
|
||||
<$> ireq emailField "email"
|
||||
<*> ireq textField "password"
|
||||
mecreds <- getEmailCreds email
|
||||
maid <-
|
||||
case (mecreds >>= emailCredsAuthId, fmap emailCredsStatus mecreds) of
|
||||
(Just aid, Just True) -> do
|
||||
mrealpass <- getPassword aid
|
||||
case mrealpass of
|
||||
Nothing -> return Nothing
|
||||
Just realpass -> return $
|
||||
if isValidPass pass realpass
|
||||
then Just aid
|
||||
else Nothing
|
||||
_ -> return Nothing
|
||||
case maid of
|
||||
Just _aid ->
|
||||
setCreds True $ Creds "email" email [("verifiedEmail", email)] -- FIXME aid?
|
||||
Nothing -> do
|
||||
setMessageI Msg.InvalidEmailPass
|
||||
toMaster <- getRouteToMaster
|
||||
redirect RedirectTemporary $ toMaster LoginR
|
||||
|
||||
getPasswordR :: YesodAuthEmail master => GHandler Auth master RepHtml
|
||||
getPasswordR = do
|
||||
toMaster <- getRouteToMaster
|
||||
maid <- maybeAuthId
|
||||
case maid of
|
||||
Just _ -> return ()
|
||||
Nothing -> do
|
||||
setMessageI Msg.BadSetPass
|
||||
redirect RedirectTemporary $ toMaster loginR
|
||||
defaultLayout $ do
|
||||
setTitleI Msg.SetPassTitle
|
||||
addWidget
|
||||
[QQ(whamlet)|
|
||||
<h3>_{Msg.SetPass}
|
||||
<form method="post" action="@{toMaster setpassR}">
|
||||
<table>
|
||||
<tr>
|
||||
<th>_{Msg.NewPass}
|
||||
<td>
|
||||
<input type="password" name="new">
|
||||
<tr>
|
||||
<th>_{Msg.ConfirmPass}
|
||||
<td>
|
||||
<input type="password" name="confirm">
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<input type="submit" value="_{Msg.SetPassTitle}">
|
||||
|]
|
||||
|
||||
postPasswordR :: YesodAuthEmail master => GHandler Auth master ()
|
||||
postPasswordR = do
|
||||
(new, confirm) <- runInputPost $ (,)
|
||||
<$> ireq textField "new"
|
||||
<*> ireq textField "confirm"
|
||||
toMaster <- getRouteToMaster
|
||||
y <- getYesod
|
||||
when (new /= confirm) $ do
|
||||
setMessageI Msg.PassMismatch
|
||||
redirect RedirectTemporary $ toMaster setpassR
|
||||
maid <- maybeAuthId
|
||||
aid <- case maid of
|
||||
Nothing -> do
|
||||
setMessageI Msg.BadSetPass
|
||||
redirect RedirectTemporary $ toMaster loginR
|
||||
Just aid -> return aid
|
||||
salted <- liftIO $ saltPass new
|
||||
setPassword aid salted
|
||||
setMessageI Msg.PassUpdated
|
||||
redirect RedirectTemporary $ loginDest y
|
||||
|
||||
saltLength :: Int
|
||||
saltLength = 5
|
||||
|
||||
-- | Salt a password with a randomly generated salt.
|
||||
saltPass :: Text -> IO Text
|
||||
saltPass = fmap DTE.decodeUtf8
|
||||
. flip PS.makePassword 12
|
||||
. DTE.encodeUtf8
|
||||
|
||||
saltPass' :: String -> String -> String
|
||||
saltPass' salt pass =
|
||||
salt ++ show (md5 $ fromString $ salt ++ pass)
|
||||
where
|
||||
fromString = encodeUtf8 . T.pack
|
||||
|
||||
isValidPass :: Text -- ^ cleartext password
|
||||
-> SaltedPass -- ^ salted password
|
||||
-> Bool
|
||||
isValidPass ct salted =
|
||||
PS.verifyPassword (DTE.encodeUtf8 ct) (DTE.encodeUtf8 salted) || isValidPass' ct salted
|
||||
|
||||
isValidPass' :: Text -- ^ cleartext password
|
||||
-> SaltedPass -- ^ salted password
|
||||
-> Bool
|
||||
isValidPass' clear' salted' =
|
||||
let salt = take saltLength salted
|
||||
in salted == saltPass' salt clear
|
||||
where
|
||||
clear = TS.unpack clear'
|
||||
salted = TS.unpack salted'
|
||||
78
yesod-auth/Yesod/Auth/Facebook.hs
Normal file
78
yesod-auth/Yesod/Auth/Facebook.hs
Normal file
@ -0,0 +1,78 @@
|
||||
{-# LANGUAGE QuasiQuotes #-}
|
||||
{-# LANGUAGE CPP #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
module Yesod.Auth.Facebook
|
||||
( authFacebook
|
||||
, facebookUrl
|
||||
) where
|
||||
|
||||
#include "qq.h"
|
||||
|
||||
import Yesod.Auth
|
||||
import qualified Web.Authenticate.Facebook as Facebook
|
||||
import Data.Aeson
|
||||
import Data.Aeson.Types (parseMaybe)
|
||||
import Data.Maybe (fromMaybe)
|
||||
|
||||
import Yesod.Form
|
||||
import Yesod.Handler
|
||||
import Yesod.Widget
|
||||
import Control.Monad.IO.Class (liftIO)
|
||||
import Control.Monad.Trans.Class (lift)
|
||||
import Data.Text (Text)
|
||||
import Control.Monad (mzero)
|
||||
import Data.Monoid (mappend)
|
||||
import qualified Data.Aeson.Types
|
||||
import qualified Yesod.Auth.Message as Msg
|
||||
|
||||
facebookUrl :: AuthRoute
|
||||
facebookUrl = PluginR "facebook" ["forward"]
|
||||
|
||||
authFacebook :: YesodAuth m
|
||||
=> Text -- ^ Application ID
|
||||
-> Text -- ^ Application secret
|
||||
-> [Text] -- ^ Requested permissions
|
||||
-> AuthPlugin m
|
||||
authFacebook cid secret perms =
|
||||
AuthPlugin "facebook" dispatch login
|
||||
where
|
||||
url = PluginR "facebook" []
|
||||
dispatch "GET" ["forward"] = do
|
||||
tm <- getRouteToMaster
|
||||
render <- getUrlRender
|
||||
let fb = Facebook.Facebook cid secret $ render $ tm url
|
||||
redirectText RedirectTemporary $ Facebook.getForwardUrl fb perms
|
||||
dispatch "GET" [] = do
|
||||
render <- getUrlRender
|
||||
tm <- getRouteToMaster
|
||||
let fb = Facebook.Facebook cid secret $ render $ tm url
|
||||
code <- runInputGet $ ireq textField "code"
|
||||
at <- liftIO $ Facebook.getAccessToken fb code
|
||||
let Facebook.AccessToken at' = at
|
||||
so <- liftIO $ Facebook.getGraphData at "me"
|
||||
let c = fromMaybe (error "Invalid response from Facebook")
|
||||
$ parseMaybe (parseCreds at') $ either error id so
|
||||
setCreds True c
|
||||
dispatch _ _ = notFound
|
||||
login tm = do
|
||||
render <- lift getUrlRender
|
||||
let fb = Facebook.Facebook cid secret $ render $ tm url
|
||||
let furl = Facebook.getForwardUrl fb $ perms
|
||||
[QQ(whamlet)|
|
||||
<p>
|
||||
<a href="#{furl}">_{Msg.Facebook}
|
||||
|]
|
||||
|
||||
parseCreds :: Text -> Value -> Data.Aeson.Types.Parser (Creds m)
|
||||
parseCreds at' (Object m) = do
|
||||
id' <- m .: "id"
|
||||
let id'' = "http://graph.facebook.com/" `mappend` id'
|
||||
name <- m .: "name"
|
||||
email <- m .: "email"
|
||||
return
|
||||
$ Creds "facebook" id''
|
||||
$ maybe id (\x -> (:) ("verifiedEmail", x)) email
|
||||
$ maybe id (\x -> (:) ("displayName ", x)) name
|
||||
[ ("accessToken", at')
|
||||
]
|
||||
parseCreds _ _ = mzero
|
||||
256
yesod-auth/Yesod/Auth/HashDB.hs
Normal file
256
yesod-auth/Yesod/Auth/HashDB.hs
Normal file
@ -0,0 +1,256 @@
|
||||
{-# LANGUAGE QuasiQuotes #-}
|
||||
{-# LANGUAGE FlexibleContexts #-}
|
||||
{-# LANGUAGE TypeFamilies #-}
|
||||
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
|
||||
{-# LANGUAGE CPP #-}
|
||||
{-# LANGUAGE TemplateHaskell #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE GADTs #-}
|
||||
-------------------------------------------------------------------------------
|
||||
-- |
|
||||
-- Module : Yesod.Auth.HashDB
|
||||
-- Copyright : (c) Patrick Brisbin 2010
|
||||
-- License : as-is
|
||||
--
|
||||
-- Maintainer : pbrisbin@gmail.com
|
||||
-- Stability : Stable
|
||||
-- Portability : Portable
|
||||
--
|
||||
-- A yesod-auth AuthPlugin designed to look users up in Persist where
|
||||
-- their user id's and a salted SHA1 hash of their password is stored.
|
||||
--
|
||||
-- Example usage:
|
||||
--
|
||||
-- > -- import the function
|
||||
-- > import Auth.HashDB
|
||||
-- >
|
||||
-- > -- make sure you have an auth route
|
||||
-- > mkYesodData "MyApp" [$parseRoutes|
|
||||
-- > / RootR GET
|
||||
-- > /auth AuthR Auth getAuth
|
||||
-- > |]
|
||||
-- >
|
||||
-- >
|
||||
-- > -- make your app an instance of YesodAuth using this plugin
|
||||
-- > instance YesodAuth MyApp where
|
||||
-- > type AuthId MyApp = UserId
|
||||
-- >
|
||||
-- > loginDest _ = RootR
|
||||
-- > logoutDest _ = RootR
|
||||
-- > getAuthId = getAuthIdHashDB AuthR (Just . UniqueUser)
|
||||
-- > authPlugins = [authHashDB (Just . UniqueUser)]
|
||||
-- >
|
||||
-- >
|
||||
-- > -- include the migration function in site startup
|
||||
-- > withServer :: (Application -> IO a) -> IO a
|
||||
-- > withServer f = withConnectionPool $ \p -> do
|
||||
-- > runSqlPool (runMigration migrateUsers) p
|
||||
-- > let h = DevSite p
|
||||
--
|
||||
-- Note that function which converts username to unique identifier must be same.
|
||||
--
|
||||
-- Your app must be an instance of YesodPersist. and the username,
|
||||
-- salt and hashed-passwords should be added to the database.
|
||||
--
|
||||
-- > echo -n 'MySaltMyPassword' | sha1sum
|
||||
--
|
||||
-- can be used to get the hash from the commandline.
|
||||
--
|
||||
-------------------------------------------------------------------------------
|
||||
module Yesod.Auth.HashDB
|
||||
( HashDBUser(..)
|
||||
, setPassword
|
||||
-- * Authentification
|
||||
, validateUser
|
||||
, authHashDB
|
||||
, getAuthIdHashDB
|
||||
-- * Predefined data type
|
||||
, User(..)
|
||||
, UserId
|
||||
, migrateUsers
|
||||
) where
|
||||
|
||||
#include "qq.h"
|
||||
|
||||
import Yesod.Persist
|
||||
import Yesod.Handler
|
||||
import Yesod.Form
|
||||
import Yesod.Auth
|
||||
import Yesod.Widget (addHamlet)
|
||||
import Text.Hamlet (hamlet, html)
|
||||
|
||||
import Control.Applicative ((<$>), (<*>))
|
||||
import Control.Monad (replicateM,liftM)
|
||||
import Control.Monad.IO.Class (MonadIO, liftIO)
|
||||
|
||||
import qualified Data.ByteString.Lazy.Char8 as BS (pack)
|
||||
import Data.Digest.Pure.SHA (sha1, showDigest)
|
||||
import Data.Text (Text, pack, unpack, append)
|
||||
import Data.Maybe (fromMaybe)
|
||||
import System.Random (randomRIO)
|
||||
|
||||
|
||||
-- | Interface for data type which holds user info. It's just a
|
||||
-- collection of getters and setters
|
||||
class HashDBUser user where
|
||||
-- | Retrieve password hash from user data
|
||||
userPasswordHash :: user -> Maybe Text
|
||||
-- | Retrieve salt for password
|
||||
userPasswordSalt :: user -> Maybe Text
|
||||
-- | Set hash and password
|
||||
setUserHashAndSalt :: Text -- ^ Salt
|
||||
-> Text -- ^ Password hash
|
||||
-> user -> user
|
||||
|
||||
-- | Generate random salt. Length of 8 is chosen arbitrarily
|
||||
randomSalt :: MonadIO m => m Text
|
||||
randomSalt = pack `liftM` liftIO (replicateM 8 (randomRIO ('0','z')))
|
||||
|
||||
-- | Calculate salted hash using SHA1.
|
||||
saltedHash :: Text -- ^ Salt
|
||||
-> Text -- ^ Password
|
||||
-> Text
|
||||
saltedHash salt =
|
||||
pack . showDigest . sha1 . BS.pack . unpack . append salt
|
||||
|
||||
-- | Set password for user. This function should be used for setting
|
||||
-- passwords. It generates random salt and calculates proper hashes.
|
||||
setPassword :: (MonadIO m, HashDBUser user) => Text -> user -> m user
|
||||
setPassword pwd u = do salt <- randomSalt
|
||||
return $ setUserHashAndSalt salt (saltedHash salt pwd) u
|
||||
|
||||
|
||||
----------------------------------------------------------------
|
||||
-- Authentification
|
||||
----------------------------------------------------------------
|
||||
|
||||
-- | Given a user ID and password in plaintext, validate them against
|
||||
-- the database values.
|
||||
validateUser :: ( YesodPersist yesod
|
||||
, PersistBackend (YesodDB yesod (GGHandler sub yesod IO))
|
||||
, PersistEntity user
|
||||
, HashDBUser user
|
||||
) =>
|
||||
Unique user -- ^ User unique identifier
|
||||
-> Text -- ^ Password in plaint-text
|
||||
-> GHandler sub yesod Bool
|
||||
validateUser userID passwd = do
|
||||
-- Checks that hash and password match
|
||||
let validate u = do hash <- userPasswordHash u
|
||||
salt <- userPasswordSalt u
|
||||
return $ hash == saltedHash salt passwd
|
||||
-- Get user data
|
||||
user <- runDB $ getBy userID
|
||||
return $ fromMaybe False $ validate . snd =<< user
|
||||
|
||||
|
||||
login :: AuthRoute
|
||||
login = PluginR "hashdb" ["login"]
|
||||
|
||||
|
||||
-- | Handle the login form. First parameter is function which maps
|
||||
-- username (whatever it might be) to unique user ID.
|
||||
postLoginR :: ( YesodAuth y, YesodPersist y
|
||||
, HashDBUser user, PersistEntity user
|
||||
, PersistBackend (YesodDB y (GGHandler Auth y IO)))
|
||||
=> (Text -> Maybe (Unique user))
|
||||
-> GHandler Auth y ()
|
||||
postLoginR uniq = do
|
||||
(mu,mp) <- runInputPost $ (,)
|
||||
<$> iopt textField "username"
|
||||
<*> iopt textField "password"
|
||||
|
||||
isValid <- fromMaybe (return False)
|
||||
(validateUser <$> (uniq =<< mu) <*> mp)
|
||||
if isValid
|
||||
then setCreds True $ Creds "hashdb" (fromMaybe "" mu) []
|
||||
else do setMessage [QQ(html)| Invalid username/password |]
|
||||
toMaster <- getRouteToMaster
|
||||
redirect RedirectTemporary $ toMaster LoginR
|
||||
|
||||
|
||||
-- | A drop in for the getAuthId method of your YesodAuth instance which
|
||||
-- can be used if authHashDB is the only plugin in use.
|
||||
getAuthIdHashDB :: ( YesodAuth master, YesodPersist master
|
||||
, HashDBUser user, PersistEntity user
|
||||
, Key user ~ AuthId master
|
||||
, PersistBackend (YesodDB master (GGHandler sub master IO)))
|
||||
=> (AuthRoute -> Route master) -- ^ your site's Auth Route
|
||||
-> (Text -> Maybe (Unique user)) -- ^ gets user ID
|
||||
-> Creds master -- ^ the creds argument
|
||||
-> GHandler sub master (Maybe (AuthId master))
|
||||
getAuthIdHashDB authR uniq creds = do
|
||||
muid <- maybeAuth
|
||||
case muid of
|
||||
-- user already authenticated
|
||||
Just (uid, _) -> return $ Just uid
|
||||
Nothing -> do
|
||||
x <- case uniq (credsIdent creds) of
|
||||
Nothing -> return Nothing
|
||||
Just u -> runDB (getBy u)
|
||||
case x of
|
||||
-- user exists
|
||||
Just (uid, _) -> return $ Just uid
|
||||
Nothing -> do
|
||||
setMessage [QQ(html)| User not found |]
|
||||
redirect RedirectTemporary $ authR LoginR
|
||||
|
||||
-- | Prompt for username and password, validate that against a database
|
||||
-- which holds the username and a hash of the password
|
||||
authHashDB :: ( YesodAuth m, YesodPersist m
|
||||
, HashDBUser user
|
||||
, PersistEntity user
|
||||
, PersistBackend (YesodDB m (GGHandler Auth m IO)))
|
||||
=> (Text -> Maybe (Unique user)) -> AuthPlugin m
|
||||
authHashDB uniq = AuthPlugin "hashdb" dispatch $ \tm -> addHamlet
|
||||
[QQ(hamlet)|
|
||||
<div id="header">
|
||||
<h1>Login
|
||||
|
||||
<div id="login">
|
||||
<form method="post" action="@{tm login}">
|
||||
<table>
|
||||
<tr>
|
||||
<th>Username:
|
||||
<td>
|
||||
<input id="x" name="username" autofocus="" required>
|
||||
<tr>
|
||||
<th>Password:
|
||||
<td>
|
||||
<input type="password" name="password" required>
|
||||
<tr>
|
||||
<td>
|
||||
<td>
|
||||
<input type="submit" value="Login">
|
||||
|
||||
<script>
|
||||
if (!("autofocus" in document.createElement("input"))) {
|
||||
document.getElementById("x").focus();
|
||||
}
|
||||
|
||||
|]
|
||||
where
|
||||
dispatch "POST" ["login"] = postLoginR uniq >>= sendResponse
|
||||
dispatch _ _ = notFound
|
||||
|
||||
|
||||
----------------------------------------------------------------
|
||||
-- Predefined datatype
|
||||
----------------------------------------------------------------
|
||||
|
||||
-- | Generate data base instances for a valid user
|
||||
share2 mkPersist (mkMigrate "migrateUsers")
|
||||
[QQ(persist)|
|
||||
User
|
||||
username Text Eq
|
||||
password Text
|
||||
salt Text
|
||||
UniqueUser username
|
||||
|]
|
||||
|
||||
instance HashDBUser User where
|
||||
userPasswordHash = Just . userPassword
|
||||
userPasswordSalt = Just . userSalt
|
||||
setUserHashAndSalt s h u = u { userSalt = s
|
||||
, userPassword = h
|
||||
}
|
||||
65
yesod-auth/Yesod/Auth/Message.hs
Normal file
65
yesod-auth/Yesod/Auth/Message.hs
Normal file
@ -0,0 +1,65 @@
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
module Yesod.Auth.Message
|
||||
( AuthMessage (..)
|
||||
, defaultMessage
|
||||
) where
|
||||
|
||||
import Data.Monoid (mappend)
|
||||
import Data.Text (Text)
|
||||
|
||||
data AuthMessage =
|
||||
NoOpenID
|
||||
| LoginOpenID
|
||||
| Email
|
||||
| Password
|
||||
| Register
|
||||
| RegisterLong
|
||||
| EnterEmail
|
||||
| ConfirmationEmailSentTitle
|
||||
| ConfirmationEmailSent Text
|
||||
| AddressVerified
|
||||
| InvalidKeyTitle
|
||||
| InvalidKey
|
||||
| InvalidEmailPass
|
||||
| BadSetPass
|
||||
| SetPassTitle
|
||||
| SetPass
|
||||
| NewPass
|
||||
| ConfirmPass
|
||||
| PassMismatch
|
||||
| PassUpdated
|
||||
| Facebook
|
||||
| LoginViaEmail
|
||||
| InvalidLogin
|
||||
| NowLoggedIn
|
||||
| LoginTitle
|
||||
|
||||
defaultMessage :: AuthMessage -> Text
|
||||
defaultMessage NoOpenID = "No OpenID identifier found"
|
||||
defaultMessage LoginOpenID = "Login via OpenID"
|
||||
defaultMessage Email = "Email"
|
||||
defaultMessage Password = "Password"
|
||||
defaultMessage Register = "Register"
|
||||
defaultMessage RegisterLong = "Register a new account"
|
||||
defaultMessage EnterEmail = "Enter your e-mail address below, and a confirmation e-mail will be sent to you."
|
||||
defaultMessage ConfirmationEmailSentTitle = "Confirmation e-mail sent"
|
||||
defaultMessage (ConfirmationEmailSent email) =
|
||||
"A confirmation e-mail has been sent to " `mappend`
|
||||
email `mappend`
|
||||
"."
|
||||
defaultMessage AddressVerified = "Address verified, please set a new password"
|
||||
defaultMessage InvalidKeyTitle = "Invalid verification key"
|
||||
defaultMessage InvalidKey = "I'm sorry, but that was an invalid verification key."
|
||||
defaultMessage InvalidEmailPass = "Invalid email/password combination"
|
||||
defaultMessage BadSetPass = "You must be logged in to set a password"
|
||||
defaultMessage SetPassTitle = "Set password"
|
||||
defaultMessage SetPass = "Set a new password"
|
||||
defaultMessage NewPass = "New password"
|
||||
defaultMessage ConfirmPass = "Confirm"
|
||||
defaultMessage PassMismatch = "Passwords did not match, please try again"
|
||||
defaultMessage PassUpdated = "Password updated"
|
||||
defaultMessage Facebook = "Login with Facebook"
|
||||
defaultMessage LoginViaEmail = "Login via email"
|
||||
defaultMessage InvalidLogin = "Invalid login"
|
||||
defaultMessage NowLoggedIn = "You are now logged in"
|
||||
defaultMessage LoginTitle = "Login"
|
||||
88
yesod-auth/Yesod/Auth/OAuth.hs
Normal file
88
yesod-auth/Yesod/Auth/OAuth.hs
Normal file
@ -0,0 +1,88 @@
|
||||
{-# LANGUAGE CPP, QuasiQuotes, OverloadedStrings #-}
|
||||
{-# OPTIONS_GHC -fwarn-unused-imports #-}
|
||||
module Yesod.Auth.OAuth
|
||||
( authOAuth
|
||||
, oauthUrl
|
||||
, authTwitter
|
||||
, twitterUrl
|
||||
) where
|
||||
|
||||
#include "qq.h"
|
||||
|
||||
import Yesod.Auth
|
||||
import Yesod.Form
|
||||
import Yesod.Handler
|
||||
import Yesod.Widget
|
||||
import Text.Hamlet (html)
|
||||
import Web.Authenticate.OAuth
|
||||
import Data.Maybe
|
||||
import Data.String
|
||||
import Data.ByteString.Char8 (pack)
|
||||
import Control.Arrow ((***))
|
||||
import Control.Monad.IO.Class (liftIO)
|
||||
import Control.Monad.Trans.Class (lift)
|
||||
import Data.Text (Text, unpack)
|
||||
import Data.Text.Encoding (encodeUtf8, decodeUtf8With)
|
||||
import Data.Text.Encoding.Error (lenientDecode)
|
||||
import Data.ByteString (ByteString)
|
||||
import Control.Applicative ((<$>), (<*>))
|
||||
|
||||
oauthUrl :: Text -> AuthRoute
|
||||
oauthUrl name = PluginR name ["forward"]
|
||||
|
||||
authOAuth :: YesodAuth m =>
|
||||
Text -- ^ Service Name
|
||||
-> String -- ^ OAuth Parameter Name to use for identify
|
||||
-> String -- ^ Request URL
|
||||
-> String -- ^ Access Token URL
|
||||
-> String -- ^ Authorize URL
|
||||
-> String -- ^ Consumer Key
|
||||
-> String -- ^ Consumer Secret
|
||||
-> AuthPlugin m
|
||||
authOAuth name ident reqUrl accUrl authUrl key sec = AuthPlugin name dispatch login
|
||||
where
|
||||
url = PluginR name []
|
||||
oauth = OAuth { oauthServerName = unpack name, oauthRequestUri = reqUrl
|
||||
, oauthAccessTokenUri = accUrl, oauthAuthorizeUri = authUrl
|
||||
, oauthSignatureMethod = HMACSHA1
|
||||
, oauthConsumerKey = fromString key, oauthConsumerSecret = fromString sec
|
||||
, oauthCallback = Nothing
|
||||
}
|
||||
dispatch "GET" ["forward"] = do
|
||||
render <- getUrlRender
|
||||
tm <- getRouteToMaster
|
||||
let oauth' = oauth { oauthCallback = Just $ encodeUtf8 $ render $ tm url }
|
||||
tok <- liftIO $ getTemporaryCredential oauth'
|
||||
redirectText RedirectTemporary (fromString $ authorizeUrl oauth' tok)
|
||||
dispatch "GET" [] = do
|
||||
(verifier, oaTok) <- runInputGet $ (,)
|
||||
<$> ireq textField "oauth_verifier"
|
||||
<*> ireq textField "oauth_token"
|
||||
let reqTok = Credential [ ("oauth_verifier", encodeUtf8 verifier), ("oauth_token", encodeUtf8 oaTok)
|
||||
]
|
||||
accTok <- liftIO $ getAccessToken oauth reqTok
|
||||
let crId = decodeUtf8With lenientDecode $ fromJust $ lookup (pack ident) $ unCredential accTok
|
||||
creds = Creds name crId $ map (bsToText *** bsToText ) $ unCredential accTok
|
||||
setCreds True creds
|
||||
dispatch _ _ = notFound
|
||||
login tm = do
|
||||
render <- lift getUrlRender
|
||||
let oaUrl = render $ tm $ oauthUrl name
|
||||
addHtml
|
||||
[QQ(html)| <a href=#{oaUrl}>Login with #{name} |]
|
||||
|
||||
authTwitter :: YesodAuth m =>
|
||||
String -- ^ Consumer Key
|
||||
-> String -- ^ Consumer Secret
|
||||
-> AuthPlugin m
|
||||
authTwitter = authOAuth "twitter"
|
||||
"screen_name"
|
||||
"http://twitter.com/oauth/request_token"
|
||||
"http://twitter.com/oauth/access_token"
|
||||
"http://twitter.com/oauth/authorize"
|
||||
|
||||
twitterUrl :: AuthRoute
|
||||
twitterUrl = oauthUrl "twitter"
|
||||
|
||||
bsToText :: ByteString -> Text
|
||||
bsToText = decodeUtf8With lenientDecode
|
||||
88
yesod-auth/Yesod/Auth/OpenId.hs
Normal file
88
yesod-auth/Yesod/Auth/OpenId.hs
Normal file
@ -0,0 +1,88 @@
|
||||
{-# LANGUAGE QuasiQuotes #-}
|
||||
{-# LANGUAGE CPP #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
module Yesod.Auth.OpenId
|
||||
( authOpenId
|
||||
, forwardUrl
|
||||
) where
|
||||
|
||||
#include "qq.h"
|
||||
|
||||
import Yesod.Auth
|
||||
import qualified Web.Authenticate.OpenId as OpenId
|
||||
import Control.Monad.Attempt
|
||||
|
||||
import Yesod.Form
|
||||
import Yesod.Handler
|
||||
import Yesod.Widget
|
||||
import Yesod.Request
|
||||
import Text.Hamlet (hamlet)
|
||||
import Text.Cassius (cassius)
|
||||
import Text.Blaze (toHtml)
|
||||
import Control.Monad.Trans.Class (lift)
|
||||
import Data.Text (Text)
|
||||
import qualified Yesod.Auth.Message as Msg
|
||||
|
||||
forwardUrl :: AuthRoute
|
||||
forwardUrl = PluginR "openid" ["forward"]
|
||||
|
||||
authOpenId :: YesodAuth m => AuthPlugin m
|
||||
authOpenId =
|
||||
AuthPlugin "openid" dispatch login
|
||||
where
|
||||
complete = PluginR "openid" ["complete"]
|
||||
name = "openid_identifier"
|
||||
login tm = do
|
||||
ident <- lift newIdent
|
||||
y <- lift getYesod
|
||||
addCassius
|
||||
[QQ(cassius)|##{ident}
|
||||
background: #fff url(http://www.myopenid.com/static/openid-icon-small.gif) no-repeat scroll 0pt 50%;
|
||||
padding-left: 18px;
|
||||
|]
|
||||
l <- lift languages
|
||||
[QQ(whamlet)|
|
||||
<form method="get" action="@{tm forwardUrl}">
|
||||
<label for="#{ident}">OpenID: #
|
||||
<input id="#{ident}" type="text" name="#{name}" value="http://">
|
||||
<input type="submit" value="_{Msg.LoginOpenID}">
|
||||
|]
|
||||
dispatch "GET" ["forward"] = do
|
||||
roid <- runInputGet $ iopt textField name
|
||||
case roid of
|
||||
Just oid -> do
|
||||
render <- getUrlRender
|
||||
toMaster <- getRouteToMaster
|
||||
let complete' = render $ toMaster complete
|
||||
res <- runAttemptT $ OpenId.getForwardUrl oid complete' Nothing []
|
||||
attempt
|
||||
(\err -> do
|
||||
setMessage $ toHtml $ show err
|
||||
redirect RedirectTemporary $ toMaster LoginR
|
||||
)
|
||||
(redirectText RedirectTemporary)
|
||||
res
|
||||
Nothing -> do
|
||||
toMaster <- getRouteToMaster
|
||||
setMessageI Msg.NoOpenID
|
||||
redirect RedirectTemporary $ toMaster LoginR
|
||||
dispatch "GET" ["complete", ""] = dispatch "GET" ["complete"] -- compatibility issues
|
||||
dispatch "GET" ["complete"] = do
|
||||
rr <- getRequest
|
||||
completeHelper $ reqGetParams rr
|
||||
dispatch "POST" ["complete", ""] = dispatch "POST" ["complete"] -- compatibility issues
|
||||
dispatch "POST" ["complete"] = do
|
||||
(posts, _) <- runRequestBody
|
||||
completeHelper posts
|
||||
dispatch _ _ = notFound
|
||||
|
||||
completeHelper :: YesodAuth m => [(Text, Text)] -> GHandler Auth m ()
|
||||
completeHelper gets' = do
|
||||
res <- runAttemptT $ OpenId.authenticate gets'
|
||||
toMaster <- getRouteToMaster
|
||||
let onFailure err = do
|
||||
setMessage $ toHtml $ show err
|
||||
redirect RedirectTemporary $ toMaster LoginR
|
||||
let onSuccess (OpenId.Identifier ident, _) =
|
||||
setCreds True $ Creds "openid" ident []
|
||||
attempt onFailure onSuccess res
|
||||
57
yesod-auth/Yesod/Auth/Rpxnow.hs
Normal file
57
yesod-auth/Yesod/Auth/Rpxnow.hs
Normal file
@ -0,0 +1,57 @@
|
||||
{-# LANGUAGE QuasiQuotes #-}
|
||||
{-# LANGUAGE CPP #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
module Yesod.Auth.Rpxnow
|
||||
( authRpxnow
|
||||
) where
|
||||
|
||||
#include "qq.h"
|
||||
|
||||
import Yesod.Auth
|
||||
import qualified Web.Authenticate.Rpxnow as Rpxnow
|
||||
import Control.Monad (mplus)
|
||||
|
||||
import Yesod.Handler
|
||||
import Yesod.Widget
|
||||
import Yesod.Request
|
||||
import Text.Hamlet (hamlet)
|
||||
import Control.Monad.IO.Class (liftIO)
|
||||
import Data.Text (pack, unpack)
|
||||
import Control.Arrow ((***))
|
||||
|
||||
authRpxnow :: YesodAuth m
|
||||
=> String -- ^ app name
|
||||
-> String -- ^ key
|
||||
-> AuthPlugin m
|
||||
authRpxnow app apiKey =
|
||||
AuthPlugin "rpxnow" dispatch login
|
||||
where
|
||||
login tm = do
|
||||
let url = {- FIXME urlEncode $ -} tm $ PluginR "rpxnow" []
|
||||
addHamlet
|
||||
[QQ(hamlet)|
|
||||
<iframe src="http://#{app}.rpxnow.com/openid/embed?token_url=@{url}" scrolling="no" frameBorder="no" allowtransparency="true" style="width:400px;height:240px">
|
||||
|]
|
||||
dispatch _ [] = do
|
||||
token1 <- lookupGetParams "token"
|
||||
token2 <- lookupPostParams "token"
|
||||
token <- case token1 ++ token2 of
|
||||
[] -> invalidArgs ["token: Value not supplied"]
|
||||
x:_ -> return $ unpack x
|
||||
Rpxnow.Identifier ident extra <- liftIO $ Rpxnow.authenticate apiKey token
|
||||
let creds =
|
||||
Creds "rpxnow" ident
|
||||
$ maybe id (\x -> (:) ("verifiedEmail", x))
|
||||
(lookup "verifiedEmail" extra)
|
||||
$ maybe id (\x -> (:) ("displayName", x))
|
||||
(fmap pack $ getDisplayName $ map (unpack *** unpack) extra)
|
||||
[]
|
||||
setCreds True creds
|
||||
dispatch _ _ = notFound
|
||||
|
||||
-- | Get some form of a display name.
|
||||
getDisplayName :: [(String, String)] -> Maybe String
|
||||
getDisplayName extra =
|
||||
foldr (\x -> mplus (lookup x extra)) Nothing choices
|
||||
where
|
||||
choices = ["verifiedEmail", "email", "displayName", "preferredUsername"]
|
||||
125
yesod-auth/auth2.hs
Normal file
125
yesod-auth/auth2.hs
Normal file
@ -0,0 +1,125 @@
|
||||
{-# LANGUAGE QuasiQuotes, TypeFamilies, TemplateHaskell #-}
|
||||
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
|
||||
import Yesod
|
||||
import Yesod.Mail
|
||||
|
||||
import Yesod.Helpers.Auth2
|
||||
import Yesod.Helpers.Auth2.OpenId
|
||||
import Yesod.Helpers.Auth2.Rpxnow
|
||||
import Yesod.Helpers.Auth2.Facebook
|
||||
import Yesod.Helpers.Auth2.Email
|
||||
|
||||
import Control.Monad (join)
|
||||
import Database.Persist.Sqlite
|
||||
import Safe (readMay)
|
||||
|
||||
mkPersist [$persist|
|
||||
Email
|
||||
email String Eq
|
||||
status Bool update
|
||||
verkey String null update
|
||||
password String null update
|
||||
UniqueEmail email
|
||||
|]
|
||||
|
||||
data A2 = A2 { connPool :: ConnectionPool }
|
||||
mkYesod "A2" [$parseRoutes|
|
||||
/auth AuthR Auth getAuth
|
||||
|]
|
||||
instance Yesod A2 where approot _ = "http://localhost:3000"
|
||||
instance YesodAuth A2 where
|
||||
type AuthId A2 = String
|
||||
loginDest _ = AuthR CheckR
|
||||
logoutDest _ = AuthR CheckR
|
||||
getAuthId = return . Just . credsIdent
|
||||
showAuthId = const id
|
||||
readAuthId = const Just
|
||||
authPlugins =
|
||||
[ authDummy
|
||||
, authOpenId
|
||||
, authRpxnow "yesod-test" "c8043882f14387d7ad8dfc99a1a8dab2e028f690"
|
||||
, authFacebook
|
||||
"d790dfc0203e31c0209ed32f90782c31"
|
||||
"a7685e10c8977f5435e599aaf1d232eb"
|
||||
[]
|
||||
, authEmail
|
||||
]
|
||||
|
||||
main :: IO ()
|
||||
main = withConnectionPool $ \p -> do
|
||||
flip runConnectionPool p $ runMigration $ migrate (undefined :: Email)
|
||||
basicHandler 3000 $ A2 p
|
||||
|
||||
instance YesodAuthEmail A2 where
|
||||
type AuthEmailId A2 = EmailId
|
||||
showAuthEmailId _ = show
|
||||
readAuthEmailId _ = readMay
|
||||
|
||||
addUnverified email verkey = runDB $ insert $ Email email False (Just verkey) Nothing
|
||||
sendVerifyEmail email verkey verurl = do
|
||||
render <- getUrlRenderParams
|
||||
tm <- getRouteToMaster
|
||||
let lbs = renderHamlet render [$hamlet|
|
||||
%p
|
||||
%a!href=$verurl$ Verify your email address.
|
||||
|]
|
||||
liftIO $ renderSendMail Mail
|
||||
{ mailHeaders =
|
||||
[ ("To", email)
|
||||
, ("From", "reply@orangeroster.com")
|
||||
, ("Subject", "OrangeRoster: Verify your email address")
|
||||
]
|
||||
, mailPlain = verurl
|
||||
, mailParts =
|
||||
[ Part
|
||||
{ partType = "text/html; charset=utf-8"
|
||||
, partEncoding = None
|
||||
, partDisposition = Inline
|
||||
, partContent = lbs
|
||||
}
|
||||
]
|
||||
}
|
||||
getVerifyKey emailid = runDB $ do
|
||||
x <- get $ fromIntegral emailid
|
||||
return $ maybe Nothing emailVerkey x
|
||||
setVerifyKey emailid verkey = runDB $
|
||||
update (fromIntegral emailid) [EmailVerkey $ Just verkey]
|
||||
verifyAccount emailid' = runDB $ do
|
||||
let emailid = fromIntegral emailid'
|
||||
x <- get emailid
|
||||
uid <-
|
||||
case x of
|
||||
Nothing -> return Nothing
|
||||
Just email -> do
|
||||
update emailid [EmailStatus True]
|
||||
return $ Just $ emailEmail email
|
||||
return uid
|
||||
getPassword email = runDB $ do
|
||||
x <- getBy $ UniqueEmail email
|
||||
return $ x >>= emailPassword . snd
|
||||
setPassword email password = runDB $
|
||||
updateWhere [EmailEmailEq email] [EmailPassword $ Just password]
|
||||
getEmailCreds email = runDB $ do
|
||||
x <- getBy $ UniqueEmail email
|
||||
case x of
|
||||
Nothing -> return Nothing
|
||||
Just (eid, e) ->
|
||||
return $ Just EmailCreds
|
||||
{ emailCredsId = fromIntegral eid
|
||||
, emailCredsAuthId = Just $ emailEmail e
|
||||
, emailCredsStatus = emailStatus e
|
||||
, emailCredsVerkey = emailVerkey e
|
||||
}
|
||||
getEmail emailid = runDB $ do
|
||||
x <- get $ fromIntegral emailid
|
||||
return $ fmap emailEmail x
|
||||
|
||||
instance YesodPersist A2 where
|
||||
type YesodDB A2 = SqlPersist
|
||||
runDB db = fmap connPool getYesod >>= runConnectionPool db
|
||||
|
||||
withConnectionPool :: MonadCatchIO m => (ConnectionPool -> m a) -> m a
|
||||
withConnectionPool = withSqlitePool "auth2.db3" 10
|
||||
|
||||
runConnectionPool :: MonadCatchIO m => SqlPersist m a -> ConnectionPool -> m a
|
||||
runConnectionPool = runSqlPool
|
||||
48
yesod-auth/browserid.hs
Normal file
48
yesod-auth/browserid.hs
Normal file
@ -0,0 +1,48 @@
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE TypeFamilies #-}
|
||||
{-# LANGUAGE QuasiQuotes #-}
|
||||
{-# LANGUAGE TemplateHaskell #-}
|
||||
{-# LANGUAGE MultiParamTypeClasses #-}
|
||||
import Yesod.Core
|
||||
import Yesod.Auth
|
||||
import Yesod.Auth.BrowserId
|
||||
import Data.Text (Text)
|
||||
import Text.Hamlet (hamlet)
|
||||
import Control.Monad.IO.Class (liftIO)
|
||||
import Yesod.Form
|
||||
import Network.Wai.Handler.Warp (run)
|
||||
|
||||
data BID = BID
|
||||
type Handler = GHandler BID BID
|
||||
|
||||
mkYesod "BID" [parseRoutes|
|
||||
/ RootR GET
|
||||
/after AfterLoginR GET
|
||||
/auth AuthR Auth getAuth
|
||||
|]
|
||||
|
||||
getRootR :: Handler ()
|
||||
getRootR = redirect RedirectTemporary $ AuthR LoginR
|
||||
|
||||
getAfterLoginR :: Handler RepHtml
|
||||
getAfterLoginR = do
|
||||
mauth <- maybeAuthId
|
||||
defaultLayout $ addHamlet [hamlet|
|
||||
<p>Auth: #{show mauth}
|
||||
|]
|
||||
|
||||
instance Yesod BID where
|
||||
approot _ = "http://localhost:3000"
|
||||
|
||||
instance YesodAuth BID where
|
||||
type AuthId BID = Text
|
||||
loginDest _ = AfterLoginR
|
||||
logoutDest _ = AuthR LoginR
|
||||
getAuthId = return . Just . credsIdent
|
||||
authPlugins = [authBrowserId "localhost:3000"]
|
||||
|
||||
instance RenderMessage BID FormMessage where
|
||||
renderMessage _ _ = defaultFormMessage
|
||||
|
||||
main :: IO ()
|
||||
main = toWaiApp BID >>= run 3000
|
||||
53
yesod-auth/facebook.hs
Normal file
53
yesod-auth/facebook.hs
Normal file
@ -0,0 +1,53 @@
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE TypeFamilies #-}
|
||||
{-# LANGUAGE QuasiQuotes #-}
|
||||
{-# LANGUAGE TemplateHaskell #-}
|
||||
{-# LANGUAGE MultiParamTypeClasses #-}
|
||||
import Yesod
|
||||
import Yesod.Auth
|
||||
import Yesod.Auth.Facebook
|
||||
import Web.Authenticate.Facebook
|
||||
import Yesod.Form
|
||||
|
||||
data FB = FB Facebook
|
||||
type Handler = GHandler FB FB
|
||||
|
||||
fb :: FB
|
||||
fb = FB Facebook
|
||||
{ facebookClientId = "154414801293567"
|
||||
, facebookClientSecret = "f901e124bee0d162c9188f92b939b370"
|
||||
, facebookRedirectUri = "http://localhost:3000/facebook"
|
||||
}
|
||||
|
||||
mkYesod "FB" [parseRoutes|
|
||||
/ RootR GET
|
||||
/after AfterLoginR GET
|
||||
/auth AuthR Auth getAuth
|
||||
|]
|
||||
|
||||
getRootR :: Handler ()
|
||||
getRootR = redirect RedirectTemporary $ AuthR LoginR
|
||||
|
||||
getAfterLoginR :: Handler RepHtml
|
||||
getAfterLoginR = defaultLayout $ return ()
|
||||
|
||||
instance Yesod FB where
|
||||
approot _ = "http://localhost:3000"
|
||||
|
||||
instance YesodAuth FB where
|
||||
type AuthId FB = String
|
||||
loginDest _ = AfterLoginR
|
||||
logoutDest _ = AuthR LoginR
|
||||
getAuthId _ = do
|
||||
liftIO $ putStrLn "getAuthId"
|
||||
return $ Just "foo"
|
||||
authPlugins = return $ authFacebook
|
||||
"154414801293567"
|
||||
"f901e124bee0d162c9188f92b939b370"
|
||||
[]
|
||||
|
||||
instance RenderMessage FB FormMessage where
|
||||
renderMessage _ _ = defaultFormMessage
|
||||
|
||||
main :: IO ()
|
||||
main = warpDebug 3000 fb
|
||||
10
yesod-auth/include/qq.h
Normal file
10
yesod-auth/include/qq.h
Normal file
@ -0,0 +1,10 @@
|
||||
|
||||
-- CPP macro which choses which quasyquotes syntax to use depending
|
||||
-- on GHC version.
|
||||
--
|
||||
-- QQ stands for quasyquote.
|
||||
#if GHC7
|
||||
# define QQ(x) x
|
||||
#else
|
||||
# define QQ(x) $x
|
||||
#endif
|
||||
62
yesod-auth/yesod-auth.cabal
Normal file
62
yesod-auth/yesod-auth.cabal
Normal file
@ -0,0 +1,62 @@
|
||||
name: yesod-auth
|
||||
version: 0.7.0
|
||||
license: BSD3
|
||||
license-file: LICENSE
|
||||
author: Michael Snoyman, Patrick Brisbin
|
||||
maintainer: Michael Snoyman <michael@snoyman.com>
|
||||
synopsis: Authentication for Yesod.
|
||||
category: Web, Yesod
|
||||
stability: Stable
|
||||
cabal-version: >= 1.6.0
|
||||
build-type: Simple
|
||||
homepage: http://www.yesodweb.com/
|
||||
extra-source-files: include/qq.h
|
||||
|
||||
flag ghc7
|
||||
|
||||
library
|
||||
if flag(ghc7)
|
||||
build-depends: base >= 4.3 && < 5
|
||||
cpp-options: -DGHC7
|
||||
else
|
||||
build-depends: base >= 4 && < 4.3
|
||||
build-depends: authenticate >= 0.9.2 && < 0.10
|
||||
, bytestring >= 0.9.1.4 && < 0.10
|
||||
, yesod-core >= 0.9 && < 0.10
|
||||
, wai >= 0.4 && < 0.5
|
||||
, template-haskell
|
||||
, pureMD5 >= 1.1 && < 2.2
|
||||
, random >= 1.0 && < 1.1
|
||||
, control-monad-attempt >= 0.3.0 && < 0.4
|
||||
, text >= 0.7 && < 0.12
|
||||
, mime-mail >= 0.3 && < 0.4
|
||||
, blaze-html >= 0.4 && < 0.5
|
||||
, yesod-persistent >= 0.2 && < 0.3
|
||||
, hamlet >= 0.9 && < 0.10
|
||||
, yesod-json >= 0.2 && < 0.3
|
||||
, containers >= 0.2 && < 0.5
|
||||
, yesod-form >= 0.3 && < 0.4
|
||||
, transformers >= 0.2 && < 0.3
|
||||
, persistent >= 0.6 && < 0.7
|
||||
, persistent-template >= 0.6 && < 0.7
|
||||
, SHA >= 1.4.1.3 && < 1.5
|
||||
, http-enumerator >= 0.6 && < 0.7
|
||||
, aeson >= 0.3.2.2 && < 0.4
|
||||
, pwstore-fast >= 2.1 && < 2.2
|
||||
|
||||
exposed-modules: Yesod.Auth
|
||||
Yesod.Auth.BrowserId
|
||||
Yesod.Auth.Dummy
|
||||
Yesod.Auth.Email
|
||||
Yesod.Auth.Facebook
|
||||
Yesod.Auth.OpenId
|
||||
Yesod.Auth.OAuth
|
||||
Yesod.Auth.Rpxnow
|
||||
Yesod.Auth.HashDB
|
||||
Yesod.Auth.Message
|
||||
ghc-options: -Wall
|
||||
include-dirs: include
|
||||
|
||||
source-repository head
|
||||
type: git
|
||||
location: git://github.com/snoyberg/yesod-auth.git
|
||||
Loading…
Reference in New Issue
Block a user