{-# 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)
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
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