mock login without passwords

This commit is contained in:
David Mosbach 2024-01-14 04:16:01 +01:00
parent c8835c1d45
commit 7bb209cb56
5 changed files with 155 additions and 28 deletions

View File

@ -21,6 +21,7 @@ library
exposed-modules: exposed-modules:
AuthCode AuthCode
DB DB
LoginForm
Server Server
User User
other-modules: other-modules:
@ -34,15 +35,18 @@ library
aeson aeson
, base >=4.7 && <5 , base >=4.7 && <5
, base64 , base64
, blaze-html
, bytestring , bytestring
, containers , containers
, http-api-data , http-api-data
, http-client , http-client
, http-media
, jose-jwt , jose-jwt
, servant , servant
, servant-client , servant-client
, servant-server , servant-server
, stm , stm
, string-interpolate
, text , text
, time , time
, transformers , transformers
@ -63,16 +67,19 @@ executable oauth2-mock-server-exe
aeson aeson
, base >=4.7 && <5 , base >=4.7 && <5
, base64 , base64
, blaze-html
, bytestring , bytestring
, containers , containers
, http-api-data , http-api-data
, http-client , http-client
, http-media
, jose-jwt , jose-jwt
, oauth2-mock-server , oauth2-mock-server
, servant , servant
, servant-client , servant-client
, servant-server , servant-server
, stm , stm
, string-interpolate
, text , text
, time , time
, transformers , transformers
@ -94,16 +101,19 @@ test-suite oauth2-mock-server-test
aeson aeson
, base >=4.7 && <5 , base >=4.7 && <5
, base64 , base64
, blaze-html
, bytestring , bytestring
, containers , containers
, http-api-data , http-api-data
, http-client , http-client
, http-media
, jose-jwt , jose-jwt
, oauth2-mock-server , oauth2-mock-server
, servant , servant
, servant-client , servant-client
, servant-server , servant-server
, stm , stm
, string-interpolate
, text , text
, time , time
, transformers , transformers

View File

@ -37,6 +37,9 @@ dependencies:
- base64 - base64
- http-api-data - http-api-data
- uuid - uuid
- blaze-html
- http-media
- string-interpolate
ghc-options: ghc-options:
- -Wall - -Wall

61
src/LoginForm.hs Normal file
View File

