When writing a backend service we will want to access information about the
authenticated user. To enable this, auth-service provides a library,
auth-service-core
.
To add auth-service, first we need to add the repositories to the stack.yaml
:
extra-deps:
- git: git@git.nejla.com:nejla-ab/auth-service.git
commit: c55a328bd8bbaee461e1e0c49ecf87eb88ec731a
subdirs:
- auth-service-core
- git: git@git.nejla.com:nejla-ab/signed-auth.git
commit: 094579cbf2b6017d2f4b601c2ec871958407d401
Replacing the commits with the respective commits we want to use (or a tag).
Next we need to add the auth-service-core
library to the haskell project by
including it as a dependency either in the cabal file or stack.yaml
:
dependencies:
- auth-service-core
Your application might want to talk to auth-service e.g. to retrieve user information. To that end, auth-service contains an internal API
To use the API, you need to declare an API secret. First, generate a random key, e.g.
dd if=/dev/random of=/dev/stdout bs=16 count=1 | base64
then set the SERVICE_TOKEN
environment variable in auth-service:
auth-service:
environment:
- SERVICE_TOKEN=Q0rp0I5B5VChHu40i47YRA
All API endpoints require that the X-Token
is set to the secret generated in the previous section
- Takes 1..n query parameters
uid={uid}
and returns user information for these users
Example: /service/users/by-uid?uid=68917a28-d8c5-42df-88e2-db97b881321b&uid=0867dc87-ec9a-4e14-8d7a-9e6e260c6390
-
Returns a list user objects
-
id
: ID of the found user -
info
(optional): User info object if the user exists, absent otherwise
User info:
id
: ID of the useremail
: registered email addressname
: Full namephone
(optional): Phone number registered for TFAinstances
: List of instance IDs the user has access toroles
: List of user's rolesdeactivate
(optional): Timestamp when the user was deactivated
You can connect to the API using servant client:
import qualified Servant.Client as Client
import AuthService.Api
import qualified AuthService.Types as AuthService
-- Extract the API endpoints from the API definition
getUsersC = Client.client (Proxy :: Proxy ServiceAPI)
You can then use Client.client to interact with the endpoint; don't forget to pass the secret as the first argument.
To resolve credentials in our backend, we first include auth-service-core in our haskell sources:
import AuthService.SignedHeaders (AuthHeader)
import qualified AuthService.SignedHeaders as Auth
Now we can use resolveAuthHeader
to resolve authentication credentials. It
acts as a middleware for our WAI-application.
We need to pass it an AuthContext
containing the public key to verify the
header signature, a nonce frame (created by newFrame
) and a logging function.
To get the public key We use readPublicKeyDer
, e.g. by reading it from an environment variable. It expects the key to be base64-encoded DER (like we produced earlier with openssl).
Example:
import qualified AuthService.SignedHeaders as Auth
import qualified System.Posix.Env.ByteString as Env
getHeaderPublicKey :: IO Auth.PublicKey
getHeaderPublicKey = do
mbKeyTxt <- Env.getEnv "SIGNED_HEADER_PUBLIC_KEY"
case mbKeyTxt of
Nothing -> do
hPutStrLn stderr $ "SIGNED_HEADER_PUBLIC_KEY must be set"
exitFailure
Just keyTxt -> case Auth.readPublicKeyDer keyTxt of
Left e -> do
hPutStrLn stderr $ "Could not parse public key" <> e
exitFailure
Right key -> return key
Unlike other middlewares (of type
Middleware
) resolveAuthHeader
passed the resolved information as a parameter to the next
Application
. We can then use the resolved user information e.g. in logging or
pass it to servant where we can capture them using AuthJWS
Here's an example:
import AuthService.SignedHeaders (AuthHeader)
import qualified AuthService.SignedHeaders as Auth
serveApp ::
Auth.Frame
-> Updates
-> App.ConnectionPool
-> Auth.PublicKey
-> LogFun
-> Application
serveApp frame updates pool pubKey logfun =
-- On every request, check for X-Auth header and parse it, checking
-- the signature and nonce. Passes on the resolved header
-- If Resolution fails, 403 is returned
Auth.resolveAuthHeader authCtx $ \authHeader ->
-- Log the basic request data using the user data from the resolved
-- auth header
Auth.logRequestBasic authCtx authHeader $
-- Pass the user information to servant
serveWithContext api (ctx authHeader frame)
$ handler updates pool conf
where
-- The required context to resolve the authentication header.
-- It requires the public key to verify the signature,
-- a nonce-frame (for checking that nonces are unique)
-- and a log function
authCtx = Auth.AuthContext (pubKey)
frame
(logFun)
ctx authHeader frame = authHeader :. EmptyContext
-- Define your servant API...
api = _
-- And your handler
handler = _
We can use the "AuthCredentials" combinator to retrieve the credentials. It
accepts either an 'AuthOptional
parameter, in which case the handler should
expect a Maybe AuthHeader
or 'AuthRequired
, which will automatically reject
requests without credentials with status code 403 and always passes AuthHeader
to the corresponding handler.
type myAPI = AuthCredentials 'AuthOptional AuthHeader
:> [...]
We can use logRequestBasic
to logbasic facts about an HTTP requests including
resolved authentication details as json
Fields:
- time: ISO 861 time when the request was received
- path: Request path
- user: Information about the user making the request (if available)
- reponse_status: Numeric HTTP response status
- response_time_ms: Number of milliseconds taken to formulate a response
User has the following fields:
- name: Name of the user
- email: Email address
- id: Unique user ID of the user