servant-oauth2-examples-0.1.0.1: Example applications using this library in three ways.
Safe HaskellSafe-Inferred
LanguageHaskell2010

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

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

data Role Source #

We will use this type to tag particular routes as being only accessible to users with the Admin role, or, alternatively, everyone, i.e. those people having the Anyone role ... namely, everyone!

Since: 0.1.0.0

Constructors

Anyone 
Admin 

data User Source #

Our user type that lives in the database. Importantly, this holds the role, which we will check when it comes to verifying if a particular person can access the Admin route.

Since: 0.1.0.0

Constructors

User 

Fields

Instances

Instances details
Show User Source # 
Instance details

Defined in Servant.OAuth2.Examples.Authorisation

Methods

showsPrec :: Int -> User -> ShowS #

show :: User -> String #

showList :: [User] -> ShowS #

data Env (r :: Role) Source #

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

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

data Routes mode Source #

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

Instances details
Generic (Routes mode) Source # 
Instance details

Defined in Servant.OAuth2.Examples.Authorisation

Associated Types

type Rep (Routes mode) :: Type -> Type #

Methods

from :: Routes mode -> Rep (Routes mode) x #

to :: Rep (Routes mode) x -> Routes mode #

type Rep (Routes mode) Source # 
Instance details

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

Instances details
Generic (SiteRoutes mode) Source # 
Instance details

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 # 
Instance details

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

Instances details
Generic (AdminRoutes mode) Source # 
Instance details

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 # 
Instance details

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 AdminPageMs into PageMs. 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 coerceing 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 PageMs; 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

main :: IO () Source #

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

loadDb :: IO Db Source #

Utility function to load the hard-coded database.

Since: 0.1.0.0