@ -0,0 +1,61 @@
{-# Language OverloadedStrings, MultiParamTypeClasses, FlexibleInstances, QuasiQuotes #-}
module LoginForm (HTML(..), Html, loginPage) where
import Prelude hiding (head)
import qualified Data.Map as M
import Data.Aeson (encode)
import qualified Data.String.Interpolate as I
import Data.String (IsString(..))
import Data.Text (Text)
import Network.HTTP.Media ((//), (/:))
import Servant.API
-- import Text.Blaze
import Text.Blaze.Html.Renderer.Utf8 (renderHtml)
import Text.Blaze.Html5
import qualified Text.Blaze.Html5.Attributes as A
data HTML
instance Accept HTML where
contentType _ = "text" // "html" /: ("charset", "utf-8")
instance MimeRender HTML Html where
mimeRender _ = renderHtml
loginPage :: M.Map Text Text -> Html
loginPage headers = docTypeHtml $ head' >> body'
where
headers' = encode headers
formID = "loginForm" :: String
emailID = "email" :: String
buttonID = "loginButton" :: String
head' = head $ do
meta ! A.charset "UTF-8"
meta ! A.name "viewport" ! A.content "width=device-width, initial-scale=1.0"
title "UniWorX Oauth2 Mock Server"
body' = body $ do
h1 "UniWorX Oauth2 Mock Server"
form ! A.id (fromString formID) ! A.autocomplete "on" $ do
label ! A.for "email" $ "User"
input ! A.type_ "email" ! A.name "email" ! A.id (fromString emailID) ! A.autocomplete "email"
button ! A.type_ "button" ! A.id (fromString buttonID) $ "Login"
script $
[I.i|
#{buttonID}.onclick = async (e) => {
let headers = new Headers(#{headers'});
let formData = new FormData(#{formID});
let creds = formData.get('#{emailID}') + ':' + '';
headers.append('Authorization', btoa(creds));
alert(creds);
alert(headers.get('Authorization'));
e.preventDefault();
await fetch('../code', {
method: 'GET',
headers: headers
})};
|]

View File

@ -7,6 +7,7 @@ module Server
)-} where )-} where
import AuthCode import AuthCode
import LoginForm
import User import User
import Control.Applicative ((<|>)) import Control.Applicative ((<|>))
@ -25,6 +26,7 @@ import Data.Maybe (fromMaybe, fromJust, isJust, isNothing)
import Data.String (IsString (..)) import Data.String (IsString (..))
import Data.Text hiding (elem, find, head, length, map, null, splitAt, tail, words) import Data.Text hiding (elem, find, head, length, map, null, splitAt, tail, words)
import Data.Text.Encoding (decodeUtf8, encodeUtf8) import Data.Text.Encoding (decodeUtf8, encodeUtf8)
import Data.Text.Encoding.Base64
import Data.Time.Clock (NominalDiffTime (..), nominalDay, UTCTime(..), getCurrentTime, addUTCTime) import Data.Time.Clock (NominalDiffTime (..), nominalDay, UTCTime(..), getCurrentTime, addUTCTime)
import Data.UUID.V4 import Data.UUID.V4
@ -52,11 +54,6 @@ import qualified Text.Read.Lex as Lex
import Web.Internal.FormUrlEncoded (FromForm(..), parseUnique, parseMaybe) import Web.Internal.FormUrlEncoded (FromForm(..), parseUnique, parseMaybe)
testUsers :: [User] -- TODO move to db
testUsers =
[ User {name = "Fallback User", email = "foo@bar.com", password = "0000", uID = "0"}
, User {name = "Tina Tester", email = "t@t.tt", password = "1111", uID = "1"}
, User {name = "Max Muster", email = "m@m.mm", password = "2222", uID = "2"}]
data AuthClient = Client data AuthClient = Client
{ ident :: Text { ident :: Text
@ -89,19 +86,34 @@ type QClient = String
type QResType = String type QResType = String
type QRedirect = String type QRedirect = String
type QState = Text type QState = Text
type QAuth = Text
type QParam = QueryParam' [Required, Strict] type QParam = QueryParam' [Required, Strict]
type Auth user userData = BasicAuth "login" user -- type Oauth2Params = QParam "scope" QScope
:> "auth" -- :> QParam "client_id" QClient
:> QParam "scope" QScope -- :> QParam "response_type" QResType
:> QParam "client_id" QClient -- :> QParam "redirect_uri" QRedirect
:> QParam "response_type" QResType -- :> QueryParam "state" QState
:> QParam "redirect_uri" QRedirect
:> QueryParam "state" QState
:> Get '[JSON] userData
-- type Insert = "insert" :> Post '[JSON] User -- type ProtectedAuth user = BasicAuth "login" user :> "auth" :> Auth -- Prompts for username & password
-- type QuickAuth = "qauth" :> Auth -- Prompts for username only
type Foo user userData = BasicAuth "login" user :> "foo" :> Get '[JSON] userData
type Auth = "auth"
:> QParam "scope" QScope
:> QParam "client_id" QClient
:> QParam "response_type" QResType
:> QParam "redirect_uri" QRedirect
:> QueryParam "state" QState
:> Get '[HTML] Html -- login
type AuthCode = "code"
:> HeaderR "Authorization" QAuth
:> HeaderR "OA2_Scope" QScope
:> HeaderR "OA2_Client_ID" QClient
:> HeaderR "OA2_Redirect_URI" QRedirect
:> Header "OA2_State" QState
:> Get '[JSON] () -- returns auth code
type AuthHandler user = ReaderT (AuthState user) Handler type AuthHandler user = ReaderT (AuthState user) Handler
@ -110,29 +122,50 @@ type AuthServer user a = ServerT a (AuthHandler user)
toHandler :: forall user userData a . UserData user userData => AuthState user -> AuthHandler user a -> Handler a toHandler :: forall user userData a . UserData user userData => AuthState user -> AuthHandler user a -> Handler a
toHandler s h = runReaderT h s toHandler s h = runReaderT h s
authServer :: forall user userData . UserData user userData => AuthServer user (Auth user userData) loginServer :: forall user userData . UserData user userData => AuthServer user Auth
authServer = handleAuth loginServer = handleAuth
where where
handleAuth :: user handleAuth :: QScope
-> QScope
-> QClient -> QClient
-> QResType -> QResType
-> QRedirect -> QRedirect
-> Maybe QState -> Maybe QState
-> AuthHandler user userData -> AuthHandler user Html
handleAuth u scopes client responseType url mState = do handleAuth scopes client responseType url mState = do
let
responseType' = read @ResponseType responseType
headers = Map.fromList @Text @Text
[ ("OA2_Scope", pack scopes)
, ("OA2_Client_ID", pack client)
, ("OA2_Redirect_URI", pack url)]
headers' = if isJust mState then Map.insert "OA2_State" (fromJust mState) headers else headers
unless (responseType' == Code) $ throwError err500 { errBody = "Unsupported response type" }
return $ loginPage headers'
codeServer :: forall user userData . UserData user userData => AuthServer user AuthCode
codeServer = handleCreds
where
handleCreds :: QAuth
-> QScope
-> QClient
-> QRedirect
-> Maybe QState
-> AuthHandler user ()
handleCreds creds scopes client url mState = do
unless (isJust $ find (\c -> ident c == pack client) trustedClients) . -- TODO fetch trusted clients from db | TODO also check if the redirect url really belongs to the client unless (isJust $ find (\c -> ident c == pack client) trustedClients) . -- TODO fetch trusted clients from db | TODO also check if the redirect url really belongs to the client
throwError $ err404 { errBody = "Not a trusted client."} throwError $ err404 { errBody = "Not a trusted client."}
let let
responseType' = read @ResponseType responseType
scopes' = map (readScope @user @userData) $ words scopes scopes' = map (readScope @user @userData) $ words scopes
liftIO $ print responseType' [userName, password] = splitOn ":" $ decodeBase64Lenient creds
unless (responseType' == Code) $ throwError err500 { errBody = "Unsupported response type" } liftIO $ print userName
mUser <- liftIO $ lookupUser @user @userData userName password
unless (isJust mUser) $ throwError err500 { errBody = "Unknown user."}
let u = fromJust mUser
mAuthCode <- asks (genUnencryptedCode (AuthRequest client 600 u scopes') url) >>= liftIO mAuthCode <- asks (genUnencryptedCode (AuthRequest client 600 u scopes') url) >>= liftIO
liftIO $ print mAuthCode liftIO $ print mAuthCode
liftIO . putStrLn $ "user: " ++ show u ++ " | scopes: " ++ show (map (showScope @user @userData) scopes') liftIO . putStrLn $ "user: " ++ show u ++ " | scopes: " ++ show (map (showScope @user @userData) scopes')
redirect $ addParams url mAuthCode mState redirect $ addParams url mAuthCode mState
redirect :: Maybe ByteString -> AuthHandler user userData redirect :: Maybe ByteString -> AuthHandler user ()
redirect (Just url) = liftIO (print url) >> throwError err303 { errHeaders = [("Location", url)]} redirect (Just url) = liftIO (print url) >> throwError err303 { errHeaders = [("Location", url)]}
redirect Nothing = throwError err500 { errBody = "Could not generate authorisation code."} redirect Nothing = throwError err500 { errBody = "Could not generate authorisation code."}
addParams :: String -> Maybe Text -> Maybe Text -> Maybe ByteString addParams :: String -> Maybe Text -> Maybe Text -> Maybe ByteString
@ -143,6 +176,7 @@ authServer = handleAuth
rState = case mState of {Just s -> "&state=" ++ (unpack . replace "/" "%2F" $ replace "=" "%3D" s); Nothing -> ""} rState = case mState of {Just s -> "&state=" ++ (unpack . replace "/" "%2F" $ replace "=" "%3D" s); Nothing -> ""}
post' = if not (null post) then '&' : tail post else post post' = if not (null post) then '&' : tail post else post
in Just . fromString $ pre ++ "?code=" ++ (unpack code) ++ post' ++ rState in Just . fromString $ pre ++ "?code=" ++ (unpack code) ++ post' ++ rState
---------------------- ----------------------
@ -284,16 +318,22 @@ userListEndpoint = handleUserData
---- Server Main ---- ---- Server Main ----
------------------- -------------------
type Routing user userData = Auth user userData type Routing user userData = Auth
:<|> AuthCode
:<|> Token :<|> Token
:<|> Me userData :<|> Me userData
:<|> UserList userData :<|> UserList userData
:<|> Foo user userData
-- :<|> "qauth" :> Get '[HTML] Html
routing :: forall user userData . UserData user userData => AuthServer user (Routing user userData) routing :: forall user userData . UserData user userData => AuthServer user (Routing user userData)
routing = authServer @user @userData routing = loginServer @user @userData
:<|> codeServer @user @userData
:<|> tokenEndpoint @user @userData :<|> tokenEndpoint @user @userData
:<|> userEndpoint @user @userData :<|> userEndpoint @user @userData
:<|> userListEndpoint @user @userData :<|> userListEndpoint @user @userData
:<|> undefined
-- :<|> return (loginPage "/foobar")
exampleAuthServer :: AuthServer User (Routing User (Map.Map Text Text)) exampleAuthServer :: AuthServer User (Routing User (Map.Map Text Text))
exampleAuthServer = routing exampleAuthServer = routing

View File

@ -3,20 +3,26 @@
module User module User
( UserData(..) ( UserData(..)
, User (..) , User (..)
, testUsers
) where ) where
import Data.Aeson import Data.Aeson
import Data.List (find)
import Data.Map.Strict import Data.Map.Strict
import Data.Text hiding (singleton) import Data.Maybe
import Data.Text hiding (singleton, find)
import GHC.Generics import GHC.Generics
type UserName = Text
type Password = Text
class (Show u, ToJSON a, Monoid a) => UserData u a where -- TODO Show maybe not necessary, but currently needed for TypeApplications class (Show u, ToJSON a, Monoid a) => UserData u a where -- TODO Show maybe not necessary, but currently needed for TypeApplications
data Scope u data Scope u
readScope :: String -> Scope u readScope :: String -> Scope u
showScope :: Scope u -> String showScope :: Scope u -> String
userScope :: u -> Scope u -> a userScope :: u -> Scope u -> a
lookupUser :: UserName -> Password -> IO (Maybe u)
data User = User data User = User
@ -26,10 +32,17 @@ data User = User
, uID :: Text , uID :: Text
} deriving (Eq, Show) } deriving (Eq, Show)
testUsers :: [User] -- TODO move to db
testUsers =
[ User {name = "Fallback User", email = "foo@bar.com", password = "0000", uID = "0"}
, User {name = "Tina Tester", email = "t@t.tt", password = "1111", uID = "1"}
, User {name = "Max Muster", email = "m@m.mm", password = "2222", uID = "2"}]
instance UserData User (Map Text Text) where instance UserData User (Map Text Text) where
data Scope User = ID | Profile deriving (Show, Read, Eq) data Scope User = ID | Profile deriving (Show, Read, Eq)
readScope = read readScope = read
showScope = show showScope = show
userScope User{..} ID = singleton "id" uID userScope User{..} ID = singleton "id" uID
userScope User{..} Profile = fromList [("name", name), ("email", email)] userScope User{..} Profile = fromList [("name", name), ("email", email)]
lookupUser e _ = return $ find (\User{..} -> email == e) testUsers