---
title: Local Development With DDEV and KeyCloak
url: "https://b13.com/products/single-sign-on-for-typo3/documentation/local-development-with-ddev-and-keycloak"
date: 2026-09-10
modified: 2026-11-10
---

# Local Development With DDEV and KeyCloak

![Icon featuring a key and overlapping squares, symbolizing security or access control.](https://b13.com/fileadmin/_processed_/0/a/csm_sharing-ext-SingleSignOn-S_512e772865.webp)

  Documentation
===============

 TYPO3 Module “Single-Sign-On”
 Version 2.0.3 (04 Nov 2026)

   Chapters of this Documentation        Testing SAML needs a real identity provider—there is no offline mode and no mock. KeyCloak in DDEV is the shortest way to one, and the repository includes an example realm so you do not have to click a SAML client together by hand.

   Setup
-------

Install the KeyCloak add-on and restart:

     Copy

```
```
ddev add-on get b13/ddev-keycloak && ddev restart
```
```

  Import the example realm. The two JSON files are development fixtures in this repository (`Resources/Private/Examples/sso-example-*.json`); they are not part of the Composer package. Copy them from a checkout:

     Copy

```
```
cp /path/to/sso-authentication/Resources/Private/Examples/sso-example-*.json \
   .ddev/keycloak/import/
ddev kcctl import
```
```

  Point the backend at the matching service provider configuration:

     Copy

```
```
$GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['sso_authentication']['BackendServiceProviderConfigurationFile']
    = 'EXT:sso_authentication/Resources/Private/Examples/sso_authentication.yaml';
```
```

  Then open the backend login screen and pick the SSO entry. The example user is `benni-example` with the password `huhu2025`.

**This is a local-development shortcut, not a template for production.** The SAML client does not require a client signature, and pointing the backend at the example inside the extension means the next `composer update` replaces your configuration. Copy `sso_authentication.yaml` into your own site package for anything else.

**The realm export carries no keys.** KeyCloak generates its own signing keys on import when the file does not include them. If you export a realm of your own to share, strip `components."org.keycloak.keys.KeyProvider"` before you commit it.

   What the Example Realm Contains
---------------------------------

| Piece | Value |
|---|---|
| Realm | `sso-example` |
| SAML client | `<a href="https://${DDEV_PROJECT}.ddev.site/typo3/" rel="noreferrer" target="_blank">${DDEV_PROJECT}.ddev.site/typo3/</a>`—the DDEV project name is substituted on import, so the same file works in any project |
| Name identifier | The KeyCloak username |
| Client signature | Not required, so TYPO3 does not have to sign its requests for this realm |
| Group | `Erste Gruppe`, which `benni-example` is a member of |
| Attribute mappers | `urn:oid:1.2.840.113549.1.9.1` (email), `urn:oid:2.5.4.42` (given name), `urn:oid:2.5.4.4` (surname), and a group-membership mapper under the attribute name `bennis-group-list` |

Those four attribute names are exactly the keys the shipped `sso_authentication.yaml` maps, which is why the pair works out of the box. Note what the realm does **not** send: no tenant and no object identifier claim. So `tx_sso_tenantid` stays empty and `tx_sso_objectidentifier` falls back to the name identifier—the same situation as a typical KeyCloak setup in production, and the reason the mapped email matters for [matching an existing user](https://b13.com/products/single-sign-on-for-typo3/documentation/user-federation#c12394).

   Setting a Realm Up by Hand
----------------------------

If you would rather build it yourself, or need a second realm:

1. **Admin console** at `<a href="https://your-project.ddev.site:8443/admin" rel="noreferrer" target="_blank">your-project.ddev.site/admin</a>`, with `admin` / `password`
2. **Create a realm**—Master > Create realm
3. **Create a user**—Users > Add user, give them a password on the Credentials tab with *Temporary* off, and add them to a group under Users > *user* > Groups
4. **Create a SAML client**—Clients > Create client, type SAML:
    - Client ID: `<a href="https://your-project.ddev.site/typo3/" rel="noreferrer" target="_blank">your-project.ddev.site/typo3/</a>` for backend login, and **mind the trailing slash**—it has to match the entity ID TYPO3 sends
    - Root URL and valid redirect URIs: the same base, with `/*` for the redirect URI
    - Master SAML Processing URL: `<a href="https://your-project.ddev.site/typo3/auth/sso/acs" rel="noreferrer" target="_blank">your-project.ddev.site/typo3/auth/sso/acs</a>`
    - Turn *Client signature required* off unless you have configured `x509cert` and `privateKey` and enabled [`security.authnRequestsSigned`](https://b13.com/products/single-sign-on-for-typo3/documentation/configuration#c12338)
5. **Attribute mappers**—Client Scopes > *add predefined mapper*. Skip the role list mapper
6. **Group mapper**—Add Mapper > By Configuration > Group list. The *Group attribute name* is what your `claimsMapping` key has to be

Export before you commit anything you changed in the admin console, or the next import overwrites it:

     Copy

```
```
ddev kcctl export
ddev kcctl import
```
```

   Troubleshooting
-----------------

Check the environment variables first—a `%env(...)%` placeholder that resolves to an empty string produces an unhelpful failure a long way from its cause. Then [switch on debug logging](https://b13.com/products/single-sign-on-for-typo3/documentation/logging#c12419) and [request the metadata endpoint](https://b13.com/products/single-sign-on-for-typo3/documentation/installation#c12333); between the two, nearly every local problem is visible.

For KeyCloak’s own side, add this to `.ddev/docker-compose.keycloak.yaml`, restart, and follow the log with `ddev logs -s keycloak -f`:

     Copy

```
```
environment:
  - KEYCLOAK_LOG_LEVEL=DEBUG
```
```

  ###  Common Issues

| Symptom | Cause |
|---|---|
| `client_not_found`, or `Cannot_match_source_hash` | The client ID in KeyCloak does not match the entity ID TYPO3 sends. Almost always the trailing slash |
| `Found an Attribute element with duplicated Name` | Two mappers in the client scopes produce the same attribute name. Remove one, or set [`security.allowRepeatAttributeName`](https://b13.com/products/single-sign-on-for-typo3/documentation/configuration#c12338) if you cannot |
| The login form comes back with nothing on screen | The normal symptom of every rejected assertion. [Logging](https://b13.com/products/single-sign-on-for-typo3/documentation/logging) has the reason. Over `https` through a proxy, start at [Reverse Proxy](https://b13.com/products/single-sign-on-for-typo3/documentation/running-behind-a-reverse-proxy) |
| A new `be_users` row appears on every login | `userStoragePid` is not `0` for the backend, or nothing in the claims matches the existing user. See [User Federation](https://b13.com/products/single-sign-on-for-typo3/documentation/user-federation) |

  ###  Mapping an Attribute You Can See in KeyCloak

The keys of `claimsMapping` are the *SAML Attribute Name* shown under Clients > *your client* > Client scopes, and the values are column names of `be_users` or `fe_users`. There is no discovery step: an attribute the identity provider sends and the mapping does not name is simply ignored, which is why an attribute arriving correctly and still not landing in the record is the most common mapping mistake. Debug logging prints both the raw claims and the mapped result of every login, which is the fastest way to see which of the two is missing.