Name: servant-auth
Owner: servant
Description: null
Created: 2016-09-05 13:35:46.0
Updated: 2018-05-21 12:14:17.0
Pushed: 2018-05-21 12:14:14.0
Homepage: null
Size: 249
Language: Haskell
GitHub Committers
User | Most Recent Commit | # Commits |
---|
Other Committers
User | Most Recent Commit | # Commits |
---|
This package provides safe and easy-to-use authentication options for
servant
. The same API can be protected via login and cookies, or API tokens,
without much extra work.
This library introduces a combinator Auth
:
(auths :: [*]) val
What Auth [Auth1, Auth2] Something :> API
means is that API
is protected by
either Auth1
or Auth2
, and the result of authentication will be of type
AuthResult Something
, where :
AuthResult val
BadPassword
NoSuchUser
Authenticated val
Indefinite
Your handlers will get a value of type AuthResult Something
, and can decide
what to do with it.
OPTIONS_GHC -fno-warn-unused-binds #-}
OPTIONS_GHC -fno-warn-deprecations #-}
rt Control.Concurrent (forkIO)
rt Control.Monad (forever)
rt Control.Monad.Trans (liftIO)
rt Data.Aeson (FromJSON, ToJSON)
rt GHC.Generics (Generic)
rt Network.Wai.Handler.Warp (run)
rt System.Environment (getArgs)
rt Servant
rt Servant.Auth.Server
rt Servant.Auth.Server.SetCookieOrphan ()
User = User { name :: String, email :: String }
eriving (Eq, Show, Read, Generic)
ance ToJSON User
ance ToJWT User
ance FromJSON User
ance FromJWT User
Login = Login { username :: String, password :: String }
eriving (Eq, Show, Read, Generic)
ance ToJSON Login
ance FromJSON Login
Protected
"name" :> Get '[JSON] String
> "email" :> Get '[JSON] String
'Protected' will be protected by 'auths', which we still have to specify.
ected :: AuthResult User -> Server Protected
f we get an "Authenticated v", we can trust the information in v, since
t was signed by a key we trust.
ected (Authenticated user) = return (name user) :<|> return (email user)
therwise, we return a 401.
ected _ = throwAll err401
Unprotected =
gin"
:> ReqBody '[JSON] Login
:> PostNoContent '[JSON] (Headers '[ Header "Set-Cookie" SetCookie
, Header "Set-Cookie" SetCookie]
NoContent)
|> Raw
otected :: CookieSettings -> JWTSettings -> Server Unprotected
otected cs jwts = checkCreds cs jwts :<|> serveDirectory "example/static"
API auths = (Auth auths User :> Protected) :<|> Unprotected
er :: CookieSettings -> JWTSettings -> Server (API auths)
er cs jwts = protected :<|> unprotected cs jwts
The code is common to all authentications. In order to pick one or more specific authentication methods, all we need to do is provide the expect configuration parameters.
The following example illustrates how to protect an API with tokens.
n main, we fork the server, and allow new tokens to be created in the
ommand line for the specified user name and email.
WithJWT :: IO ()
WithJWT = do
We generate the key for signing tokens. This would generally be persisted,
and kept safely
Key <- generateKey
Adding some configurations. All authentications require CookieSettings to
be in the context.
t jwtCfg = defaultJWTSettings myKey
cfg = defaultCookieSettings :. jwtCfg :. EmptyContext
--- Here we actually make concrete
api = Proxy :: Proxy (API '[JWT])
<- forkIO $ run 7249 $ serveWithContext api cfg (server defaultCookieSettings jwtCfg)
tStrLn "Started server on localhost:7249"
tStrLn "Enter name and email separated by a space for a new token"
rever $ do
xs <- words <$> getLine
case xs of
[name', email'] -> do
etoken <- makeJWT (User name' email') jwtCfg Nothing
case etoken of
Left e -> putStrLn $ "Error generating token:t" ++ show e
Right v -> putStrLn $ "New token:\t" ++ show v
_ -> putStrLn "Expecting a name and email separated by spaces"
And indeed:
adme JWT
Started server on localhost:7249
Enter name and email separated by a space for a new token
alice alice@gmail.com
New token: "eyJhbGciOiJIUzI1NiJ9.eyJkYXQiOnsiZW1haWwiOiJhbGljZUBnbWFpbC5jb20iLCJuYW1lIjoiYWxpY2UifX0.xzOIrx_A9VOKzVO-R1c1JYKBqK9risF625HOxpBzpzE"
localhost:7249/name -v
* Hostname was NOT found in DNS cache
* Trying 127.0.0.1...
* Connected to localhost (127.0.0.1) port 7249 (#0)
> GET /name HTTP/1.1
> User-Agent: curl/7.35.0
> Host: localhost:7249
> Accept: */*
>
< HTTP/1.1 401 Unauthorized
< Transfer-Encoding: chunked
< Date: Wed, 07 Sep 2016 20:17:17 GMT
* Server Warp/3.2.7 is not blacklisted
< Server: Warp/3.2.7
<
* Connection #0 to host localhost left intact
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJkYXQiOnsiZW1haWwiOiJhbGljZUBnbWFpbC5jb20iLCJuYW1lIjoiYWxpY2UifX0.xzOIrx_A9VOKzVO-R1c1JYKBqK9risF625HOxpBzpzE" \
calhost:7249/name -v
* Hostname was NOT found in DNS cache
* Trying 127.0.0.1...
* Connected to localhost (127.0.0.1) port 7249 (#0)
> GET /name HTTP/1.1
> User-Agent: curl/7.35.0
> Host: localhost:7249
> Accept: */*
> Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJkYXQiOnsiZW1haWwiOiJhbGljZUBnbWFpbC5jb20iLCJuYW1lIjoiYWxpY2UifX0.xzOIrx_A9VOKzVO-R1c1JYKBqK9risF625HOxpBzpzE
>
< HTTP/1.1 200 OK
< Transfer-Encoding: chunked
< Date: Wed, 07 Sep 2016 20:16:11 GMT
* Server Warp/3.2.7 is not blacklisted
< Server: Warp/3.2.7
< Content-Type: application/json
< Set-Cookie: JWT-Cookie=eyJhbGciOiJIUzI1NiJ9.eyJkYXQiOnsiZW1haWwiOiJhbGljZUBnbWFpbC5jb20iLCJuYW1lIjoiYWxpY2UifX0.xzOIrx_A9VOKzVO-R1c1JYKBqK9risF625HOxpBzpzE; HttpOnly; Secure
< Set-Cookie: XSRF-TOKEN=TWcdPnHr2QHcVyTw/TTBLQ==; Secure
<
* Connection #0 to host localhost left intact
"alice"%
What if, in addition to API tokens, we want to expose our API to browsers? All we need to do is say so!
WithCookies :: IO ()
WithCookies = do
We *also* need a key to sign the cookies
Key <- generateKey
Adding some configurations. 'Cookie' requires, in addition to
CookieSettings, JWTSettings (for signing), so everything is just as before
t jwtCfg = defaultJWTSettings myKey
cfg = defaultCookieSettings :. jwtCfg :. EmptyContext
--- Here is the actual change
api = Proxy :: Proxy (API '[Cookie])
n 7249 $ serveWithContext api cfg (server defaultCookieSettings jwtCfg)
ere is the login handler
kCreds :: CookieSettings
-> JWTSettings
-> Login
-> Handler (Headers '[ Header "Set-Cookie" SetCookie
, Header "Set-Cookie" SetCookie]
NoContent)
kCreds cookieSettings jwtSettings (Login "Ali Baba" "Open Sesame") = do
- Usually you would ask a database for the user info. This is just a
- regular servant handler, so you can follow your normal database access
- patterns (including using 'enter').
et usr = User "Ali Baba" "ali@email.com"
ApplyCookies <- liftIO $ acceptLogin cookieSettings jwtSettings usr
ase mApplyCookies of
Nothing -> throwError err401
Just applyCookies -> return $ applyCookies NoContent
kCreds _ _ _ = throwError err401
XSRF protection works by requiring that there be a header of the same value as
a distinguished cookie that is set by the server on each request. What the
cookie and header name are can be configured (see xsrfCookieName
and
xsrfHeaderName
in CookieSettings
), but by default they are “XSRF-TOKEN” and
“X-XSRF-TOKEN”. This means that, if your client is a browser and your are using
cookies, Javascript on the client must set the header of each request by
reading the cookie. For jQuery, and with the default values, that might be:
token = (function() {
= document.cookie.match(new RegExp('XSRF-TOKEN=([^;]+)'))
(r) return r[1];
axPrefilter(function(opts, origOpts, xhr) {
r.setRequestHeader('X-XSRF-TOKEN', token);
I believe nothing at all needs to be done if you're using Angular's $http
directive, but I haven't tested this.
XSRF protection can be disabled just for GET
requests by setting
xsrfExcludeGet = False
. You might want this if you're relying on the browser
to navigate between pages that require cookie authentication.
XSRF protection can be completely disabled by setting cookieXsrfSetting =
Nothing
in CookieSettings
. This is not recommended! If your cookie
authenticated web application runs any javascript, it's recommended to send the
XSRF header. However, if your web application runs no javascript, disabling
XSRF entirely may be required.
This README is a literate haskell file. Here is 'main', allowing you to pick between the examples above.
:: IO ()
= do
gs <- getArgs
t usage = "Usage: readme (JWT|Cookie)"
se args of
["JWT"] -> mainWithJWT
["Cookie"] -> mainWithCookies
e -> error $ "Arguments: \"" ++ unwords e ++ "\" not understood\n" ++ usage