{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE ImportQualifiedPost #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
module HOAuth2Tutorial where
import Control.Monad (void)
import Control.Monad.IO.Class (liftIO)
import Control.Monad.Trans.Except
import Data.Aeson
( FromJSON (parseJSON),
defaultOptions,
genericParseJSON,
)
import Data.ByteString.Char8 qualified as BS
import Data.ByteString.Lazy.Char8 qualified as BSL
import Data.IORef (IORef, newIORef, readIORef, writeIORef)
import Data.Text.Encoding qualified as T
import Data.Text.Lazy qualified as TL
import GHC.Generics (Generic)
import Network.HTTP.Conduit (newManager, tlsManagerSettings)
import Network.HTTP.Types (status302)
import Network.OAuth.OAuth2.AuthorizationRequest
( authorizationUrl,
)
import Network.OAuth.OAuth2.HttpClient (authGetJSON)
import Network.OAuth.OAuth2.Internal
( ExchangeToken (ExchangeToken),
OAuth2 (..),
OAuth2Error,
OAuth2Token (accessToken),
appendQueryParams,
)
import Network.OAuth.OAuth2.TokenRequest (fetchAccessToken)
import Network.OAuth.OAuth2.TokenRequest qualified as TR
import URI.ByteString (URI, serializeURIRef')
import URI.ByteString.QQ (uri)
import Web.Scotty (ActionM, scotty)
import Web.Scotty qualified as Scotty
auth0 :: OAuth2
auth0 :: OAuth2
auth0 =
OAuth2
{ oauth2ClientId :: Text
oauth2ClientId = Text
"TZlmNRtLY9duT8M4ztgFBLsFA66aEoGs",
oauth2ClientSecret :: Text
oauth2ClientSecret = Text
"",
oauth2AuthorizeEndpoint :: URIRef Absolute
oauth2AuthorizeEndpoint = [uri|https://freizl.auth0.com/authorize|],
oauth2TokenEndpoint :: URIRef Absolute
oauth2TokenEndpoint = [uri|https://freizl.auth0.com/oauth/token|],
oauth2RedirectUri :: URIRef Absolute
oauth2RedirectUri = [uri|http://localhost:9988/oauth2/callback|]
}
authorizeUrl :: URI
authorizeUrl :: URIRef Absolute
authorizeUrl =
forall a. [(ByteString, ByteString)] -> URIRef a -> URIRef a
appendQueryParams
[ (ByteString
"scope", ByteString
"openid profile email"),
(ByteString
"state", ByteString
randomStateValue)
]
forall a b. (a -> b) -> a -> b
$ OAuth2 -> URIRef Absolute
authorizationUrl OAuth2
auth0
randomStateValue :: BS.ByteString
randomStateValue :: ByteString
randomStateValue = ByteString
"random-state-to-prevent-csrf"
auth0UserInfoUri :: URI
auth0UserInfoUri :: URIRef Absolute
auth0UserInfoUri = [uri|https://freizl.auth0.com/userinfo|]
data Auth0User = Auth0User
{ Auth0User -> Text
name :: TL.Text,
Auth0User -> Text
email :: TL.Text,
Auth0User -> Text
sub :: TL.Text
}
deriving (Int -> Auth0User -> ShowS
[Auth0User] -> ShowS
Auth0User -> [Char]
forall a.
(Int -> a -> ShowS) -> (a -> [Char]) -> ([a] -> ShowS) -> Show a
showList :: [Auth0User] -> ShowS
$cshowList :: [Auth0User] -> ShowS
show :: Auth0User -> [Char]
$cshow :: Auth0User -> [Char]
showsPrec :: Int -> Auth0User -> ShowS
$cshowsPrec :: Int -> Auth0User -> ShowS
Show, forall x. Rep Auth0User x -> Auth0User
forall x. Auth0User -> Rep Auth0User x
forall a.
(forall x. a -> Rep a x) -> (forall x. Rep a x -> a) -> Generic a
$cto :: forall x. Rep Auth0User x -> Auth0User
$cfrom :: forall x. Auth0User -> Rep Auth0User x
Generic)
instance FromJSON Auth0User where
parseJSON :: Value -> Parser Auth0User
parseJSON = forall a.
(Generic a, GFromJSON Zero (Rep a)) =>
Options -> Value -> Parser a
genericParseJSON Options
defaultOptions
app :: IO ()
app :: IO ()
app = do
IORef (Maybe Auth0User)
refUser <- forall a. a -> IO (IORef a)
newIORef forall a. Maybe a
Nothing
Int -> ScottyM () -> IO ()
scotty Int
9988 forall a b. (a -> b) -> a -> b
$ do
RoutePattern -> ActionM () -> ScottyM ()
Scotty.get RoutePattern
"/" forall a b. (a -> b) -> a -> b
$ IORef (Maybe Auth0User) -> ActionM ()
indexH IORef (Maybe Auth0User)
refUser
RoutePattern -> ActionM () -> ScottyM ()
Scotty.get RoutePattern
"/login" ActionM ()
loginH
RoutePattern -> ActionM () -> ScottyM ()
Scotty.get RoutePattern
"/logout" (IORef (Maybe Auth0User) -> ActionM ()
logoutH IORef (Maybe Auth0User)
refUser)
RoutePattern -> ActionM () -> ScottyM ()
Scotty.get RoutePattern
"/oauth2/callback" forall a b. (a -> b) -> a -> b
$ IORef (Maybe Auth0User) -> ActionM ()
callbackH IORef (Maybe Auth0User)
refUser
indexH :: IORef (Maybe Auth0User) -> ActionM ()
indexH :: IORef (Maybe Auth0User) -> ActionM ()
indexH IORef (Maybe Auth0User)
refUser = do
Maybe Auth0User
muser <- forall (m :: * -> *) a. MonadIO m => IO a -> m a
liftIO (forall a. IORef a -> IO a
readIORef IORef (Maybe Auth0User)
refUser)
let info :: [Text]
info = case Maybe Auth0User
muser of
Just Auth0User
user ->
[ Text
"<p>Hello, " Text -> Text -> Text
`TL.append` Auth0User -> Text
name Auth0User
user Text -> Text -> Text
`TL.append` Text
"</p>",
Text
"<a href='/logout'>Logout</a>"
]
Maybe Auth0User
Nothing -> [Text
"<a href='/login'>Login</a>"]
Text -> ActionM ()
Scotty.html forall b c a. (b -> c) -> (a -> b) -> a -> c
. forall a. Monoid a => [a] -> a
mconcat forall a b. (a -> b) -> a -> b
$ Text
"<h1>hoauth2 Tutorial</h1>" forall a. a -> [a] -> [a]
: [Text]
info
loginH :: ActionM ()
loginH :: ActionM ()
loginH = do
Text -> Text -> ActionM ()
Scotty.setHeader Text
"Location" (URIRef Absolute -> Text
uriToText URIRef Absolute
authorizeUrl)
Status -> ActionM ()
Scotty.status Status
status302
logoutH :: IORef (Maybe Auth0User) -> ActionM ()
logoutH :: IORef (Maybe Auth0User) -> ActionM ()
logoutH IORef (Maybe Auth0User)
refUser = do
forall (m :: * -> *) a. MonadIO m => IO a -> m a
liftIO (forall a. IORef a -> a -> IO ()
writeIORef IORef (Maybe Auth0User)
refUser forall a. Maybe a
Nothing)
forall a. Text -> ActionM a
Scotty.redirect Text
"/"
callbackH :: IORef (Maybe Auth0User) -> ActionM ()
callbackH :: IORef (Maybe Auth0User) -> ActionM ()
callbackH IORef (Maybe Auth0User)
refUser = do
[Param]
pas <- ActionM [Param]
Scotty.params
forall a. Show a => ExceptT Text IO a -> ActionM a
excepttToActionM forall a b. (a -> b) -> a -> b
$ do
forall (f :: * -> *) a. Functor f => f a -> f ()
void forall a b. (a -> b) -> a -> b
$ forall e (m :: * -> *) a. m (Either e a) -> ExceptT e m a
ExceptT forall a b. (a -> b) -> a -> b
$ forall (f :: * -> *) a. Applicative f => a -> f a
pure forall a b. (a -> b) -> a -> b
$ Text -> [Param] -> Either Text Text
paramValue Text
"state" [Param]
pas
Text
codeP <- forall e (m :: * -> *) a. m (Either e a) -> ExceptT e m a
ExceptT forall a b. (a -> b) -> a -> b
$ forall (f :: * -> *) a. Applicative f => a -> f a
pure forall a b. (a -> b) -> a -> b
$ Text -> [Param] -> Either Text Text
paramValue Text
"code" [Param]
pas
Manager
mgr <- forall (m :: * -> *) a. MonadIO m => IO a -> m a
liftIO forall a b. (a -> b) -> a -> b
$ ManagerSettings -> IO Manager
newManager ManagerSettings
tlsManagerSettings
let code :: ExchangeToken
code = Text -> ExchangeToken
ExchangeToken forall a b. (a -> b) -> a -> b
$ Text -> Text
TL.toStrict Text
codeP
OAuth2Token
tokenResp <- forall (m :: * -> *) e e' a.
Functor m =>
(e -> e') -> ExceptT e m a -> ExceptT e' m a
withExceptT OAuth2Error Errors -> Text
oauth2ErrorToText (Manager
-> OAuth2
-> ExchangeToken
-> ExceptT (OAuth2Error Errors) IO OAuth2Token
fetchAccessToken Manager
mgr OAuth2
auth0 ExchangeToken
code)
let at :: AccessToken
at = OAuth2Token -> AccessToken
accessToken OAuth2Token
tokenResp
Auth0User
user <- forall (m :: * -> *) e e' a.
Functor m =>
(e -> e') -> ExceptT e m a -> ExceptT e' m a
withExceptT ByteString -> Text
bslToText (forall b.
FromJSON b =>
Manager
-> AccessToken -> URIRef Absolute -> ExceptT ByteString IO b
authGetJSON Manager
mgr AccessToken
at URIRef Absolute
auth0UserInfoUri)
forall (m :: * -> *) a. MonadIO m => IO a -> m a
liftIO forall a b. (a -> b) -> a -> b
$ forall a. IORef a -> a -> IO ()
writeIORef IORef (Maybe Auth0User)
refUser (forall a. a -> Maybe a
Just Auth0User
user)
forall a. Text -> ActionM a
Scotty.redirect Text
"/"
uriToText :: URI -> TL.Text
uriToText :: URIRef Absolute -> Text
uriToText = Text -> Text
TL.fromStrict forall b c a. (b -> c) -> (a -> b) -> a -> c
. ByteString -> Text
T.decodeUtf8 forall b c a. (b -> c) -> (a -> b) -> a -> c
. forall a. URIRef a -> ByteString
serializeURIRef'
bslToText :: BSL.ByteString -> TL.Text
bslToText :: ByteString -> Text
bslToText = [Char] -> Text
TL.pack forall b c a. (b -> c) -> (a -> b) -> a -> c
. ByteString -> [Char]
BSL.unpack
paramValue ::
TL.Text ->
[Scotty.Param] ->
Either TL.Text TL.Text
paramValue :: Text -> [Param] -> Either Text Text
paramValue Text
key [Param]
params =
if forall (t :: * -> *) a. Foldable t => t a -> Bool
null [Text]
val
then forall a b. a -> Either a b
Left (Text
"No value found for param: " forall a. Semigroup a => a -> a -> a
<> Text
key)
else forall a b. b -> Either a b
Right (forall a. [a] -> a
head [Text]
val)
where
val :: [Text]
val = forall a b. (a, b) -> b
snd forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> forall a. (a -> Bool) -> [a] -> [a]
filter (Text -> Param -> Bool
hasParam Text
key) [Param]
params
hasParam :: TL.Text -> Scotty.Param -> Bool
hasParam :: Text -> Param -> Bool
hasParam Text
t = (forall a. Eq a => a -> a -> Bool
== Text
t) forall b c a. (b -> c) -> (a -> b) -> a -> c
. forall a b. (a, b) -> a
fst
excepttToActionM :: Show a => ExceptT TL.Text IO a -> ActionM a
excepttToActionM :: forall a. Show a => ExceptT Text IO a -> ActionM a
excepttToActionM ExceptT Text IO a
e = do
Either Text a
result <- forall (m :: * -> *) a. MonadIO m => IO a -> m a
liftIO forall a b. (a -> b) -> a -> b
$ forall e (m :: * -> *) a. ExceptT e m a -> m (Either e a)
runExceptT ExceptT Text IO a
e
forall a c b. (a -> c) -> (b -> c) -> Either a b -> c
either forall a. Text -> ActionM a
Scotty.raise forall (f :: * -> *) a. Applicative f => a -> f a
pure Either Text a
result
oauth2ErrorToText :: OAuth2Error TR.Errors -> TL.Text
oauth2ErrorToText :: OAuth2Error Errors -> Text
oauth2ErrorToText OAuth2Error Errors
e = [Char] -> Text
TL.pack forall a b. (a -> b) -> a -> b
$ [Char]
"Unable fetch access token. error detail: " forall a. [a] -> [a] -> [a]
++ forall a. Show a => a -> [Char]
show OAuth2Error Errors
e