PHPackages                             elcreator/asocialauth - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. elcreator/asocialauth

ActiveEvolutioncms-plugin

elcreator/asocialauth
=====================

Social and password authentication for Evolution CMS: every HybridAuth provider, multi-provider account linking, and e-mail/password login, registration and recovery in one configurable widget.

00PHPCI passing

Since Aug 25Pushed todayCompare

[ Source](https://github.com/elcreator/aSocialAuth)[ Packagist](https://packagist.org/packages/elcreator/asocialauth)[ RSS](/packages/elcreator-asocialauth/feed)WikiDiscussions main Synced today

READMEChangelog (1)DependenciesVersions (1)Used By (0)

aSocialAuth
===========

[](#asocialauth)

Authentication for [Evolution CMS](https://github.com/evolution-cms/evolution) 3.5+ — social, password, or both, in one widget.

- **Every provider HybridAuth ships.** All 52 of them, catalogued as an enum. Enabling one is a config entry; there is no per-provider code.
- **One account, many identities.** Sign in with Google today, add Telegram tomorrow — both open the same account. Identities are rows, not columns, so the tenth provider costs what the first did.
- **E-mail and password too.** Sign in, register, request a reset link, set a new password. A login box with nothing but third-party buttons strands everyone who uses none of them.
- **Registration without an e-mail address.** Whenever a provider returns none — Telegram, Steam, a user who withheld consent — the account still works, and the owner can add and verify an address later.
- **Configurable URLs.** Don't want `/asocialauth/`? Change one value.
- **A widget, not a fixed chrome.** Inline or modal, every hook a class, every colour a custom property, and the stylesheet can be turned off entirely.
- **Ready for whatever is in your login pipeline.** A captcha or TOTP pipe can tell a social attempt from a form post, and can ask the visitor for a code even though an OAuth callback has no form.

---

Requirements
------------

[](#requirements)

DependencyVersionPHP8.3+Evolution CMS3.5.2+hybridauth/hybridauthdev-master---

Installation
------------

[](#installation)

```
composer require elcreator/asocialauth
php artisan migrate
```

Then switch it on:

```
ASOCIALAUTH_ENABLED=true
ASOCIALAUTH_GOOGLE_ENABLED=true
ASOCIALAUTH_GOOGLE_CLIENT_ID=123456789.apps.googleusercontent.com
ASOCIALAUTH_GOOGLE_CLIENT_SECRET=GOCSPX-…
```

A **Sign in with Google** button appears on the manager login page. Register the redirect URI shown by `/asocialauth/google/callback` in the Google console and you are done.

Optional publishing:

```
php artisan vendor:publish --tag=asocialauth-config   # core/custom/config/cms/settings/aSocialAuth.php
php artisan vendor:publish --tag=asocialauth-views
php artisan vendor:publish --tag=asocialauth-lang
```

### Languages

[](#languages)

Every manager language Evolution CMS ships with is translated: `az be bg cs da de en es fa fi fr he it ja nl nn/no pl pt sv uk zh`. The active manager language is used automatically; anything else falls back to English.

---

The widget
----------

[](#the-widget)

Three snippets, all of which take `&return=` to come back to the page they sit on.

```
[[aSocialAuthLogin]]                                   sign-in: forms + provider buttons
[[aSocialAuthLogin? &mode=`modal` &trigger=`Sign in`]]  the same, behind a dialog
[[aSocialAuthLogin? &form=`register`]]                  open on the registration form
[[aSocialAuthButtons]]                                  provider buttons only
[[aSocialAuthCredentials]]                              e-mail and password only
[[aSocialAuthIdentities]]                               linked accounts, for a profile page

```

ParameterValuesMeaning`mode``inline` (default), `modal`Embedded in the page, or behind a trigger button`form``login`, `register`, `recover`, `reset`Which credential form to show`credentials``1` (default), `0`Show the e-mail/password forms`providers``1` (default), `0`Show the provider buttons`trigger`any stringModal trigger label`class`any stringExtra classes on the wrapper`return`a site-relative pathWhere to land afterwardsThe modal is progressive: with JavaScript off the dialog renders inline, so the trigger is never a dead button.

### Styling

[](#styling)

Every element carries a stable, prefixed class and every colour is a custom property, so a retint is a handful of declarations with no specificity fight:

```
.asocialauth {
    --asa-accent: #c0392b;
    --asa-radius: 4px;
    --asa-bg: #fff;
}
.asocialauth__btn--google { border-color: #4285F4; }
```

ClassElement`.asocialauth`wrapper; carries the `--asa-*` properties`.asocialauth--login` / `--identities`which widget`.asocialauth--inline` / `--modal`presentation`.asocialauth__panel`, `__inner`, `__trigger`, `__close`modal chrome`.asocialauth__btn`, `__btn--{slug}`a provider button`.asocialauth__form`, `__field`, `__label`, `__input`, `__submit`, `__links`credential forms`.asocialauth__list`, `__item`, `__unlink`linked accounts`.asocialauth__divider`, `__msg`, `__msg--error`, `__msg--ok`shared bitsStyling it yourself? Drop the packaged CSS entirely:

```
ASOCIALAUTH_INLINE_STYLES=false
```

Or replace a whole view without publishing:

```
'ui' => ['views' => ['login' => 'mytheme::auth.box']],
```

---

Configuration
-------------

[](#configuration)

### Routes

[](#routes)

```
'routes' => [
    'prefix'   => 'auth',
    'patterns' => [
        'login'    => 'in/{provider}',
        'callback' => 'in/{provider}/back',
    ],
],
```

gives `/auth/in/google` and `/auth/in/google/back`. Callback URLs are derived from the same values, so they always follow the prefix — **re-register the redirect URI in each provider's console after changing it on a live site.**

### Context

[](#context)

Evolution CMS 3.x keeps manager users and web users in one `users` table, separated only by role, so the same OAuth round trip can open either kind of session.

`context`Effect`mgr` (default)Manager session`web`Front-end session, for a member area`auto`Whichever side the flow started on### Providers

[](#providers)

The five scaffolded in the config are only a starting point. Any of the 52 slugs in `Elcreator\aSocialAuth\Enums\SocialProvider` works:

```
'providers' => [
    'discord' => [
        'enabled' => true,
        'keys'    => ['id' => env('…'), 'secret' => env('…')],
        'sort'    => 60,
    ],
],
```

A provider whose required credentials are empty stays hidden even with `enabled => true`, so a half-filled `.env` never renders a button that would fail on click. The OpenID-based providers (`openid`, `steam`, `aolopenid`, …) require no credentials at all.

Something the catalogue has never heard of — an in-house Keycloak realm, a fork:

```
'custom_providers' => [
    'acme' => [
        'enabled' => true,
        'label'   => 'Acme SSO',
        'adapter' => 'Keycloak',
        'keys'    => ['id' => '…', 'secret' => '…'],
        'extra'   => ['url' => 'https://sso.acme.test/'],
    ],
],
```

### E-mail and password

[](#e-mail-and-password)

```
'credentials' => [
    'login'       => true,   // on by default
    'register'    => false,  // opt in: installing a login widget must not open sign-ups
    'recover'     => true,
    'login_field' => 'both', // 'email' | 'username' | 'both'
    'reset_path'  => '/account/reset',
],
```

`reset_path` is the page carrying `[[aSocialAuthCredentials? &form=`reset`]]`. **Front-end recovery does nothing until you set it** — the core's `PasswordRecoveryService` only ever builds a *manager* reset link and returns early on the front end, leaving the mail to an extra. This package sends it, but it needs to know where to point.

---

How accounts work
-----------------

[](#how-accounts-work)

### Linking

[](#linking)

```
first sign-in with Google  →  social_accounts row  →  user #42
"connect Telegram" on the profile page  →  second row  →  user #42

```

Either identity now signs that user in. The widget is `[[aSocialAuthIdentities]]`; connecting goes through `/{prefix}/{provider}/link`, disconnecting through a CSRF-protected POST.

An identity already linked to someone else is **refused**, not moved — otherwise anyone who can authenticate with a provider could detach it from its owner.

By default a user cannot remove their last identity (`linking.allow_unlink_last`), because an account created socially holds no password its owner knows.

### Matching by e-mail

[](#matching-by-e-mail)

A new identity whose e-mail matches an existing user is attached to that user rather than forking a second account — but only if the provider **verified** the address (`linking.require_verified_email`, on by default). With it off, any provider that lets a user type an arbitrary e-mail into their own profile becomes a way to claim the matching Evolution CMS account.

### Registration without an e-mail

[](#registration-without-an-e-mail)

Some sign-ins simply carry no address — Telegram and Steam never send one, X only does with an approved scope, and any provider returns none for a user who withheld consent. **The trigger is the empty field in the response, never the provider's name**: nothing in the package branches on which network a profile came from. Evolution CMS requires a unique non-empty e-mail on every user, so one is synthesised:

```
telegram-a1b2c3d4e5f6g7h8@social.invalid

```

`.invalid` is reserved by RFC 2606 and can never resolve, so nothing will try to deliver there and it cannot collide with a real address. The account works normally.

### Adding and proving an address later

[](#adding-and-proving-an-address-later)

An account that started without one can fill it in from the identities widget. The claim is held in `social_email_verifications` and a confirmation link is mailed; **only following that link writes the address onto the user** and sets `verified`.

That ordering is the security of the feature, not bookkeeping. E-mail is unique across users and `match_by_email` attaches new identities to whoever holds the address, so an unproven claim in the user row would let someone collect a stranger's Google identity — and block the rightful owner from registering it.

```
'credentials' => [
    'verify_email'    => true,
    'verify_ttl'      => 1440,   // minutes
    'verify_redirect' => '/members/profile',
],
```

The link needs no page of your own: the route handles the token and redirects. Once confirmed, the account is promotable and `match_by_email` will work for it.

The mirror-image defence is `linking.require_local_verified_email` (on by default): a provider vouching for an address proves the *arriving* person owns it, not that the account already holding it does. Without this, self-registration with someone else's address would collect their identity.

### Giving a social user manager rights

[](#giving-a-social-user-manager-rights)

Yes, an administrator can — it is ordinary core behaviour. Open the user under **Users → Web user management**, pick a role, save. The form calls `\UserManager::setRole()`, the single door role changes go through, and it needs the `save_role` permission.

Two things worth knowing:

- Social accounts are created with **role 0** — "no user role", a web user with no manager access at all. Any other default would hand manager standing to whoever completes an OAuth flow.
- **A manager needs a real e-mail address.** Password recovery is the way back into a manager account and it works by e-mail; an account holding a `@social.invalid` placeholder has no route back if its provider is lost. Ask the owner to add a real address first — `Elcreator\aSocialAuth\Support\Promotion::canBecomeManager($userId)` answers this, and a warning is written to the event log if a role is granted anyway.

---

Login pipeline
--------------

[](#login-pipeline)

Evolution CMS 3.5 made the login flow chainable: ordered pipes under `cms.auth.pipeline` run around every way into a session. This package goes **through** that pipeline, not around it — `SocialAuth::signIn()` calls `\UserManager::loginById()`, so a second factor configured under `'*'` covers social sign-ins too. Enabling social login is not a way to bypass 2FA.

Attempts are tagged so a pipe can tell them apart:

KeyValue`social``true` for a provider callback, `false` for a credential form`social_provider`the provider slug`social_provider_user_id`the opaque provider identifier### Captcha

[](#captcha)

A captcha on an OAuth callback is meaningless — the provider already did the human check, and there is no form to render a challenge into. Skip it:

```
public function handle(LoginAttempt $attempt, Closure $next)
{
    if ($attempt->get('social')) {
        return $next($attempt);
    }

    return $this->verifyCaptcha($attempt) ? $next($attempt) : throw new ServiceActionException('…');
}
```

### TOTP and other interactive challenges

[](#totp-and-other-interactive-challenges)

A second-factor pipe has a real problem with social login: a password attempt arrives as a POST it can read a code from, but an OAuth callback is a GET from a third party. Throwing a plain refusal there breaks social sign-in for every account with 2FA on.

Throw `ChallengeRequiredException` instead. The callback parks the resolved identity, redirects to the page you name, and that page finishes the job:

```
// in your pipe
if ($attempt->get('social') && $attempt->get('totp') === null) {
    throw ChallengeRequiredException::at('/two-factor', 'totp');
}

// on /two-factor
$destination = SocialAuth::completePendingLogin(['totp' => $_POST['code']]);
header('Location: ' . $destination);
```

Nothing is signed in while a challenge is outstanding: `PendingLogin` holds a user id and expires in ten minutes, and the session is written only when the retried pipeline runs to completion.

---

Protocols, and the two things that silently block them
------------------------------------------------------

[](#protocols-and-the-two-things-that-silently-block-them)

HybridAuth's providers are not one protocol, and the differences are the kind that only surface once a site enables a particular one.

FamilyHow it answersWorks out of the box?OAuth 2 — Google, Facebook, Discord, GitHub, …top-level GET redirect with `?code=`yesOpenID 2.0 — Steam, AOLGET redirect (POST permitted)yes; no credentials requiredOpenID Connect / **Apple**`response_mode=form_post` → **cross-site POST**route yes, cookie **see below**Custom (Keycloak, a fork)whatever its adapter doesyes, via `custom_providers`The callback route accepts **GET and POST**, so a `form_post` provider is not 404'd. Nothing in the package branches on protocol — the adapter knows how to answer its own provider, and the package only supplies config.

**The cookie is the part you may have to change.** Evolution CMS ships `session.same_site = 'lax'`, and browsers default to the same. Under Lax a cookie *is* sent on a top-level GET redirect — so every OAuth 2 provider and Steam work untouched — but is deliberately **withheld on a cross-site POST**. Apple's callback therefore arrives with an empty session, HybridAuth cannot find the `state` it must check, and the visitor gets a generic "that attempt expired".

If you enable Apple, or any provider configured for `form_post`:

```
SESSION_SAME_SITE=none
SESSION_SECURE_COOKIE=true    # None requires Secure; HTTPS only
```

The package cannot make that change for you — it is a site-wide cookie setting — so instead it names the cause in the event log rather than leaving you with "expired".

Two things that are **not** a problem, since they come up: the routes carry no manager-auth middleware (the visitor is not signed in yet — that is the point), and the widget uses no iframe and loads no third-party asset, so `X-Frame-Options: SAMEORIGIN` and a strict CSP leave it alone.

---

Database
--------

[](#database)

Two tables. No column is ever added to `users`.

**`social_providers`** — one row per network on offer, synced from config. Metadata only; **credentials stay in config and are never written here**, because a database dump is a routine artefact and OAuth secrets have no business travelling with it.

ColumnNotes`slug``google`, `telegram`, … — also the URL segment`adapter`HybridAuth class name, or a FQCN for a custom adapter`label`, `enabled`, `sort`, `icon`presentation**`social_accounts`** — one row per identity.

ColumnNotes`provider_id`→ `social_providers.id``provider_user_id`opaque, kept as a string (Facebook ids exceed PHP's int range)`user_id`→ `users.id``email`, `name`, `avatar`, `email_verified`cached, refreshed on each sign-in`last_login_at`Unique on `(provider_id, provider_user_id)`, so an identity belongs to exactly one local user.

> Upgrading from the first release, which stored the provider as a varchar? The migration registers every slug already present (disabled), repoints the rows, and drops the old column. Nothing is lost.

---

Events
------

[](#events)

EventFired when`OnSocialAuthLogin`a social sign-in completed — `provider`, `provider_user_id`, `user_id`, `context`, `created`, `ip`, `user_agent``OnSocialAuthLink`an identity was connected to an account`OnSocialAuthUnlink`an identity was disconnected```
Event::listen('evolution.OnSocialAuthLogin', function (array $params) {
    // $params['user_id'], $params['provider'], $params['created'], …
});
```

---

Security notes
--------------

[](#security-notes)

- **Open redirects.** `&return=` accepts same-site absolute paths only; an absolute URL, a protocol-relative one, a backslash or a control character is discarded.
- **CSRF — with a caveat specific to Evolution CMS.** Every POST here is checked by the package's own `VerifyRequestToken`, *not* by the core's `csrf` alias. `EvolutionCMS\Middleware\VerifyCsrfToken` returns early when `$_SESSION['mgrValidated']` is empty — sound for the core, whose state-changing endpoints all sit behind a manager session, and wrong for this package, whose endpoints are aimed at front-end users who only ever have `webValidated`. Using the core alias would leave these routes open to cross-site POSTs while showing as protected on the route list. Concretely, that allowed a forged `credentials/email` claim → confirmation link to the attacker's mailbox → password reset → account takeover. The package's middleware reads the same `$_SESSION['_token']` the core's `csrf_token()` helper issues, so the forms need nothing special.
- **The provider legs are exempt**, in both verbs — a callback comes from a third party and cannot carry a token; HybridAuth's `state` parameter guards them instead.
- **Referer-driven context checks the host, not just the path.** Under `context: auto`, the side a flow started on is read from the `Referer`, which is chosen by whoever wrote the page the visitor clicked from. Matching on the path alone let any page at `evil.test/manager/` decide that an ordinary member's login should open a *manager* session — handing them `mgrValidated`, which the core reads as "backend user" well beyond this package (site-offline bypass, the debug-output guard in the exception handler, the document-group merge in `getUserDocGroups()`).
- **Account enumeration.** Sign-in failures, and the recovery form, give the same answer whether or not the account exists.
- **Routes 404 when disabled**, rather than erroring — off means absent, not "installed but broken".
- **Auto-registration is off by default** (`registration.enable`), and creates role 0 when on.

---

Testing
-------

[](#testing)

```
composer install
composer test
```

258 unit tests, no database or booted CMS required.

---

License
-------

[](#license)

GPL-3.0-or-later

###  Health Score

20

↑

LowBetter than 12% of packages

Maintenance65

Regular maintenance activity

Popularity0

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity11

Early-stage or recently created project

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

### Community

Maintainers

![](https://www.gravatar.com/avatar/4d36fa562f4887de196822681d345f451099c93dddef44546230176f1df051a8?d=identicon)[artur.work](/maintainers/artur.work)

---

Top Contributors

[![elcreator](https://avatars.githubusercontent.com/u/974975?v=4)](https://github.com/elcreator "elcreator (8 commits)")

### Embed Badge

![Health badge](/badges/elcreator-asocialauth/health.svg)

```
[![Health](https://phpackages.com/badges/elcreator-asocialauth/health.svg)](https://phpackages.com/packages/elcreator-asocialauth)
```

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
