OpenID Connect (OIDC) Authentication¶
Poweradmin supports OpenID Connect (OIDC) for single sign-on (SSO) authentication with identity providers like Azure AD, Google, Keycloak, Okta, Authentik, and Auth0.
Overview¶
OIDC allows users to authenticate using their existing identity provider credentials. When enabled, users see additional "Sign in with..." buttons on the login page.
Key features:
- Automatic user provisioning from OIDC provider
- Link OIDC accounts to existing users by email
- Sync user information (name, email) from provider
- Map OIDC groups to Poweradmin permission templates
- Support for multiple providers simultaneously
- PKCE (Proof Key for Code Exchange) for enhanced security
Requirements¶
interface.application_url must be set¶
OIDC will not start without it. The OAuth redirect_uri sent to the provider is built from interface.application_url alone, and no request header is consulted. With it unset, login fails and Poweradmin reports:
Failed to initiate OIDC authentication: interface.application_url must be configured
before OIDC can be used: it defines the OAuth redirect_uri registered with the provider.
Set it to the full public URL of the install, matching the redirect URI registered with your provider:
Earlier versions derived the host from the web server's SERVER_NAME when this was empty. That fallback has been removed: under the official Docker image (FrankenPHP/Caddy) and under Apache's default UseCanonicalName Off, SERVER_NAME comes from the client's Host header, so a forged header could redirect the authorization code to another host.
Global Settings¶
| Setting | Default | Description |
|---|---|---|
oidc.enabled |
false | Enable OIDC authentication |
oidc.auto_provision |
true | Auto-create users from OIDC provider |
oidc.link_by_email |
true | Link OIDC accounts to existing users by email |
oidc.sync_user_info |
true | Sync user info (name, email) on each login |
oidc.default_permission_template |
"Guest" | Default permission template for new users |
Superuser rights are never provisioned from an identity provider¶
oidc.allow_superuser_provisioning (default false, added in 4.5.0) blocks two ways an
IdP claim could otherwise mint a global administrator:
- a
permission_template_mappingentry pointing at a template that grantsuser_is_ueberuser, and - a
group_mappingentry pointing at a Poweradmin group whose template grants it - note the installer ships anAdministratorsgroup bound to exactly such a template.
With the default in place, both are refused and logged. If your deployment genuinely
relies on the IdP deciding who is an administrator, set the flag to true; otherwise
grant administrator rights in Poweradmin itself, where an existing administrator has to
act.
Account linking and template resolution¶
oidc.default_permission_template must name a template that exists. When a new
user matches no group mapping and the named template cannot be found, provisioning
is refused rather than falling back to an arbitrary template - the lowest template
id is normally the bundled Administrator template.
oidc.link_by_email only links an incoming identity to an existing local account when:
- the ID token carries no
email_verifiedclaim, or carries one that is true. An address the provider has not vouched for is not treated as proof of identity. - the matched local account does not hold
user_is_ueberuser. A superuser account is never claimed by email; link it explicitly by subject instead.
Both checks are logged when they block a link, so a login that stops working after an upgrade can be traced in the application log.
Permission Template Mapping¶
Map OIDC groups to Poweradmin permission templates for automatic role assignment. This connects the group/role names from your OIDC token to Poweradmin's permission system.
How it works:
- When a user logs in via OIDC, Poweradmin reads their groups from the token claim specified by
user_mapping.groups(defaults togroups) - Each group name is checked against the keys in
permission_template_mapping - The first match determines the user's permission template
- If no groups match and the user's template was previously assigned via SSO group mapping, it is revoked and replaced with
default_permission_template - If no groups match and the template was manually assigned by an admin, it is preserved
- The
default_permission_templateis only applied to new users during initial provisioning, not on every login
Note: Matching is case-sensitive and exact - the group name in the token must match the mapping key exactly.
'oidc' => [
'enabled' => true,
'default_permission_template' => 'Guest',
'permission_template_mapping' => [
'poweradmin-admins' => 'Administrator',
'dns-operators' => 'Viewer',
'dns-viewers' => 'Guest',
],
],
In this example, a user whose OIDC token contains the group poweradmin-admins receives the "Administrator" permission template. A user with no matching groups receives the "Guest" template.
Predefined permission templates:
- Administrator - Full administrative rights
- Viewer - Read-only access to own zones
- Guest - Temporary access with no permissions (awaiting approval)
Using custom group claim names¶
By default, Poweradmin reads groups from the groups claim in the OIDC token. If your identity provider uses a different claim name (e.g., roles, realm_roles, or memberOf), configure it in user_mapping.groups:
'providers' => [
'keycloak' => [
// ... other settings ...
'user_mapping' => [
// ... other mappings ...
'groups' => 'roles', // Read groups from the 'roles' claim instead
],
],
],
For Docker deployments using the generic provider, set PA_OIDC_GENERIC_GROUPS_ATTR:
End-to-end example: Keycloak with roles¶
Suppose your Keycloak access token includes a roles claim:
To map dns-admin to the "Administrator" template:
'oidc' => [
'enabled' => true,
'default_permission_template' => 'Guest',
'permission_template_mapping' => [
'dns-admin' => 'Administrator',
'dns-viewer' => 'Viewer',
],
'providers' => [
'keycloak' => [
// ... other settings ...
'user_mapping' => [
'username' => 'preferred_username',
'email' => 'email',
'first_name' => 'given_name',
'last_name' => 'family_name',
'display_name' => 'name',
'groups' => 'roles', // Tell Poweradmin to read the 'roles' claim
],
],
],
],
Since the user has both dns-admin and dns-viewer, the first match in the mapping order wins - they get the "Administrator" template.
Group Membership Mapping¶
Separate from permission templates, you can also map OIDC groups to Poweradmin groups. This controls zone ownership and access through group membership.
Key differences from permission template mapping:
permission_template_mappingassigns one permission template per usergroup_mappingassigns multiple Poweradmin groups per user
'oidc' => [
'enabled' => true,
'group_mapping' => [
'external-admins' => 'Administrators',
'dns-managers' => 'Zone Managers',
'dns-editors' => 'Editors',
'dns-viewers' => 'Viewers',
'dns-guests' => 'Guests',
],
],
A single OIDC group can also be mapped to multiple Poweradmin groups by giving an array as the value (added in 4.4.0). The user is added to every Poweradmin group listed:
'group_mapping' => [
'team1' => ['Editors', 'Viewers'],
'team2' => ['Editors'],
'platform-admins' => 'Administrators', // single-value form still works
],
Predefined Poweradmin groups:
- Administrators - Full administrative access to all system functions
- Zone Managers - Full zone management including creation, editing, and deletion
- Editors - Edit zone records but cannot modify SOA and NS records
- Viewers - Read-only access to zones with search capability
- Guests - Temporary group with no permissions (awaiting approval)
Note: Both
permission_template_mappingandgroup_mappingread from the same token claim specified byuser_mapping.groups. Group memberships are also re-evaluated on every login.Important: Group mapping only works if the provider actually returns a groups claim. Many providers (SimpleSAML, Keycloak, Okta, and most generic OIDC providers) only include it when the
groupsscope is explicitly requested - add it to that provider'sscopeslist (see Scopes). Azure AD and Google surface group/role membership through other mechanisms; see their provider sections.
The scopes field lives on the individual provider, not on the top-level oidc block:
'oidc' => [
'enabled' => true,
'group_mapping' => [
'dns-managers' => 'Zone Managers',
'dns-editors' => 'Editors',
],
'providers' => [
'simplesaml' => [
// ... client_id, client_secret, endpoints/discovery ...
'scopes' => 'openid profile email groups', // 'groups' required for the groups claim
],
],
],
Provider Configuration¶
Each provider requires specific configuration. All providers share these common fields:
| Field | Required | Description |
|---|---|---|
name |
Yes | Display label for the provider. The provider identifier is the array key, not this field |
display_name |
Yes | Text shown on login button |
client_id |
Yes | OAuth client ID from provider |
client_secret |
Yes | OAuth client secret from provider |
auto_discovery |
No | Use OpenID Connect Discovery (default: false). When disabled, authorize_url, token_url and userinfo_url are all required or the provider is rejected |
metadata_url |
Conditional | OpenID Configuration endpoint (if auto_discovery) |
scopes |
No | OAuth scopes (default: openid profile email) |
logout_url |
No | Provider logout endpoint |
response_mode |
No | How the provider returns the authorization code: query (default) or form_post |
user_mapping |
No | Map OIDC claims to user fields |
Response mode (response_mode)¶
By default the provider returns the authorization code in the callback URL query
string (response_mode = query). Setting response_mode to form_post asks the
provider to return the code in an HTTP POST body instead, so it never appears in
URLs, proxy logs, or browser history.
Notes:
- Default is
query; existing deployments are unaffected until you opt in. form_postrequires HTTPS (the callback runs over a cross-site POST that needs a secure cookie). It is also allowed onlocalhostfor development. On a plain-HTTP deployment Poweradmin logs a warning and falls back toquery.- Set
interface.application_urlto your external HTTPS URL when Poweradmin runs behind a TLS-terminating proxy, so the HTTPS requirement is judged by the browser-facing URL rather than the backend request. - Works with any provider that supports the OpenID Connect Form Post Response Mode
(Azure AD, Keycloak, Okta, Auth0, and others). PKCE (S256) is always used
regardless of
response_mode.
Scopes (scopes)¶
Each provider's scopes string controls which OAuth scopes Poweradmin requests at login. The default is openid profile email, which does not include group information.
If you use group_mapping or permission_template_mapping, the provider must return a groups claim, and many providers only include it when a groups scope is requested explicitly. Add it to that provider's scopes:
Note:
groupsis not a universal scope. SimpleSAML, Keycloak, and Okta expose group membership through it, but Azure AD and Google do not - they surface groups/roles via provider-specific configuration (see their sections below). The scope name is also independent of the claim name, which you set withuser_mapping.groups(defaultgroups).
Azure AD (Microsoft)¶
- Register an application in Azure Portal > App registrations
- Add a redirect URI:
https://your-poweradmin.com/oidc/callback - Create a client secret
-
Configure optional claims for groups if needed
'oidc' => [ 'enabled' => true, 'providers' => [ 'azure' => [ 'name' => 'Microsoft Azure AD', 'display_name' => 'Sign in with Microsoft', 'client_id' => 'your-client-id', 'client_secret' => 'your-client-secret', 'tenant' => 'your-tenant-id', // or 'common' for multi-tenant 'auto_discovery' => true, 'metadata_url' => 'https://login.microsoftonline.com/{tenant}/v2.0/.well-known/openid-configuration', 'logout_url' => 'https://login.microsoftonline.com/{tenant}/oauth2/v2.0/logout', 'scopes' => 'openid profile email', 'user_mapping' => [ 'username' => 'email', 'email' => 'email', 'first_name' => 'given_name', 'last_name' => 'family_name', 'display_name' => 'name', 'groups' => 'groups', ], ], ], ],
Note: For group claims, configure "Group claims" in Azure AD App > Token configuration.
Google¶
- Go to Google Cloud Console > APIs & Services > Credentials
- Create an OAuth 2.0 Client ID
-
Add authorized redirect URI:
https://your-poweradmin.com/oidc/callback'oidc' => [ 'enabled' => true, 'providers' => [ 'google' => [ 'name' => 'Google', 'display_name' => 'Sign in with Google', 'client_id' => 'your-client-id.apps.googleusercontent.com', 'client_secret' => 'your-client-secret', 'auto_discovery' => true, 'metadata_url' => 'https://accounts.google.com/.well-known/openid-configuration', 'logout_url' => 'https://accounts.google.com/logout', 'scopes' => 'openid profile email', 'user_mapping' => [ 'username' => 'email', 'email' => 'email', 'first_name' => 'given_name', 'last_name' => 'family_name', 'display_name' => 'name', ], ], ], ],
Keycloak¶
- Create a client in Keycloak Admin Console
- Set Access Type to "confidential"
- Add valid redirect URI:
https://your-poweradmin.com/oidc/callback -
Enable "groups" scope if using group mapping
'oidc' => [ 'enabled' => true, 'providers' => [ 'keycloak' => [ 'name' => 'Keycloak', 'display_name' => 'Sign in with Keycloak', 'client_id' => 'poweradmin', 'client_secret' => 'your-client-secret', 'base_url' => 'https://keycloak.example.com', 'realm' => 'master', 'auto_discovery' => true, 'metadata_url' => '{base_url}/realms/{realm}/.well-known/openid-configuration', 'logout_url' => '{base_url}/realms/{realm}/protocol/openid-connect/logout', 'scopes' => 'openid profile email groups', 'user_mapping' => [ 'username' => 'preferred_username', 'email' => 'email', 'first_name' => 'given_name', 'last_name' => 'family_name', 'display_name' => 'name', 'groups' => 'groups', ], ], ], ],
Okta¶
- Create an OIDC application in Okta Admin Console
- Set Sign-in redirect URI:
https://your-poweradmin.com/oidc/callback -
Note your Okta domain (e.g., your-org.okta.com)
'oidc' => [ 'enabled' => true, 'providers' => [ 'okta' => [ 'name' => 'Okta', 'display_name' => 'Sign in with Okta', 'client_id' => 'your-client-id', 'client_secret' => 'your-client-secret', 'domain' => 'your-org.okta.com', 'auto_discovery' => true, 'metadata_url' => 'https://{domain}/.well-known/openid-configuration', 'logout_url' => 'https://{domain}/oauth2/v1/logout', 'scopes' => 'openid profile email groups', 'user_mapping' => [ 'username' => 'preferred_username', 'email' => 'email', 'first_name' => 'given_name', 'last_name' => 'family_name', 'display_name' => 'name', 'groups' => 'groups', ], ], ], ],
Authentik¶
- Create an OAuth2/OpenID Provider in Authentik
- Create an Application linked to the provider
-
Add redirect URI:
https://your-poweradmin.com/oidc/callback'oidc' => [ 'enabled' => true, 'providers' => [ 'authentik' => [ 'name' => 'Authentik', 'display_name' => 'Sign in with Authentik', 'client_id' => 'your-client-id', 'client_secret' => 'your-client-secret', 'base_url' => 'https://authentik.example.com', 'application_slug' => 'poweradmin', 'auto_discovery' => true, 'metadata_url' => '{base_url}/application/o/{application_slug}/.well-known/openid-configuration', 'logout_url' => '{base_url}/application/o/{application_slug}/end-session/', 'scopes' => 'openid profile email', 'user_mapping' => [ 'username' => 'preferred_username', 'email' => 'email', 'first_name' => 'given_name', 'last_name' => 'family_name', 'display_name' => 'name', 'groups' => 'groups', ], ], ], ],
Auth0¶
- Create an application in Auth0 Dashboard
- Set Application Type to "Regular Web Application"
-
Add callback URL:
https://your-poweradmin.com/oidc/callback'oidc' => [ 'enabled' => true, 'providers' => [ 'auth0' => [ 'name' => 'Auth0', 'display_name' => 'Sign in with Auth0', 'client_id' => 'your-client-id', 'client_secret' => 'your-client-secret', 'domain' => 'your-tenant.auth0.com', 'auto_discovery' => true, 'metadata_url' => 'https://{domain}/.well-known/openid-configuration', 'logout_url' => 'https://{domain}/v2/logout', 'scopes' => 'openid profile email', 'user_mapping' => [ 'username' => 'nickname', 'email' => 'email', 'first_name' => 'given_name', 'last_name' => 'family_name', 'display_name' => 'name', 'groups' => 'groups', ], ], ], ],
Generic OIDC Provider¶
For providers not listed above, use manual endpoint configuration:
'oidc' => [
'enabled' => true,
'providers' => [
'custom' => [
'name' => 'Custom OIDC',
'display_name' => 'Sign in with SSO',
'client_id' => 'your-client-id',
'client_secret' => 'your-client-secret',
'auto_discovery' => false,
'authorize_url' => 'https://provider.example.com/oauth/authorize',
'token_url' => 'https://provider.example.com/oauth/token',
'userinfo_url' => 'https://provider.example.com/oauth/userinfo',
'logout_url' => 'https://provider.example.com/oauth/logout',
'scopes' => 'openid profile email', // add 'groups' if your provider requires it for group claims
'user_mapping' => [
'username' => 'preferred_username',
'email' => 'email',
'first_name' => 'given_name',
'last_name' => 'family_name',
'display_name' => 'name',
'groups' => 'groups',
],
],
],
],
User Mapping¶
The user_mapping array maps OIDC claims to Poweradmin user fields:
| Poweradmin Field | Common OIDC Claims | Description |
|---|---|---|
username |
preferred_username, email | User's login name |
email |
Email address | |
first_name |
given_name | First name |
last_name |
family_name | Last name |
display_name |
name | Display name |
groups |
groups | Group memberships |
avatar |
picture | Profile picture URL |
Docker Configuration¶
Use environment variables with the PA_OIDC_ prefix:
The entrypoint can generate three OIDC providers: azure (PA_OIDC_AZURE_*), google (PA_OIDC_GOOGLE_*) and generic (PA_OIDC_GENERIC_*). Any other IdP - Keycloak, Authentik, Okta - is configured through the generic provider:
environment:
PA_OIDC_ENABLED: "true"
PA_OIDC_AUTO_PROVISION: "true"
PA_OIDC_DEFAULT_PERMISSION_TEMPLATE: "Guest"
PA_OIDC_GENERIC_ENABLED: "true"
PA_OIDC_GENERIC_NAME: "Keycloak"
PA_OIDC_GENERIC_DISPLAY_NAME: "Sign in with Keycloak"
PA_OIDC_GENERIC_CLIENT_ID: "poweradmin"
PA_OIDC_GENERIC_CLIENT_SECRET: "your-secret"
PA_OIDC_GENERIC_AUTO_DISCOVERY: "true"
PA_OIDC_GENERIC_METADATA_URL: "https://keycloak.example.com/realms/master/.well-known/openid-configuration"
Note: There are no
PA_OIDC_KEYCLOAK_*variables. To run a named Keycloak provider alongside another IdP, configure it inconfig/settings.phpinstead - the entrypoint only writes the three providers above.
To change the groups claim name for the generic OIDC provider, use PA_OIDC_GENERIC_GROUPS_ATTR:
Note: The
permission_template_mappingandgroup_mappingsettings can be configured via environment variables using the=delimiter and comma-separated entries:PA_OIDC_PERMISSION_TEMPLATE_MAPPING: "admins=Administrator,editors=Viewer" PA_OIDC_GROUP_MAPPING: "admins=Administrators,editors=Editors"Group names containing colons (e.g., SAML URNs) are supported. Whitespace around commas and delimiters is trimmed automatically.
For 1:n group mappings, separate the Poweradmin groups with a pipe (
|) so one OIDC group can grant access to several Poweradmin groups (added in 4.4.0):
For secrets, use the __FILE suffix:
secrets:
oidc_client_secret:
file: ./secrets/oidc_client_secret.txt
services:
poweradmin:
environment:
PA_OIDC_GENERIC_CLIENT_SECRET__FILE: /run/secrets/oidc_client_secret
secrets:
- oidc_client_secret
Multiple Providers¶
You can configure multiple OIDC providers. Users will see all enabled providers on the login page:
'oidc' => [
'enabled' => true,
'providers' => [
'azure' => [
// Azure AD configuration...
],
'google' => [
// Google configuration...
],
],
],
Security Considerations¶
- Use HTTPS: OIDC requires HTTPS for redirect URIs in production
- Protect client secrets: Use Docker secrets or environment variables, never commit secrets to version control
- Validate redirect URIs: Configure exact redirect URIs in your identity provider
- PKCE: Poweradmin uses PKCE (Proof Key for Code Exchange) automatically for enhanced security
- State validation: CSRF protection via state parameter is enabled by default
Troubleshooting¶
Login redirects fail¶
- Verify the redirect URI in your provider matches exactly:
https://your-poweradmin.com/oidc/callback - Check that your Poweradmin URL is accessible from the user's browser
- For reverse proxy setups, ensure
X-Forwarded-Protoheader is set
User not created after login¶
- Check
auto_provisionis enabled - Verify the user mapping includes required fields (username, email)
- Check application logs for provisioning errors
Groups not mapped¶
- Ensure the
groupsscope is requested (some providers require it explicitly) - Verify your provider returns groups in the expected claim - decode your token at jwt.io to inspect the actual claim names and values
- Check that
user_mapping.groupsmatches your provider's claim name exactly (e.g.,rolesinstead ofgroupsfor some Keycloak configurations) - Verify that the group names in
permission_template_mappingmatch the token values exactly (matching is case-sensitive) - Poweradmin checks both the userinfo endpoint and the ID token for groups - if your provider only includes groups in the ID token (common with Azure AD), this is handled automatically
- In Azure AD, configure Group claims in Token configuration
- In Keycloak, if using realm roles instead of groups, create a protocol mapper to include roles in the token under a custom claim name, then set
user_mapping.groupsto that claim name
"Invalid state" error¶
- Clear browser cookies and try again
- Check session configuration in Poweradmin
- Verify server time is synchronized (NTP)
Provider discovery fails behind a web proxy¶
If your Poweradmin host can only reach the internet through an HTTP proxy, OIDC discovery (the .well-known/openid-configuration fetch) will time out unless the proxy is exported in the environment. From v4.4.0, the discovery client honors the standard HTTPS_PROXY / http_proxy variables. Set them where PHP can see them - typically in the systemd unit, the FPM pool, or the Docker environment:
Restart the web server (or php-fpm) so the new environment is picked up. Earlier versions ignored these variables and required a manual cURL build with proxy support.