This repository offers a login server to be used with the Cocoda Mapping Tool. It allows users to authenticate using different providers (e.g. GitHub, ORCID). See https://coli-conc.gbv.de/login/api for an example on how you could use this.
- Install
- Usage
- Test
- Strategies
- JWTs
- Web interface
- OAuth endpoint
- HTTP API
- WebSocket
- Maintainers
- Contribute
- Related Works
- License
login-server requires Node.js (>= v18, v20 recommended) and access to a MongoDB database (>= v5, v7 recommended).
git clone https://github.com/gbv/login-server.git
cd login-server
npm install
# after setting up or changing providers, create indexes
npm run indexes
login-server is also available via Docker. Please refer to the documentation at https://github.com/gbv/login-server/blob/master/docker/README.md for more details.
If running the server behind a reverse proxy, make sure to include the X-Forwarded-Proto
header, allow all HTTP methods, and enable WebSocket proxying.
You need to provide two configuration files:
To configure the application:
# recommended, port for express, default: 3004
PORT=
# recommended, full base URL, default: http://localhost[:PORT]/
# (required when used in production or behind a reverse proxy)
BASE_URL=
# title of application (will be shown in header)
TITLE=My Login Server
# list of allowed origins separated by comma, includes the hostname of BASE_URL by default
ALLOWED_ORIGINS=
# required for some strategies to enable production mode, default: development
NODE_ENV=production
# strongly recommended, imprint and privacy URLs for footer and clients
IMPRINT_URL=
PRIVACY_URL=
# recommended, secret used by the session
SESSION_SECRET=
# optional, maximum number of days a session is valid (rolling), default: 30
COOKIE_MAX_DAYS=
# threshold in minutes when to send "sessionAboutToExpire" events, default: 60
SESSION_EXPIRATION_MESSAGE_THRESHOLD=
# interval in minutes in which to check for expiring sessions, default: 5
SESSION_EXPIRATION_MESSAGE_INTERVAL=
# username used for MongoDB, default: <empty>
MONGO_USER=
# password used for MongoDB, default: <empty>
MONGO_PASS=
# host used for MongoDB, default: 127.0.0.1
MONGO_HOST=
# port used for MongoDB, default: 27017
MONGO_PORT=
# database used for MongoDB, default: login-server
MONGO_DB=
# the rate limit window in ms, default: 60 * 1000
RATE_LIMIT_WINDOW=
# the rate limit tries, default: 10
RATE_LIMIT_MAX=
# a jsonwebtoken compatible keypair
JWT_PRIVATE_KEY_PATH=
JWT_PUBLIC_KEY_PATH=
# the jsonwebtoken algorithm used
JWT_ALGORITHM=
# expiration time of JWTs in seconds, default: 120, min: 10
JWT_EXPIRES_IN=
# URL for Sources, default: https://github.com/gbv/login-server
SOURCES_URL=
# the path to the providers.json file, default: ./providers.json
PROVIDERS_PATH=
# log = log all messages, warn = only log warnings and errors, error = only log errorsl default: log
VERBOSITY=
To configure the providers. See Providers.
To provide the user with information about which applications are accessing their data, and which application initiated a session's login, you can provide a list of applications in applications.json
. The list has to be an array of objects and each objects needs to have a url
and name
. Example:
[
{
"url": "https://bartoc.org",
"name": "BARTOC"
},
{
"url": "https://coli-conc.gbv.de/coli-rich/",
"name": "coli-rich"
},
{
"url": "https://coli-conc.gbv.de/cocoda/app/",
"name": "Cocoda"
},
{
"url": "https://coli-conc.gbv.de/cocoda/dev/",
"name": "Cocoda (dev)"
},
{
"url": "https://coli-conc.gbv.de/cocoda/rvk/",
"name": "Cocoda (RVK)"
},
{
"url": "https://coli-conc.gbv.de/cocoda/wikidata/",
"name": "Cocoda (Wikidata)"
},
{
"url": "https://coli-conc.gbv.de/cocoda/",
"name": "Cocoda (other)"
},
{
"url": "https://coli-conc.gbv.de",
"name": "Other coli-conc application"
}
]
The URL should be accessible because the interface will link to it. A session is associated with an application if its referrer URL contains the application's url
. Applications will be checked from top to bottom, so you should order it from most specific URL to least specific URL (see example above).
npm run start
The server provides a web interface, a HTTP API and a WebSocket.
The web interface allows users to create and manage accounts with connections to multiple identities at identity providers (see providers). Providers are used to authenticate users because the login server does not store any passwords (single sign-on).
The HTTP API and WebSocket allow client applications to interact with the login server, for instance to check whether a user has been logged in and to find out which identities belong to a user (see login-client and login-client-vue for a JavaScript libraries to connect web applications with login-server).
The login server can further be used to authenticate users against other services so users can proof their identities.
Directory bin
contains helper script for administration of a server instance such as listing user accounts and managing local providers.
Tests use the same MongoDB as configured in .env
, just with the postfix -test
after the database name.
npm test
login-server uses Passport (GitHub) as authentication middleware. Passport uses so-called "strategies" to support authentication with different providers. A list of available strategies can be found here. Currently supported strategies in login-server are:
- GitHub (via passport-github)
- ORCID (via passport-orcid)
- Mediawiki (via passport-mediawiki-oauth)
- LDAP (via passport-ldapauth)
- Stack Exchange (via passport-oauth2)
- easydb (via passport-easydb)
- Local (via passport-local)
- Script (see #117)
Because strategies use different parameters in their verify callbacks, each strategy has its own wrapper file in the folder strategies/
. To add another strategy to login-server, add a file called {name}.js
(where {name}
is the name of the strategy that is used with passport.authenticate
) with the following structure (GitHub as example):
/**
* OAuth Stategy for GitHub.
*/
// Import strategy here
import { Strategy } from "passport-github"
// Don't change this part!
export default (options, provider, callback) => new Strategy(options,
// Strategies have different callback parameters.
// `req` is always the first and the `done` callback is always last.
(req, token, tokenSecret, profile, done) => {
// Prepare a standardized object for the user profile,
// usually using information from the `profile` parameter
let providerProfile = {
// Required, don't change this!
provider: provider.id,
// Required: Choose a field that represents a unique user ID for this user
id: profile.id,
// Optional: Provides a display name (e.g. full name)
name: profile.displayName,
// Optional: Provides a username
username: profile.username
}
// Call a custom callback. `req`, `providerProfile`, and `done` are required,
// `token` and `tokenSecret` can be null.
callback(req, token, tokenSecret, providerProfile, done)
})
You can look at the existing strategies as examples and add your own via a Pull Request.
After you have added the strategy, you can use it by adding a provider to providers.json
:
[
{
"id": "github",
"strategy": "github",
"name": "GitHub",
"template": "https://github.com/{username}",
"options": {
"clientID": "abcdef1234567890",
"clientSecret": "abcdef1234567890abcdef1234567890"
},
"image": "https://upload.wikimedia.org/wikipedia/commons/9/91/Octicons-mark-github.svg",
"url": "https://github.com"
}
]
Each object in the list of providers can have the following properties:
id
(required) - Unique ID for the provider.strategy
(required) - Name of the Passport strategy used by the provider.name
(required) - Display name of the provider.template
(optional) - A template string to generate a URI (the placeholder{field}
can be any field provided in theproviderProfile
object, usually{id}
or{username}
).credentialsNecessary
(optional) - Set totrue
if username and password credentials are necessary for this provider. Instead of a redirect (for OAuth), login-server will show a login form that will send the credentials to a POST endpoint.options
(mostly required) - A options object for the strategy, often containing client credentials for the authentication endpoint.image
(optional) - An image associated with the provider. Will be shown on the login page and in the list of connected identities. You can provide static images in the folderstatic/
. The value for the property would then bestatic/myimage.svg
. If the filename matches theid
of the provider, the image will be automatically associated.url
(optional) - A URL for the provider; will be linked on its image/icon under/account
. There are default URLs for the strategiesgithub
,orcid
,mediawiki
, andstackexchange
.
The following is an example providers.json
that shows how to configure each of the existing providers:
[
{
"id": "github",
"strategy": "github",
"name": "GitHub",
"template": "https://github.com/{username}",
"options": {
"clientID": "abcdef1234567890",
"clientSecret": "abcdef1234567890abcdef1234567890"
}
},
{
"id": "orcid",
"strategy": "orcid",
"name": "ORCID",
"template": "https://orcid.org/{id}",
"options": {
"clientID": "APP-abcdef1234567890",
"clientSecret": "abcdef1-23456-7890ab-cdef12-34567890"
}
},
{
"id": "mediawiki",
"strategy": "mediawiki",
"name": "Mediawiki",
"template": "https://www.mediawiki.org/wiki/User:{username}",
"options": {
"consumerKey": "abcdef1234567890",
"consumerSecret": "abcdef1234567890abcdef1234567890"
}
},
{
"id": "stackexchange",
"strategy": "stackexchange",
"name": "Stack Exchange",
"template": "https://stackexchange.com/users/{id}",
"options": {
"clientID": "12345",
"clientSecret": "abcdef1234567890((",
"stackAppsKey": "1234567890abcdefg(("
}
},
{
"id": "my-ldap",
"strategy": "ldapauth",
"name": "My LDAP",
"credentialsNecessary": true,
"options": {
"server": {
"url": "ldap://ldap.example.com",
"bindDN": "uid=admin,dc=example,dc=com",
"bindCredentials": "abcdef1234567890",
"searchBase": "dc=example,dc=com",
"searchFilter": "(uid={{username}})"
}
}
},
{
"id": "easydb",
"name": "easydb test provider",
"strategy": "easydb",
"credentialsNecessary": true,
"options": {
"url": "https://easydb5-test.example.com/api/v1/"
}
},
{
"id": "some-script",
"strategy": "script",
"name": "Some Script",
"credentialsNecessary": true,
"template": "https://example.org/some-script/{id}",
"options": {
"script": "./bin/example-script"
}
}
]
To configure local providers, please use the provided script under bin/manage-local.js
. It will allow you to create/delete local providers, and create/delete users for local providers.
You can adjust the path to the providers.json
file with PROVIDERS_PATH
in .env
.
Notes about using the MediaWiki provider:
- If your consumer is limited to a specific instance (e.g. Wikidata only), you need to provide the baseURL for that instance in the options, for example:
"baseURL": "https://www.wikidata.org/"
. - There seems to be a bug either in Mediawiki or in passport-mediawiki-oauth that causes custom callback URLs to not work. This means that you need to provide the exact callback URL when registering your consumer (e.g.
https://coli-conc.gbv.de/login/login/wikidata/return
for our login-server instance). - See also: https://www.mediawiki.org/wiki/OAuth/For_Developers
Notes about using the Script provider:
- The Script provider is currently implemented inside Login Server (see
lib/script-strategy.js
). - The script's path (provided in
options.script
) can either be relative to Login Server's root folder, or an absolute path (recommended for Docker). - An example for a very basic Bash script can be found in
bin/example-script
. - The script needs to be executable (
chmod +x
). - The script needs to return valid JSON with the
id
value being set when authentication was successful. Optionally,name
can be provided and will be used as the display name. - Whatever language or environment the script is using needs to be available on the host that is running Login Server. When run inside a Docker container, only Bash and Node.js v20 are available. To use a different language, you need to extend Login Server's Docker image and install the required dependencies yourself.
login-server offers JSON Web Tokens that can be used to authenticate against other services (like jskos-server). jsonwebtoken is used for signing the tokens.
By default, a new RSA keypair is generated when the application is first started (2048 bits, using node-rsa). This generated keypair will by default be available in ./private.key
and ./public.key
. You can give the ./public.key
file to any other service that needs to verify the tokens. Alternatively, the currently used public key is offered at the /about endpoint.
You can also provide a custom path for the key files by setting JWT_PRIVATE_KEY_PATH
and JWT_PUBLIC_KEY_PATH
in .env
. If one or both of the keys can't be found, the keys will be generated. By default, the RS256
algorithm is used, but any other public key algorithm can be used by setting JWT_ALGORITHM
.
By default, each token is valid for 120 seconds. You can adjust this by setting JWT_EXPIRES_IN
in .env
.
Tokens get be received either through the /token endpoint or by using the WebSocket request of type token
. Additionally, a token is sent via the WebSocket after the user is logged in and then regularly before the last token expires.
Example how to verify a token:
import jwt from "jsonwebtoken"
// token, e.g. from user request
let token = "..."
// get public key from file or endpoint
let publicKey = "..."
jwt.verify(token, publicKey, (error, decoded) => {
if (error) {
// handle error
// ...
} else {
let { user, iat, exp } = decoded
// user is the user object
// iat is the issued timestamp
// exp is the expiration timestamp
// ...
}
})
Alternatively, you can use passport-jwt (example will follow).
Shows a landing page with general information about the login server.
Shows a site to manage one's user account (if already authenticated) or redirects to /login
(if not authenticated).
Shows a site to manage the user's sessions (if authenticated) or redirects to /login
(if not authenticated).
Shows a site to login (if not authenticated) or directs to /account
(if authenticated).
If the query parameter redirect_uri
is given, the site will redirect to the specified URI after a successful login. (If the parameter is given, but empty, it will use the referrer as a URI.)
Shows a login page for a provider. For OAuth providers, this page will redirect to the provider's page to connect your identity, which then redirects to /login/:provider/return
. For providers using credentials, this will show a login form.
This page also handles redirect_uri
(see /login
above).
POST endpoint for providers using credentials. If successful, it will redirect to /account
, otherwise it will redirect back to /login/:provider
.
Disconnects a provider from the user and redirects to /account
.
Logs the user out of their account. Note that the session will remain because it is used for the WebSockets. This enables the application to send events to active WebSockets for the current session, even if the user has logged out.
Shows a site to delete one's user account.
Commits user account deletion and redirects to /login
.
The server provides an OAuth redirection endpoint (redirection URI) for each OAuth provider.
Callback endpoint for OAuth requests. Will save the connected identity to the user (or create a new user if necessary) and redirect to /account
.
Before directly programming against HTTP API and WebSocket API have a look at the login-client JavaScript browser library. It can be seen in action here (source for that site).
Returns an object with keys title
(title of the login-server instance), env
(environment, like development
or production
), publicKey
(usually a RSA public key), and algorithm
(the jsonwebtoken algorithm used). The corresponding private key to the given public key is used when signing JWTs.
Returns a list of available providers (stripped off sensitive information).
Returns the currently logged in user. Returns an 404 error when no user is logged in.
Returns a specific user. Currently restricted to one's own user ID.
Adjusts a specific user. Can only be used if the same user is currently logged in. Allowed properties to change: name
(everything else will be ignored).
Removes all sessions for the current user, except the current session.
Removes the session with sessionID :id
(needs to be a session for the current user).
Returns a JSON Web Token in the format:
{
"token": "<JWT>",
"expiresIn": 120
}
See also: JWTs.
The token itself will contain a user
property (which either contains information about the currently logged in user, or is null if the user is not logged in) and a sessionID
property which is needed to authenticate within a WebSocket connection.
The WebSocket API at base URL /
sends events about the current user or session. Events are sent as JSON-encoded strings that look like this:
{
"type": "event name (see below)",
"date": "date (as ISOString)",
"data": {
"user": {
"uri": "URI of user",
"name": "name of user",
"identities": {
"xzy": {
"id": "ID of user for provider xzy",
"uri": "URI or profile URL of user for provider xzy",
"name": "display name of user for provider xzy (if available)",
"username": "username of user for provider xzy (if available)"
}
}
}
}
}
open
- sent after WebSocket connection was established, use this instead ofws.onopen
!loggedIn
- sent when the user has logged in (will be sent immediately after establishing the WebSocket if the user is already logged in)loggedOut
- sent when the user has logged out (will be sent immediately after establishing the WebSocket if the user is not logged in)updated
- sent when the user was updated (e.g. added a new identity, etc.)providers
- sent after WebSocket connection was established (consists of a propertydata.providers
with a list of available providers)about
- sent after WebSocket connection was established (propertydata
will have the same format as in GET /about)token
- sent when the user has logged in and then in intervals before the previous token expires (propertydata
will have the same format as in GET /token)authenticated
- sent as a success reply when requesting authentication (see below)pong
- sent as an answer to a request of typeping
(can be used to determine whether the WebSocket has become stale)sessionAboutToExpire
- sent when the currently associated session is about to expireerror
- sent as answer to a malformed message via the WebSocket (consists of a propertydata.message
with an error message)
You can also send requests to the WebSocket. These also have to be JSON-encoded strings in the following form:
{
"type": "name of request"
}
This is a special request that uses a JWT acquired from GET /token to associate the current WebSocket with a particular session (sent request object needs property token
)
The authenticate
request is sometimes necessary when the WebSocket is used from a different domain than login-server. In that case, a token needs to be requested via the API (e.g. using fetch with option credentials: "include"
or axios with option withCredentials: true
) and be sent via the WebSocket. The token includes the encrypted sessionID that will then be associated with the WebSocket connection. Here is an example on how a workflow from a web application could look like: https://coli-conc.gbv.de/login/api
The following is a simple example on how to connect to the WebSocket.
// Assumes server is run on localhost:3005
let socket = new WebSocket("ws://localhost:3005")
socket.addEventListener("message", (message) => {
try {
let event = JSON.parse(message)
alert(event.event, event.user && event.user.uri)
} catch(error) {
console.warn("Error parsing WebSocket message", message)
}
})
PRs accepted.
- Please use the
dev
branch as a basis. Changes fromdev
will be merged intomaster
only for new releases. - Please run the tests before committing.
- Please do not skip the pre-commit hook when committing your changes.
- If editing the README, please conform to the standard-readme specification.
For maintainers only
Please work on the dev
branch during development (or better yet, develop in a feature branch and merge into dev
when ready).
When a new release is ready (i.e. the features are finished, merged into dev
, and all tests succeed), run the included release script (replace "patch" with "minor" or "major" if necessary):
npm run release:patch
This will:
- Check that we are on
dev
- Run tests and build to make sure everything works
- Make sure
dev
is up-to-date - Run
npm version patch
(or "minor"/"major") - Ask you to confirm the version
- Push changes to
dev
- Switch to
master
- Merge changes from
dev
- Push
master
with tags - Switch back to
dev
After running this, GitHub Actions will automatically create a new GitHub Release draft. Please edit and publish the release manually.
MIT © 2019 Verbundzentrale des GBV (VZG)