magenxcommerce / module-social-login-graph-ql
Headless social-login bridge: a secret-gated GraphQL mutation that verifies a provider-signed OpenID Connect ID token (Google, Apple) against the issuer's JWKS and exchanges it for a Magento customer token, find-or-creating the customer by the token's verified email.
Package info
github.com/magenxcommerce/module-social-login-graph-ql
Type:magento2-module
pkg:composer/magenxcommerce/module-social-login-graph-ql
Requires
- php: ~8.3.0||~8.4.0||~8.5.0
- firebase/php-jwt: ^7.1
- magento/framework: *
- magento/module-customer: *
- magento/module-graph-ql: *
- magento/module-integration: *
- magento/module-store: *
- magento/module-store-graph-ql: *
- psr/log: *
Requires (Dev)
None
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
Built with AI assistance. This module is built with significant use of Claude Code, under active human review and guidance. It is open source and in active development. AI assistance does not lower the engineering bar: every change must meet the same correctness, security, documentation and maintainability requirements as any other contribution.
No warranty. We are not responsible for any incompatibility. By installing this software, you accept responsibility for its security, upgrades, patches and monitoring.
A minimal, headless-friendly social-login bridge for Magento.
Stock Magento GraphQL can only mint a customer token from an email and
password (generateCustomerToken). Social login produces a verified
identity, not a password, so there is no stock way to turn "this is a verified
Google user" into a Magento session. This companion module fills exactly that
gap with a single mutation — nothing more. (Same "extend with GraphQL beside the
core module" pattern as Magenx_ProductAlertGraphQl.)
What it adds
mutation { socialLogin(input: { secret: "…", idToken: "eyJhbGciOi…" }) { token created } }
The resolver:
- Verifies the shared
secret(constant-time) — fails closed if none configured. - Verifies the
idTokenagainst the issuer's own published signing keys: signature,iss,aud(plusazpfor a multi-audience token),exp/iatwith 60s of clock leeway, andemail_verified. Google and Apple ship configured; more issuers are adi.xmlentry. - Finds the customer by the token's verified email on the request's
website, or creates one (random password, name from the token's
given_name/family_name). One call therefore covers both register and login. - For an existing account, refuses to mint a token when the account is locked out or still awaiting email confirmation — the same guard rails password sign-in applies. Accounts this mutation creates itself skip the confirmation check, since the provider has already verified the address.
- Returns a Magento customer access token (via the same Integration token model
generateCustomerTokenuses internally).
The optional firstname / lastname inputs are a display-name fallback
only, used when creating an account whose token carries no name claims — Apple
puts the name in the first authorization callback rather than in the ID token.
They never influence which account is matched, so they carry no authority.
Security model
Two independent checks guard this mutation, and they answer different questions.
The shared secret answers "is this my broker calling?" Magento's /graphql
may be reachable outside the storefront's Next.js proxy — directly (if nginx
exposes a public /graphql location) or from anything on the internal network.
The storefront's persisted-query allowlist is not a boundary here: it only
guards the /api/graphql proxy, and this call deliberately bypasses it. Without
the secret, anyone who can reach the endpoint could create customer records at
will. The resolver fails closed when no secret is configured and compares
with hash_equals.
The ID token answers "does this person own this email?" Magento verifies it itself, against keys fetched from the provider, so the broker is not trusted to assert an identity — it only relays what the provider signed. This is what keeps a leaked secret from being account takeover: the holder still cannot produce a Google or Apple signature over an email they do not control. The email is read from the verified claims and nowhere else.
Neither check replaces the other, so keep both:
| Shared secret | ID token verification | |
|---|---|---|
| Answers | Is this my broker? | Does this person own this email? |
| If it alone is defeated | Attacker can still only sign in as themselves | Attacker cannot reach the mutation at all |
Still the caller's job, because Magento cannot see it: completing the OAuth
round-trip, checking the state parameter, and binding the nonce it sent to
the nonce claim in the token it got back.
The secret must only ever live on the broker's server — never expose it or this mutation to the browser. A secret in a client bundle is a public constant.
Rejected calls (wrong secret, unverified or expired token, wrong audience) are
logged as warnings with the store code and the reason, and with no PII, token
material or secret material. Every rejection answers the caller with the same
Not authorized., so the mutation cannot be used as an oracle; var/log/ is
where the actual reason is.
Configuration
Set the shared secret and each provider's OAuth client id. Either as store config — Stores > Configuration > Magenx > Social Login, or the same three paths from the CLI:
bin/magento config:set magenx_social_login/general/shared_secret <secret>
bin/magento config:set magenx_social_login/general/google_client_id <client-id>.apps.googleusercontent.com
bin/magento config:set magenx_social_login/general/apple_client_id com.example.storefront
The secret field is encrypted on save, so a value entered in the admin is
stored as ciphertext; one written straight to core_config_data as plain text
is still read correctly, so an existing row keeps working.
Or as environment variables, which take precedence and keep the values out of the database:
MAGENX_SOCIAL_LOGIN_SECRET=<secret>
MAGENX_SOCIAL_LOGIN_GOOGLE_CLIENT_ID=<client-id>.apps.googleusercontent.com
MAGENX_SOCIAL_LOGIN_APPLE_CLIENT_ID=com.example.storefront
Each value is read from getenv(), $_ENV and $_SERVER, so any of the ways
a Magento host passes environment through works — including an nginx
fastcgi_param, which is what to use when the php-fpm pool keeps the default
clear_env = yes and strips the worker environment:
fastcgi_param MAGENX_SOCIAL_LOGIN_GOOGLE_CLIENT_ID "<client-id>.apps.googleusercontent.com";
The client id must be the same one the broker used to obtain the token, or
the aud check rejects it. A comma-separated list is accepted, for a store that
serves sibling clients (web, iOS, Android) from one provider. An issuer with no
client id configured rejects every token — including one that is otherwise
perfectly valid — so both halves of a provider's setup must be present.
Store-config fallback
Each value also falls back to store config, read in store scope so a
multi-site instance can hold a different value per store view. The module ships
no system.xml yet, so these paths are not in the admin and bin/magento config:set rejects them ("The path doesn't exist") — the only way to populate
them today is SQL:
INSERT INTO core_config_data (scope, scope_id, path, value) VALUES ('default', 0, 'magenx_social_login/general/google_client_id', '<client-id>.apps.googleusercontent.com');
followed by bin/magento cache:clean config. An environment variable takes
precedence over a row like this, so a stale variable will shadow it.
Outbound network access
Verification needs the issuer's JWKS, so the Magento container must be able to
reach https://www.googleapis.com/oauth2/v3/certs and
https://appleid.apple.com/auth/keys over HTTPS. The key set is cached for the
lifetime the provider's Cache-Control header asks for (clamped to 5 min – 24 h),
and an expired set is kept for a further 24 hours and served if the provider is
unreachable — a transient outage degrades to slightly stale keys rather than to
nobody being able to sign in. A token naming a key id that is not in the cached
set triggers one forced refresh, so a key rotation costs a single extra
round-trip rather than a wave of failures.
Adding another provider
Declare a virtual type in etc/di.xml with the provider's exact iss values
and JWKS URL, add it to IssuerRegistry, and configure
magenx_social_login/general/<code>_client_id. No PHP changes.
Install
bin/magento module:enable Magenx_SocialLoginGraphQl
bin/magento setup:upgrade