This project provides:
- 2fa backend: This handles all communication related to keycloak
- 2fa frontend: This provides a self-service and an admin page and allows to reset OTP tokens
The project includes a Docker Compose setup for local development and testing.
All necessary images can be built locally from the docker/ directory.
- Docker and Docker Compose installed
- Git repository cloned locally
For all types of integration tests we need a keycloak realm which has a specific setup. The realm's export is part of this repository and is loaded by keycloak in the docker compose environment.
This section describes how to (re-)create the realm if necessary.
The steps in this sections are only required to be executed whenever we change the keycloak version and the realm we've exported under tests/integration/data/export/realm-export-with-user.json doesn't work well anymore.
Start the keycloak setup container:
docker compose up -d keycloak-setup
docker compose exec -it keycloak-setup bashStart keycloak within the setup container:
export PATH=/opt/keycloak/bin:$PATH
kc.sh start-dev- Login as
admin(pw:admin) underlocalhost:8080 - Create realm:
test-realm - Create client:
2fa-helpdesk- Activate
Direct Access Grants - Set "Valid redirect URIs":
* - Set "Valid post logout redirect URIs":
* - Set "Web origins":
*
- Activate
- Create groups:
2fa-users2FA Admins
- Create "Client Scope":
twofa-default- Create and add new mapper:
2fa Groups- Set "Claim Name":
2fa_user_groups - Turn off the "Full group path" option
- Set "Claim Name":
- Create and add new mapper:
- Add client scope
twofa-defaultto client2fa-helpdesk - Adjust "Authentication Flow":
- Deactivate OTP for
direct grant - Change
browser/Conditional OTPto required.
- Deactivate OTP for
- Create user with username
test- Add user to groups:
2fa-users2FA Admins
- Create non-temporary password:
123qwe
- Add user to groups:
Stop keycloak the keycloak setup container and export the realm (by Ctrl+C). Then export the realms to JSON:
kc.sh export --file=/opt/keycloak/data/export/realm-export.json --optimizedExit and shutdown the container:
docker compose down --timeout 0 keycloak-setupRemove the admin user from the export to avoid failures on import:
jq '.[].users |= map(select(.realmRoles | index("admin") | not))' \
tests/integration/data/export/realm-export.json \
> tests/integration/data/export/realm-export-with-user.json
rm tests/integration/data/export/realm-export.jsonThe Docker Compose configuration supports different profiles for different use cases:
To run the complete 2FA helpdesk stack with all services:
# Build all images and start the integration test environment
docker compose --profile test-it up --build
# This will start:
# - Keycloak server (localhost:8080)
# - 2FA Helpdesk Backend API (localhost:8081)# (Re-)build and run the testrunner for testing tests/integration/api-2fa
docker compose run -it --rm --build testrunner
# Alternatively: (Re-)build and run the testrunner for testing tests/integration/api-2fa
docker compose run -it --rm --build testrunner pytest -vv tests/integration/api-2fa
# Run tests from above without explicit building
docker compose run -it --rm testrunner
docker compose run -it --rm testrunner pytest -vv tests/integration/api-2fa
In case you want to build and use images named the same as the one specified in the compose file:
# Build the testrunner image
docker compose --profile test-it up --build --no-start
docker compose --profile test-it down --remove-orphans --volumes
# Run the integration tests
docker compose run -it --rm testrunner pytest -vv tests/integration/api-2faPrerequisites:
- Python 3.13
First load the pipenv environment:
cd docker/testrunner
pipenv sync
pipenv shell
cd -The execute the tests:
pytest -vv tests/integration/api-2faTo run the complete 2FA helpdesk stack with all services, to build all images and to start the e2e test environment for testing headless via docker compose:
docker compose --profile test-e2e-service up --build
# This will start:
# - Keycloak server (keycloak:8080, localhost:8080)
# - 2FA Helpdesk Backend API (api:8080, localhost:8081)
# - 2FA Helpdesk Frontend (keycloak:80)In the scenario above keycloak and frontend share the same network (similar to a sidecar container in k8s).
The reason is, that otherwise keycloak won't make the login page available and raise a "Web Crypto API is not available" error in the browser.
That's a security feature, which enforces that pure http requests for login are only supported from localhost.
Thus the e2e tests wouldn't run properly.
(see also keycloak/keycloak#36804)
To run the complete 2FA helpdesk stack with all services, to build all images and start the e2e test environment for testing directly via pytest:
docker compose --profile test-e2e-local up --build
# This will start:
# - Keycloak server (keycloak:8080, localhost:8080)
# - 2FA Helpdesk Backend API (api:8080, localhost:8081)
# - 2FA Helpdesk Frontend (frontend:80, localhost:3000)This allows also to test in headed mode which is helpful during development.
Ensure all services are started with the profile test-e2e-service (see section on E2E setup).
# (Re-)build and run the testrunner for testing tests/integration/api-2fa
docker compose run -it --rm --build testrunner
# Alternatively: (Re-)build and run the testrunner for testing tests/integration/api-2fa
docker compose run -it --rm --build testrunner pytest -vv tests/integration/e2e
# Run tests from above without explicit building
docker compose run -it --rm testrunner
docker compose run -it --rm testrunner pytest -vv tests/integration/e2e
In case you want to build and use an image named the same as the one specified in the compose file:
# Build the testrunner image
docker compose --profile test-e2e up --build --no-start
docker compose --profile test-e2e down --remove-orphans --volumes
# Run the integration tests
docker compose run -it --rm testrunner pytest -vv tests/integration/e2ePrerequisites:
- Python 3.13
First load the pipenv environment:
cd docker/testrunner
pipenv sync
pipenv shell
cd -The execute the tests:
pytest -vv tests/integration/e2e# Run the test suite
docker compose run -it --rm test-chart
# Deal with trouble via pdb
docker compose run -it --rm test-chart tests/chart --pdb
# Have a shell
docker compose run -it --rm test-chart bash
pytest tests/chartWhen running the full stack (either with profile test-it or test-e2e-services):
- Keycloak Admin Console: http://localhost:8080/admin (username: admin & password:admin)
- Backend API: Available internally to other services or access under http://localhost:8081
When running the full stack (with profile test-e2e-local):
- Keycloak Admin Console: http://localhost:8080/admin (username: admin & password:admin)
- Backend API: Available internally to other services or access under http://localhost:8081
- Frontend: Access under:
- Self-Service: http://localhost:3000/univention/2fa/self-service
- Admin Page: http://localhost:3000/univention/2fa/admin
- User credentials: User
testwith password123qwe
To force rebuild all images:
docker compose --profile test-e2e build --no-cacheTo rebuild a specific service:
docker compose build --no-cache apiCode samples
POST /token/reset/own/
Reset Own Token
Example responses
200 Response
{
"users": [
{
"keycloak_internal_id": "string",
"username": "string",
"email": "string",
"firstname": "string",
"lastname": "string"
}
],
"succes": true,
"detail": "string"
}| Status | Meaning | Description | Schema |
|---|---|---|---|
| 200 | OK | Successful Response | ListUserResponse |
Code samples
POST /token/reset/user/
Reset User Tokens
Body parameter
{
"user_ids": [
"string"
]
}| Name | In | Type | Required | Description |
|---|---|---|---|---|
| body | body | ResetUsersRequest | true | none |
Example responses
200 Response
{
"success": true,
"detail": "string",
"resets_by_user": ""
}| Status | Meaning | Description | Schema |
|---|---|---|---|
| 200 | OK | Successful Response | ResetResponse |
| 422 | Unprocessable Entity | Validation Error | HTTPValidationError |
Code samples
POST /list_users
List Users
Body parameter
{
"query": ""
}| Name | In | Type | Required | Description |
|---|---|---|---|---|
| body | body | ListUserQuery | false | none |
Example responses
200 Response
{
"users": [
{
"keycloak_internal_id": "string",
"username": "string",
"email": "string",
"firstname": "string",
"lastname": "string"
}
],
"succes": true,
"detail": "string"
}| Status | Meaning | Description | Schema |
|---|---|---|---|
| 200 | OK | Successful Response | ListUserResponse |
| 422 | Unprocessable Entity | Validation Error | HTTPValidationError |
Code samples
GET /whoami
Whoami
Example responses
200 Response
{
"token": {},
"success": true,
"twofa_admin": true
}| Status | Meaning | Description | Schema |
|---|---|---|---|
| 200 | OK | Successful Response | WhoAmIResponse |
{
"detail": [
{
"loc": [
"string"
],
"msg": "string",
"type": "string"
}
]
}
HTTPValidationError
| Name | Type | Required | Restrictions | Description |
|---|---|---|---|---|
| detail | [ValidationError] | false | none | none |
{
"query": ""
}
ListUserQuery
| Name | Type | Required | Restrictions | Description |
|---|---|---|---|---|
| query | any | false | none | Search for users matching this query |
anyOf
| Name | Type | Required | Restrictions | Description |
|---|---|---|---|---|
| » anonymous | string | false | none | none |
or
| Name | Type | Required | Restrictions | Description |
|---|---|---|---|---|
| » anonymous | null | false | none | none |
{
"users": [
{
"keycloak_internal_id": "string",
"username": "string",
"email": "string",
"firstname": "string",
"lastname": "string"
}
],
"succes": true,
"detail": "string"
}
ListUserResponse
| Name | Type | Required | Restrictions | Description |
|---|---|---|---|---|
| users | [User] | true | none | none |
| succes | boolean | true | none | none |
| detail | string | true | none | none |
{
"success": true,
"detail": "string",
"resets_by_user": ""
}
ResetResponse
| Name | Type | Required | Restrictions | Description |
|---|---|---|---|---|
| success | boolean | true | none | none |
| detail | string | true | none | none |
| resets_by_user | object | false | none | Map of usernames to reset counts |
| » additionalProperties | integer | false | none | none |
{
"user_ids": [
"string"
]
}
ResetUsersRequest
| Name | Type | Required | Restrictions | Description |
|---|---|---|---|---|
| user_ids | [string] | true | none | none |
{
"keycloak_internal_id": "string",
"username": "string",
"email": "string",
"firstname": "string",
"lastname": "string"
}
User
| Name | Type | Required | Restrictions | Description |
|---|---|---|---|---|
| keycloak_internal_id | string | true | none | none |
| username | string | true | none | none |
| any | false | none | none |
anyOf
| Name | Type | Required | Restrictions | Description |
|---|---|---|---|---|
| » anonymous | string | false | none | none |
or
| Name | Type | Required | Restrictions | Description |
|---|---|---|---|---|
| » anonymous | null | false | none | none |
continued
| Name | Type | Required | Restrictions | Description |
|---|---|---|---|---|
| firstname | any | false | none | none |
anyOf
| Name | Type | Required | Restrictions | Description |
|---|---|---|---|---|
| » anonymous | string | false | none | none |
or
| Name | Type | Required | Restrictions | Description |
|---|---|---|---|---|
| » anonymous | null | false | none | none |
continued
| Name | Type | Required | Restrictions | Description |
|---|---|---|---|---|
| lastname | any | false | none | none |
anyOf
| Name | Type | Required | Restrictions | Description |
|---|---|---|---|---|
| » anonymous | string | false | none | none |
or
| Name | Type | Required | Restrictions | Description |
|---|---|---|---|---|
| » anonymous | null | false | none | none |
{
"loc": [
"string"
],
"msg": "string",
"type": "string"
}
ValidationError
| Name | Type | Required | Restrictions | Description |
|---|---|---|---|---|
| loc | [anyOf] | true | none | none |
anyOf
| Name | Type | Required | Restrictions | Description |
|---|---|---|---|---|
| » anonymous | string | false | none | none |
or
| Name | Type | Required | Restrictions | Description |
|---|---|---|---|---|
| » anonymous | integer | false | none | none |
continued
| Name | Type | Required | Restrictions | Description |
|---|---|---|---|---|
| msg | string | true | none | none |
| type | string | true | none | none |
{
"token": {},
"success": true,
"twofa_admin": true
}
WhoAmIResponse
| Name | Type | Required | Restrictions | Description |
|---|---|---|---|---|
| token | object | true | none | none |
| success | boolean | true | none | none |
| twofa_admin | boolean | true | none | none |