Security
Form login (coreAPI)
Since version 5.8 an elasticms web application can be secured with a form login.
On submit the application will preform a coreApi login to the environment api.
!> User need be enabled and the role api is not required for logging in.
For forcing authentication on a route set default _authenticated to true. See Routing for more information. Or define a regex in
EMSCH_SECURITY_FIREWALLenv variable, this way you can protect all routes at once.create the login route
For using a different name change the environment variable EMSCH_SECURITY_ROUTE_LOGIN, default value
emsch_login.yamlemsch_login: config: controller: 'emsch.controller.security.login' path: '/{_locale}/login' requirements: { _locale: 'nl|fr' } defaults: { _locale: 'nl' } method: [POST, GET] template_static: template/login.html.twigdefine a login template
twig{% extends '@EMSCH/template/base.html.twig' %} {% trans_default_domain trans_default_domain %} {% block body %} <h1>Login</h1> {% if error %} <div class="alert alert-danger" role="alert"> {{ error.message|trans }} </div> {% endif %} {{ form_start(form, { 'attr': { 'class': 'form' } }) }} {{ form_row(form.username, { 'row_attr': { 'class': 'form-group' }, 'attr': { 'class': 'form-control' } }) }} {{ form_row(form.password, { 'row_attr': { 'class': 'form-group' }, 'attr': { 'class': 'form-control' } }) }} {{ form_row(form.submit, { 'attr': { 'class': 'btn btn-primary' } }) }} {{ form_end(form) }} {% endblock body %}define login translations
yamlemsch: security: exception: bad_credentials: 'Bad credentials!' error: 'Something went wrong!' form: username: Username password: Password submit: Login
Example using authenticated user attributes
{% if app.user %}
<ul>
<li>{{ app.user.userIdentifier }}</li>
<li>{{ app.user.email }}</li>
<li>{{ app.user.displayName }}</li>
<li>{{ app.user.circles|join(' | ') }}</li>
<li>{{ app.user.roles|join(' | ') }}</li>
{% if is_granted('ROLE_TASK_MANAGER') %}<li>Task admin</li>{% endif %}
</ul>
<a href="{{ path("emsch_logout") }}">Logout</a>
{% endif %}SSO (Single sign on)
We have two SSO authenticators SAML and OAuth2:
- if enabled the login, logout and callback routes will be registered.
- can be used together, but without login page the entry point will call the Oauth2 first
For implementing an SSO, we need a IDP (Identity Provider). Enable a dev IDP see dev-env.
Note: the current SSO implementation does only support the login. The logout on the IDP was not required.
!> If Keycloak runs on a different domain, you need to set the environment variable SESSION_COOKIE_SAMESITE to lax.
Implementation
See the demo project for a full implementation.
For forcing authentication on a route set default _authenticated to true. See Routing for more information.
Enable OAuth2 or SAML
Visit the authentication route, if both protocols are enabled only OAuth2 will be called.
Add buttons on login page (optional)
twig{% if app.user %} <h1>Welcome {{ app.user.userIdentifier }}</h1> <a href="{{ path("emsch_logout") }}">Logout</a> {% else %} {% if 'true' == app.request.server.get('EMSCH_SAML') %} <a href="{{ path('emsch_saml_login') }}">SSO SAML</a> {% endif %} {% if 'true' == app.request.server.get('EMSCH_OAUTH2') %} <a href="{{ path('emsch_oauth2_login') }}">SSO OAuth2</a> {% endif %} {% endif %}
Core Users
After a successful SSO login, the skeleton can make an API proxy authentication call. You can enable this feature by setting EMSCH_SSO_CORE_USER to true and making sure you have defined an EMS_BACKEND_API_KEY that has the ROLE_USER_MANAGEMENT grant.
If the SSO user's username or email already exists in the admin, the user's existing roles will be used.
Since version 6.9.30 we can also create users on the fly. This requires the corresponding groups to already exist in the admin, and the mapping between SSO roles and those groups to be configured via the JSON structure below in the environment variable EMSCH_SSO_CORE_USER_GROUPS.
On successful login, each expression is evaluated against the access token, and the first one that evaluates to true determines the group. Add an entry without an expression as the last item to define a default group.
The resolved group is then passed along in the proxy call. If the user doesn't exist yet, it will be created with this group; if it already exists, its email and group will be updated accordingly.
The example below checks whether webmaster, publisher, or author is present in the access token, falling back to the users group if none match.
[
{
"expression": "\"webmaster\" in (resource_access[\"demo-skeleton-oauth2\"][\"roles\"] ?? [])",
"group": "webmasters"
},
{
"expression": "\"publisher\" in (resource_access[\"demo-skeleton-oauth2\"][\"roles\"] ?? [])",
"group": "publishers"
},
{
"expression": "\"author\" in (resource_access[\"demo-skeleton-oauth2\"][\"roles\"] ?? [])",
"group": "authors"
},
{
"group": "users"
}
]EMSCH_SSO_CORE_USER_GROUPS='[{"expression": "\"webmaster\" in (resource_access[\"demo-skeleton-oauth2\"][\"roles\"] ?? [])", "group": "webmasters"}, {"expression": "\"publisher\" in (resource_access[\"demo-skeleton-oauth2\"][\"roles\"] ?? [])", "group": "publishers"}, {"expression": "\"author\" in (resource_access[\"demo-skeleton-oauth2\"][\"roles\"] ?? [])", "group": "authors"}, {"group": "users"}]'OAuth2 (OpenId connect)
OIDC is a simple identity layer built on top of OAuth 2.0, adding authentication capabilities.
Elasticms web has support for:
- Keycloak stevenmaguire/oauth2-keycloak
- Azure TheNetworg/oauth2-azure
These clients are just an implementation of thephpleague/oauth2-client
Keycloak
| Name | Description |
|---|---|
| EMSCH_OAUTH2 | bool for enabling OAUTH2 |
| EMSCH_OAUTH2_AUTH_SERVER | Keycloak server url |
| EMSCH_OAUTH2_REALM | Keycloak REALM |
| EMSCH_OAUTH2_CLIENT_ID | Keycloak client id |
| EMSCH_OAUTH2_CLIENT_SECRET | Keycloak client secret |
| EMSCH_OAUTH2_REDIRECT_URI | https://mywebsite/oauth2/connect |
| EMSCH_OAUTH2_VERSION | Optional: keycloak version |
| EMSCH_OAUTH2_ENCRYPTION_ALGORITHM | Optional: RS256,ES256,... |
| EMSCH_OAUTH2_ENCRYPTION_KEY | Optional: base64 encode |
For encryption see Identity provider (IDP) (Keycloak)
Ping Identity
| Name | Description |
|---|---|
| EMSCH_OAUTH2 | bool for enabling OAUTH2 |
| EMSCH_OAUTH2_PROVIDER | 'ping_identity' (default = 'keycloak') |
| EMSCH_OAUTH2_ISSUER | ping identity issuer url |
| EMSCH_OAUTH2_CLIENT_ID | client id |
| EMSCH_OAUTH2_CLIENT_SECRET | client secret |
| EMSCH_OAUTH2_REDIRECT_URI | https://mywebsite/oauth2/connect |
| EMSCH_OAUTH2_SCOPES | default openid profile email |
Azure
| Name | Description |
|---|---|
| EMSCH_OAUTH2 | bool for enabling OAUTH2 |
| EMSCH_OAUTH2_PROVIDER | 'azure' (default = 'keycloak') |
| EMSCH_OAUTH2_REALM | Tenant id |
| EMSCH_OAUTH2_CLIENT_ID | Client id |
| EMSCH_OAUTH2_CLIENT_SECRET | Client secret value |
| EMSCH_OAUTH2_REDIRECT_URI | https://mywebsite/oauth2/connect |
| EMSCH_OAUTH2_VERSION | Default value = 2.0 |
| EMSCH_OAUTH2_SCOPES | Default value = ["openid","profile","offline_access"] |
Create an app in the Microsoft entra admin centrum (Home > App registrations)
Tenant id & client id you should find on the overview page of the newly created app.
Client secret should be created (App registrations > my app > Certificates & secrets)
Important:
- for retrieving a
refresh_token, scope offline_access is required - add optional claim for
upn(App registrations > my app > Token configuration), because elasticms uses the upn for the username - Add yammer scopes will create a new service token: EMSCH_OAUTH2_SCOPES='["openid","profile","offline_access","https://api.yammer.com/user_impersonation"]'
Azure twig example
{% set oauth2 = app.token %}
{% set me = ems_http(
'https://graph.microsoft.com/v1.0/me',
'GET',
{'headers': { 'Authorization': "Bearer #{oauth2.token}" } }
).content|ems_json_decode %}
{% set events = ems_http(
'https://graph.microsoft.com/v1.0/me/events?$select=subject,body,bodyPreview,organizer,attendees,start,end,location',
'GET',
{ 'headers': { 'Authorization': "Bearer #{oauth2.token}" } }
).content|ems_json_decode %}
{# for yammer EMSCH_OAUTH2_SCOPES needs to contain https://api.yammer.com/user_impersonation #}
{% set yammerUser = ems_http(
'https://www.yammer.com/api/v1/users/current.json',
'GET',
{ 'headers': { 'Authorization': "Bearer #{oauth2.token('api.yammer.com')}" } }
).content|ems_json_decode %}
{% set yammerMessages = ems_http(
'https://www.yammer.com/api/v1/messages.json',
'GET',
{ 'headers': { 'Authorization': "Bearer #{oauth2.token('api.yammer.com')}" } }
).content|ems_json_decode %}SAML (Security Assertion Markup Language)
SAML is an XML-based framework for exchanging authentication and authorization data between parties.
We use the following library onelogin/php-saml
| Name | Description |
|---|---|
| EMSCH_SAML | bool for enabling SAML |
| EMSCH_SAML_SP_ENTITY_ID | unique name of our application known by the IDP |
| EMSCH_SAML_SP_PUBLIC_KEY | skeleton public key hosted /saml/metadata |
| EMSCH_SAML_SP_PRIVATE_KEY | skeleton private key |
| EMSCH_SAML_IDP_ENTITY_ID | client name in the IDP |
| EMSCH_SAML_IDP_PUBLIC_KEY | IDP public key |
| EMSCH_SAML_IDP_SSO | url to the IDP |
| EMSCH_SAML_SECURITY | overwrite settings |
