Skip to content

Authentication

OS2mo uses the OpenID Connect protocol (OIDC) for authentication. This mechanism is facilitated by Keycloak which is a powerful authentication and authorization component that enables delegation of all auth mechanisms to a separate component in the tech stack.

Authentication Flow#

The key parts of the OS2mo infrastructure relevant for the authentication flow is shown in the figure below:

image

The autentication flow is as follows:

  1. An unauthenticated user navigates to the OS2mo frontend in the browser.

  2. The frontend redirects the user to Keycloak.

  3. The user is prompted for their username and password (or redirected to an IdP if configured).

  4. The user is after successful authentication redirected back to the frontend with an OpenID Connect token issued by Keycloak.

  5. The frontend sets the Authorization header with the bearer token obtained from Keycloak on all backend requests, i.e.

    Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSld...
    
  6. The backend validates the token (using a public key from Keycloak) and sends back the requested JSON to the frontend.

  7. The user is presented with the requested data from the frontend.

The tokens are signed with a private JSON Web Key (JWK) by Keycloak, and in order to validate the token, the backend needs the public (JWK) key from Keycloak. This key can be retreived by a one-time operation, i.e. the backend does not need to call Keycloak for each request entering the backend. The Keycloak public key is only changed very rarely, so a request to Keycloak is only necessary when the public key changes.

Configuration#

This section describes the Keycloak configurations required by the backand and the frontend.

Backend#

The MO backend needs a few configurations to be able to communicate with Keycloak (which is actually only needed when the backend have to fetch the public signing key - the public JWK - from Keycloak):

keycloak_schema = "https"
keycloak_host = "keycloak"
keycloak_port = 443
keycloak_realm = "mo"
keycloak_signing_alg = "RS256"

See the corresponding TOML files for overwrites in (e.g.) the development environment.

Frontend#

The frontend has to know where to reach Keycloak in order to be able to redirect unauthenticated users to Keycloak, when they hit the frontend landing page in the browser. This is configured in a keycloak.json file served by the backend (e.g. for the development environment) http://localhost:5000/keycloak.json. The content of this file is similar to this:

{
  "realm": "mo",
  "auth-server-url": "http://localhost:8081/auth/",
  "ssl-required": "external",
  "resource": "mo",
  "public-client": true,
  "confidential-port": 0
}

The keycloak.json file is dynamically generated by the backend, based on configuration values. Currently auth-server-url and ssl-required is configurable.

In the above section describing then authentication flow, it is mentioned that a token is sent back from Keycloak to the frontend af the user has authenticated successfully. In practice, a few more requests go back and forth between the frontend and Keycloak, but this is handled automatically by the Keycloak JS Adapter used by the frontend code and this extra traffic is hence abstracted away from the developer.

Getting a token#

Depending on the context, there are two main ways we can use to retreive tokens manually from Keycloak:

  1. Requesting a token by using username/password credentials for a user in the MO realm in Keycloak.
  2. Requesting a token using a client credential for a client in the MO realm in Keycloak. This mechanism is used for backend-to-backend communication, e.g. a DIPEX client can use this method to get a token from Keycloak and pass it in on following requests to the MO backend.

By username/password#

A Python example of how to get a token from Keycloak and use this in a backend request can be is show here request is shown here

https://github.com/OS2mo/os2mo/blob/development/backend/tests/manual/keycloak-username-password.py

and the corresponding curl request is shown below. The user used in the example is Bruce Lee having the credentials username/password = bruce/bruce.

$ curl -s -X POST -d 'grant_type=password&client_id=mo&username=bruce&password=bruce' \
  "http://localhost:8081/auth/realms/mo/protocol/openid-connect/token"
{
  "access_token":"eyJhbGciOi...",
  "expires_in": 300,
  "refresh_expires_in": 1800,
  "refresh_token": "eyJhbGciOiJIUzI1NiI...",
  "token_type": "Bearer",
  "not-before-policy": 0,
  "session_state": "628f4269-63f7-48ec-816d-cb9ab4ec6abd",
  "scope": "email profile"
}

As can be seen, the payload returned from Keycloak contains the access_token, a refresh_token and some data about the token expiration time etc.

By Client Credentials#

A Python example of how to get a token from Keycloak and use this in a backend request is shown here

https://github.com/OS2mo/os2mo/blob/development/backend/tests/manual/keycloak-client-secret.py

and the corresponding curl request is shown below. The client used in this example is called dipex, and the client credential for this client is 603f1c82-d012-4d04-9382-dbe659c533fb.

$ curl -s -X POST \
  -d 'grant_type=client_credentials&client_id=dipex&client_secret=603f1c82-d012-4d04-9382-dbe659c533fb' \
  "http://localhost:8081/auth/realms/mo/protocol/openid-connect/token"
{
  "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiA...",
  "expires_in": 300,
  "refresh_expires_in": 0,
  "token_type": "Bearer",
  "not-before-policy": 0,
  "scope": "email profile"
}

