Safe Haskell | Safe-Inferred |
---|---|
Language | Haskell2010 |
Servant.OAuth2.Examples.Authorisation
Description
This is the last example we provide, but also the most interesting, and, indeed, the main motivation for this libraries existence!
Here we show how to build type-level authorisation into your Servant API, backed by authentication with OAuth2.
We assume you've read over the previous two examples, as we build directly on that knowledge:
Synopsis
- type Db = HashMap Text User
- data Role
- data User = User {}
- data Env (r :: Role) = Env {}
- type PageM = ReaderT (Env 'Anyone) Handler
- type AdminPageM = ReaderT (Env 'Admin) Handler
- type OAuth2Result = '[WithStatus 303 RedirectWithCookie]
- optionalUserAuthHandler :: Db -> Key -> AuthHandler Request (Maybe User)
- data Routes mode = Routes {
- site :: mode :- (AuthProtect "optional-cookie" :> NamedRoutes SiteRoutes)
- authGithub :: mode :- (AuthProtect Github :> ("auth" :> ("github" :> NamedRoutes (OAuth2Routes OAuth2Result))))
- authGoogle :: mode :- (AuthProtect Google :> ("auth" :> ("google" :> NamedRoutes (OAuth2Routes OAuth2Result))))
- data SiteRoutes mode = SiteRoutes {
- home :: mode :- Get '[HTML] Html
- admin :: mode :- ("admin" :> NamedRoutes AdminRoutes)
- logout :: mode :- ("logout" :> UVerb 'GET '[HTML] '[WithStatus 303 RedirectWithCookie])
- siteServer :: SiteRoutes (AsServerT PageM)
- data AdminRoutes mode = AdminRoutes {
- adminHome :: mode :- Get '[HTML] Html
- adminHandler :: AdminPageM Html
- verifyAdmin :: ServerT (NamedRoutes AdminRoutes) AdminPageM -> ServerT (NamedRoutes AdminRoutes) PageM
- adminServer :: ServerT (NamedRoutes AdminRoutes) PageM
- isAdmin :: Maybe User -> Bool
- isLoggedIn :: PageM Bool
- getUser :: PageM (Maybe User)
- getAdmin :: AdminPageM User
- homeHandler :: PageM Html
- server :: Routes (AsServerT PageM)
- mkGithubSettings :: Key -> OAuthConfig -> OAuth2Settings PageM Github OAuth2Result
- mkGoogleSettings :: Key -> OAuthConfig -> OAuth2Settings PageM Google OAuth2Result
- main :: IO ()
- loadDb :: IO Db
Documentation
type Db = HashMap Text User Source #
This time we're going to have users. We're keeping it light and easy here, so our database is simply a map of emails to users. At this point I'd like to note a slight quirk of oauth2-based authentication.
Note that the ident that comes back from the provider is up to that provider itself. So, for example, I could make an entirely new oauth2 provider that always returns the same email, for example. In particular, it could always return _you_ email. Then, if this website added my (dodgey) provider to it's list, I would be able to log in as you, if all you to do verify accounts is look up the user by the email. So, in any real system, you should track the provider name along side the user ident, and only use that combination to find users. We don't do that here, but it's worth remembering.
Since: 0.1.0.0
This is a collection of data that we'll want to have available during
page processing; so we will wrap the servant Handler
type with a
ReaderT
over this type.
Since: 0.1.0.0
Constructors
Env | |
Fields
|
type PageM = ReaderT (Env 'Anyone) Handler Source #
Our type-level authorisation system. We tag two kinds of page monads;
one that works for Anyone
; this one.
Since: 0.1.0.0
type AdminPageM = ReaderT (Env 'Admin) Handler Source #
And this one, that is specialised to Admin
users. If we make a mistake,
we will get a type error along the lines of Cannot match 'Admin with
'Anyone
.
Since: 0.1.0.0
type OAuth2Result = '[WithStatus 303 RedirectWithCookie] Source #
As in the Servant.OAuth2.Examples.Cookies example, our result type is just a redirection with a cookie.
Since: 0.1.0.0
optionalUserAuthHandler :: Db -> Key -> AuthHandler Request (Maybe User) Source #
This is almost identical to the Servant.OAuth2.Examples.Cookies example, except we look up the user in the database, and if we find it, we return it.
Since: 0.1.0.0
This follows exactly the Servant.OAuth2.Examples.Cookies example; we're using two providers because in the hard-coded `db.txt` file I've set different roles for my own account with different providers; you'll be able to edit that file to do the same.
Since: 0.1.0.0
Constructors
Routes | |
Fields
|
Instances
Generic (Routes mode) Source # | |
type Rep (Routes mode) Source # | |
Defined in Servant.OAuth2.Examples.Authorisation type Rep (Routes mode) = D1 ('MetaData "Routes" "Servant.OAuth2.Examples.Authorisation" "servant-oauth2-examples-0.1.0.1-EmzJtQfsyJPG4cNzNv1mEY" 'False) (C1 ('MetaCons "Routes" 'PrefixI 'True) (S1 ('MetaSel ('Just "site") 'NoSourceUnpackedness 'NoSourceStrictness 'DecidedLazy) (Rec0 (mode :- (AuthProtect "optional-cookie" :> NamedRoutes SiteRoutes))) :*: (S1 ('MetaSel ('Just "authGithub") 'NoSourceUnpackedness 'NoSourceStrictness 'DecidedLazy) (Rec0 (mode :- (AuthProtect Github :> ("auth" :> ("github" :> NamedRoutes (OAuth2Routes OAuth2Result)))))) :*: S1 ('MetaSel ('Just "authGoogle") 'NoSourceUnpackedness 'NoSourceStrictness 'DecidedLazy) (Rec0 (mode :- (AuthProtect Google :> ("auth" :> ("google" :> NamedRoutes (OAuth2Routes OAuth2Result))))))))) |
data SiteRoutes mode Source #
We now have a slightly more complicated route setup; we need our
homepage, and our admin area, which we will aim to protect with our
type-level tags; we also need a logout
route, because it'll be convenient
for testing. This route will simply delete the present cookie.
Since: 0.1.0.0
Constructors
SiteRoutes | |
Fields
|
Instances
Generic (SiteRoutes mode) Source # | |
Defined in Servant.OAuth2.Examples.Authorisation Associated Types type Rep (SiteRoutes mode) :: Type -> Type # Methods from :: SiteRoutes mode -> Rep (SiteRoutes mode) x # to :: Rep (SiteRoutes mode) x -> SiteRoutes mode # | |
type Rep (SiteRoutes mode) Source # | |
Defined in Servant.OAuth2.Examples.Authorisation type Rep (SiteRoutes mode) = D1 ('MetaData "SiteRoutes" "Servant.OAuth2.Examples.Authorisation" "servant-oauth2-examples-0.1.0.1-EmzJtQfsyJPG4cNzNv1mEY" 'False) (C1 ('MetaCons "SiteRoutes" 'PrefixI 'True) (S1 ('MetaSel ('Just "home") 'NoSourceUnpackedness 'NoSourceStrictness 'DecidedLazy) (Rec0 (mode :- Get '[HTML] Html)) :*: (S1 ('MetaSel ('Just "admin") 'NoSourceUnpackedness 'NoSourceStrictness 'DecidedLazy) (Rec0 (mode :- ("admin" :> NamedRoutes AdminRoutes))) :*: S1 ('MetaSel ('Just "logout") 'NoSourceUnpackedness 'NoSourceStrictness 'DecidedLazy) (Rec0 (mode :- ("logout" :> UVerb 'GET '[HTML] '[WithStatus 303 RedirectWithCookie])))))) |
siteServer :: SiteRoutes (AsServerT PageM) Source #
Nothing too innovative; we just pass off to respective handlers and
servers; in the logout
route we set an empty cookie and redirect home.
Since: 0.1.0.0
data AdminRoutes mode Source #
Our admin routes. At this point they look normal.
Since: 0.1.0.0
Constructors
AdminRoutes | |
Fields
|
Instances
Generic (AdminRoutes mode) Source # | |
Defined in Servant.OAuth2.Examples.Authorisation Associated Types type Rep (AdminRoutes mode) :: Type -> Type # Methods from :: AdminRoutes mode -> Rep (AdminRoutes mode) x # to :: Rep (AdminRoutes mode) x -> AdminRoutes mode # | |
type Rep (AdminRoutes mode) Source # | |
Defined in Servant.OAuth2.Examples.Authorisation type Rep (AdminRoutes mode) = D1 ('MetaData "AdminRoutes" "Servant.OAuth2.Examples.Authorisation" "servant-oauth2-examples-0.1.0.1-EmzJtQfsyJPG4cNzNv1mEY" 'False) (C1 ('MetaCons "AdminRoutes" 'PrefixI 'True) (S1 ('MetaSel ('Just "adminHome") 'NoSourceUnpackedness 'NoSourceStrictness 'DecidedLazy) (Rec0 (mode :- Get '[HTML] Html)))) |
adminHandler :: AdminPageM Html Source #
Here is where we introduce the AdminPageM
type. Typically, a handler
like this would have type Handler
; but here we're denoting it as having
the AdminPageM
type. This means we can call specific functions, that we
will define below, such as getAdmin
. Importantly, we will see that we
need to unwrap this type (by verifying the current user!) before we can
render this page.
Since: 0.1.0.0
verifyAdmin :: ServerT (NamedRoutes AdminRoutes) AdminPageM -> ServerT (NamedRoutes AdminRoutes) PageM Source #
Here's the most important function. We aim to convert AdminPageM
s into
PageM
s. We do this in the context of an PageM
function, where we
investigate the current user. If that user is an admin (vi aisAdmin
) then
we convert the given AdminPageM
into a PageM
by simply coerce
ing it;
after all, the Role
type was just a phantom type.
If we fail to verify that they are an admin, we throw a http 404 error.
Since: 0.1.0.0
adminServer :: ServerT (NamedRoutes AdminRoutes) PageM Source #
Note here that this function returns a server of PageM
s; that's because
we pass the routes through the verifyAdmin
function.
Since: 0.1.0.0
isAdmin :: Maybe User -> Bool Source #
A simple check to see if the user is present and has a role
that is
equal to `"admin"`.
Since: 0.1.0.0
isLoggedIn :: PageM Bool Source #
Check if a user is present and therefore logged in.
Since: 0.1.0.0
getUser :: PageM (Maybe User) Source #
In the context of a PageM
, maybe return the user; this is the best we
can do.
Since: 0.1.0.0
getAdmin :: AdminPageM User Source #
In the present of an AdminPageM
, definitely return a user. We're
happy with an error if this fails, because we know that a user needs to be
present.
Note that it could be an extension to this code to eliminate the fromJust
here, and ensure that whatever context we're referencing has eliminated the
Maybe
over the user.
We leave this as an exercise for the reader :)
Since: 0.1.0.0
homeHandler :: PageM Html Source #
This time our home handler does a bit of busywork to show whether or not you're logged in, and provide the relevant links. It also detects if you're an admin, and if not, provides you a link to the admin page anyway, to see if you can hack into it! :)
Since: 0.1.0.0
server :: Routes (AsServerT PageM) Source #
The final full server; we need a special hoistServer
for the site
route, because we need to add the 'Maybe User' into the Env
. Otherwise,
we just do as we've always done - pass off to the authServer
.
Since: 0.1.0.0
mkGithubSettings :: Key -> OAuthConfig -> OAuth2Settings PageM Github OAuth2Result Source #
Our usual approach for Github
settings.
Since: 0.1.0.0
mkGoogleSettings :: Key -> OAuthConfig -> OAuth2Settings PageM Google OAuth2Result Source #
Our usual approach for Google
settings.
Since: 0.1.0.0
Our usual approach to the main
function; setting up the settings,
setting up the contexts for the relevant auth handler functions.
Since: 0.1.0.0