A simple and unobtrusive token management µ-service
This is a RESTful web service, written in javascript and built on top of Node.js
As token database backend an internal in-memory store (development) can be used or alternatively a redis store (production).
The doorkeeper service uses strictly tokens in JSON Web Token format.
This µ-service is intended as a starting point for the development of distributed web apps/services that need token-based verification. The architecture is designed in such a way that further required business logic can be easily inserted and extend the existing code base.
Another design decision of this service is the principle of minimalism. as little as possible is written into the database and if e.g. a token expires, it is also deleted from the database. this also applies to the logging (there are really only a few meaningful outputs in the production log) and no detailed error messages are sent to the client (this also benefits the security in the end)
so, let us build the next big thing and have fun 🚀
⚠️ doorkeeper is intended for productive use - but so far only used in development environments. so attentiveness is required when using it in the wild.
✔️ this is open source - using the Apache License, Version 2.0. contributions, issue reports or pull requests are always welcome!
- doorkeeper
The doorkeeper service knows two different types of tokens: The login token can be generated using a valid login id and a password. A login token can then be used to generate a session token. Such a session token can be used to authenticate api calls to your other web app/services.
Both tokens have a limited lifetime (expiration). While the session token is usually only valid for a short time (1 hour) the login token is different, it could be valid for much longer (e.g. 6 months), but expires after 7 days if the login token is not used to generate session tokens.
NOTE: all timeouts mentioned are of course customizable
In order to generate a login token, the doorkeeper service must rely on a (possibly external) user provider and on the store (a redis database).
When generating a session token based on a login token, the content of the login token is trusted and it is therefore no longer necessary to ask the user provider for it, but the store is still accessed (of course only login tokens issued by the service itself are trusted)
NOTE: The current implementation of the doorkeeper service uses a simple
users.json
file as user provider (at this point the developer is encouraged to add more enhanced user providers and extended login-is-valid verifications 😉)
The goal of the whole procedure is to give the client a valid session token that can be used to access other api calls from your actual web app services.
The session token contains the uid
, displayName
and roles
, these properties can be used by other services for authorization.
To decode and verify the content of the session token, the doorkeeper service is no longer necessary, this is possible from any other service, only the public token key from the doorkeeper is needed for verification.
NOTE: all tokens generated by the doorkeeper are cryptographically signed. this can be verified using the public token key. without the (secret) private token key, it should also not be possible for anyone else to generate valid tokens.
A list of the data contained in the session token follows:
👉 at this point the hint that the doorkeeper service can and should be adapted and extended if these properties are not enough
key | type | source of truth | description |
---|---|---|---|
iss |
string | doorkeeper | normally it is set to doorkeeper , but this is also customizable in config |
toktyp |
string | doorkeeper | the token type. if this token is a session token it will always be session |
sub |
string | doorkeeper | the login id which was used for the login |
uid |
string | user provider | the user id — is provided by the user provider |
displayName |
string | user provider | the public display name for the user. if the user provider does not specify a value here, then it will be the same as in sub |
roles |
string[] | user provider | an array of string based permissions or roles. it is up to the respective application to interpret these permissions. the doorkeeper service currently only knows the admin role. if no roles have been assigned for this token, this field can also be undefined |
route | method | description |
---|---|---|
/token/login |
POST | create login token |
/token/session |
POST | create session token |
/token |
GET | verify token and return payload as json |
/tokens |
DELETE | invalidate all login tokens |
POST /token/login
parameter | type | in | description |
---|---|---|---|
login |
string | body | login id |
password |
string | body | login secret |
Create a new login token. You will need to send the login
and password
parameters.
The service will accept the parameters only as form or json encoded body data.
Returns a signed jwt token.
POST /token/session
parameter | type | in | description |
---|---|---|---|
Authorization |
string | header | login token |
Create a new session token.
You need to pass your login token as header parameter: Authorization: Bearer XYZ123
.
Returns a signed jwt token.
GET /token
Verify and return the token payload as json.
You need to pass your token (this can be either a login or a session token) as header parameter: Authorization: Bearer XYZ123
.
parameter | type | in | description |
---|---|---|---|
Authorization |
string | header | token |
Returns the token payload data as json object.
Returns with an error (400 Bad Request
) if the token can not be verified.
DELETE /tokens
Invalidate all login tokens. So all clients have to log in again when they want to create new session tokens.
NOTE: the existing session tokens will remain in place until they expire
You need to pass your session token as header parameter: Authorization: Bearer xyz123
.
parameter | type | in | description |
---|---|---|---|
Authorization |
string | header | jwt token |
💂 SECURITY NOTE: for this api call the
admin
role must be present in the token data props under theroles
key!
For reading the config files node-config is used. All rules defined there apply.
The config files are located in the directory ./config/.
It is accepted *.json
but also *.yml
formats.
Which files are loaded depends on the environment, respectively the environment variable NODE_ENV
if this is not set, it is equivalent to NODE_ENV=development
Some config properties can also be set by environment variables, which are defined in custom-environment-variables.yml
key | type | default value | description |
---|---|---|---|
port |
number | 6100 | the http port for the doorkeeper service |
log.pretty |
boolean | true | if true all log messages are in human readable form, otherwise a json format is used |
log.httpRequests |
boolean | false | log all http requests. quite verbose. your decision if this is what you need |
authKey |
string | Authorization |
the http header parameter in which the token is expected |
jwt.iss |
string | doorkeeper |
when creating a token this value is entered as issuer |
token.login.ttl |
number | 1209600 | the login token time to live in seconds. default is two weeks |
token.login.lastLoginExpire |
number | 604800 | if a login token has not been used for this duration (specified in seconds), it is automatically invalidated (deleted from the store) so that a new login is required. default is 7 days |
token.session.expiresIn |
string or number | 1h |
the expiration for the session token. see here for a detailed format description |
keyFile.public |
string | keys/public.pem |
path to the public token key file. this key is needed by anyone who wants to verify the tokens. so it is safe and okay to make this key available to other services |
keyFile.private |
string | keys/private.pem |
path to the public token key file. this key is used to sign the tokens. this key should remain secret! |
keyFile.passphrase |
string | *** | the passphrase for the private token key. you never know |
users.staticUsersFile |
string | config/users.json |
path to the users.json file. you should definitiv create your own users.json |
users.passwordHashSecrets |
string[] | *** | the secrets used to encrypt the passwords in the users.json file. you should use scripts/set-user-secret.mjs to update or create users in a users.json file |
tokenStore |
string | redis |
in-memory and redis are the only possible stores at the moment |
destroyAllTokensAtStartup |
boolean | false | yes, it does exactly what you think :) actually more suitable for testing |
key | type | default value | description |
---|---|---|---|
redis.client |
object | { url: "redis://127.0.0.1" } |
the node-redis client config |
redis.namespace |
string | doorkeeeper.dev |
the prefix for all redis keys used by the doorkeeper service |
redis.token.namespace |
string | token |
the prefix for all token keys. a token key looks like this: doorkeeper.dev.token.{UID} |
redis.user.namespace |
string | user |
the prefix for all user keys. a user key looks for example like this: doorkeeper.dev.user.lastLoginTime.{UID} |
As a prerequisite, you need a node v16+ and a current docker environment installed.
$ npm install
Run npm test
for running all tests from test/* against the local in-memory database.
Using npm run test:redis:run
will use docker-compose to run all tests against a dockerized redis instance.
Or just use npm run test:all
to run all tests against both environments (intended for ci).
For development npm run test:watch
is meant, which only test against the in-memory database, but restarts the tests every time the sources have changed. very useful for development 😉
👉 NOTE: at the moment we use mocha as test runner and not jest because jest support for native es6 imports is still experimental AND jest runs incredibly slow in a docker context 😢
The diagrams (in this README) are generated using Mermaid.js.
To use mermaid, the docker image must first be loaded once: npm run pull-mermaid
.
The diagrams (which can be found in ./docs/) are then generated with npm run generate-diagrams
In order to run a local server, you need to start a redis instance:
$ npm run dev:redis:up
After that, you can start a local server with:
$ npm start
# .. or use:
$ npm run watch
This will start a server listening on http://localhost:6100 and restart the application when a source file changed.
for the very lazy
npm run dev
is meant ... this starts both a redis instance and then immediately the server in watch mode
To create a release version and build the docker image, simply run:
$ npm run docker:build
Start the docker container with npm run docker:run
(or use docker:start
which starts the doorkeeper service in the background) or run an interactive shell session via npm run docker:run:shell
After you build the docker image locally you can push it to the http://ghcr.io
$ docker images | grep doorkeeper
$ docker tag c958b8d5e94c ghcr.io/spearwolf/doorkeeper:latest
$ docker push ghcr.io/spearwolf/doorkeeper:latest