Note that in this case, a refresh token is not returned to the caller (as Keycloak writes \"... The OAuth 2.0 RFC6749 Section 4.4.3 states that a refresh_token should not be generated when client_credentials grant is used.\"). If the client credentials method is needed, this has to be specifically enabled for the relevant Keycloak client in the MO realm.

Calling the backend#

Once a token has been fetched from Keycloak, it can be passed along in the requests to the backend. The Python files referenced in the section above demonstrate how to do this in Python, and a example with curl is provided here:

$ curl -i -H "Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCIgOiA..." \
"http://localhost:5000/service/o/"

Token content#

The parsed OIDC token contains among relevant information (name, username, email address, The parsed OIDC token contains relevant information about the user (name, username, email address, roles, group memberships,...). The token is parsed automatically by the libraries in the frontend and the backend, respectively, and the token info can be extracted by the application and used in whatever logic that needs to be implemented. An example token (no roles and group info) is shown here:

{
  "acr": "1",
  "allowed-origins": ["http://localhost:5001"],
  "azp": "mo",
  "email": "bruce@kung.fu",
  "email_verified": false,
  "exp": 1621779689,
  "family_name": "Lee",
  "given_name": "Bruce",
  "iat": 1621779389,
  "iss": "http://localhost:8081/auth/realms/mo",
  "jti": "25dbb58d-b3cb-4880-8b51-8b92ada4528a",
  "name": "Bruce Lee",
  "preferred_username": "bruce",
  "scope": "email profile",
  "session_state": "d94f8dc3-d930-49b3-a9dd-9cdc1893b86a",
  "sub": "c420894f-36ba-4cd5-b4f8-1b24bd8c53db",
  "typ": "Bearer"
}

Keycloak Admin Interface#

The Keycloak admin interface can be accessed on the URL path /auth, e.g. for the development environment it can be accessed at http://localhost:8081/auth. In the development environment, the credentials username/password = admin/admin can be used.

Keycloak Endpoints#

Keycloak exposes a number of relevant OIDC endpoints. A list of these can found on the URL path /auth/realms/mo/.well-known/openid-configuration, e.g. for the development environment it can be accessed at http://localhost:8081/auth/realms/mo/.well-known/openid-configuration.

Authorization#

Role-based access control (RBAC) can be enabled in MO, if this is required.

The RBAC feature is based on the owner feature of MO and the role concept in Keycloak. The systems utilizes two Keycloak roles: the \"admin\" role and the \"owner\" role (these must be created in Keycloak as described below). Access is granted to the system according to these rules:

  1. All users have read permissions to all parts of the system.
  2. Users with no role assigned do not have write permissions to any parts of the system.
  3. Users with the admin role assigned are always authorized and have write permissions to all parts of the system.
  4. A user with the owner role assigned is only authorized for write operations if the user is the owner of the org unit(s) subject to modification or one of the units ancestors.

Configuration of MO#

Set the following environment variables on the Docker container of MO to enable RBAC:

KEYCLOAK_RBAC_ENABLED: "true"

Configuration of Keycloak#

Keycloak is configured via the OS2mo Keycloak Realm Builder. The realm builder takes a number of environment variables as input and returns a Keycloak realm JSON configuration file as output. In order to enable RBAC, the following environment variable must to set on the realm builder Docker container (note: NOT the Keycloak container itself):

KEYCLOAK_RBAC_ENABLED: "true"

In the development environment, it is also necessary to

  1. Set the desired role for each user (or leave out the role attribute if no role should be set).
  2. Set a UUID on the user via the uuid Keycloak user attribute. This UUID must match the UUID of the corresponding employee in MO.

The users can be set on the realm builder container by using the KEYCLOAK_REALM_USERS environment variable e.g. like this:

KEYCLOAK_REALM_USERS: '[
  {
    "username": "eline",
    "password": "eline",
    "firstname": "Eline",
    "lastname": "Wedsgaard Christensen",
    "email": "elinec@kolding.dk",
    "uuid": "1c571f8f-0e3e-4ffa-9ff0-d35505781924",
    "roles": ["owner"],
    "enabled": true
  }
]'

In this case 1c571f8f-0e3e-4ffa-9ff0-d35505781924 is the UUID of the employee eline in MO. See a full example in the [docker-compose.yml]{.title-ref} file.

Keycloak token and RBAC#

When RBAC is enabled, the Keycloak token will contain information about the roles of the user, i.e. the decoded token will contain e.g. these attributes:

{
  "realm_access": {"roles": ["owner"]},
  "uuid": "1c571f8f-0e3e-4ffa-9ff0-d35505781924"
}

OS2mo will then (in this example) grant access according the owner priviledges in the system.

Back to top