diff --git a/README.md b/README.md
index 4caf9fe..8544b2d 100644
--- a/README.md
+++ b/README.md
@@ -1,56 +1,170 @@
-# PresideCMS Extension: Social Media Login
+# PresideCMS Social Login
-This is an extension for [PresideCMS](http://github.com/pixl8/Preside-CMS/) that enables signing up / logging in using third party/social media login such as Facebook, Twitter and Google
+This PresideCMS extension supports secure website-user login with:
-## How it works
+- Facebook Login
+- Google OpenID Connect
+- X OAuth 2.0
+- Sign in with LinkedIn using OpenID Connect
-The extension extends the current Login functionality for Website users in preside to enable them to sign up or login using social media credentials.
+The implementation uses authorization-code flows, one-time session-bound `state`, PKCE where the provider supports it, short callback lifetimes, server-side token exchange, and explicit account linking.
-You will need to create an application/project on the social media platform from the steps below to obtain API keys for respective platform. The key can be then keyed in in the Social Media Login settings under System > Settings in preside.
+## Requirements
-### Facebook
+- A PresideCMS application with website users enabled
+- HTTPS for every non-local environment
+- A provider application for each enabled login option
+- A stable public hostname; callback URLs must match exactly
-Create a Facebook application at https://developers.facebook.com/apps/. The crendentials for Facebook can be obtained from the Facebook application settings.
+Twitter4j is no longer used or required.
-### Twitter
+## Install
-Create a new Twitter application at https://apps.twitter.com. The credentials for Twitter can be obtained from your Twitter application's Key and Access Tokens
+From the root of the Preside application:
-### Google
+```bash
+box install seakchiew/preside-ext-social-login
+```
-Create a new project at https://console.developers.google.com/ and create a new OAuth 2.0 client ID and an API Key under credentials.
+Alternatively, install this repository under:
-## Installation
+```text
+application/extensions/preside-ext-social-login
+```
-Install the extension to your application via either of the methods detailed below (Git submodule / CommandBox) and then enable the extension by opening up the Preside developer console and entering:
+Enable and reload it in the Preside developer console:
- extension enable preside-ext-social-login
- reload all
+```text
+extension enable preside-ext-social-login
+reload all
+```
-### Git Submodule method
+Run the Preside database synchronisation after upgrading. Version 1 adds `label` and `email_verified` to `social_account` and creates a unique `(type, social_id)` identity constraint.
-From the root of your application, type the following command:
+## Callback URLs
- git submodule add https://github.com/seakchiew/preside-ext-social-login.git application/extensions/preside-ext-social-login
+Replace `https://example.com` with the canonical site URL. Register each URL exactly, including its query string:
-### CommandBox (box.json) method
+```text
+https://example.com/login/auth/?type=facebook
+https://example.com/login/auth/?type=google
+https://example.com/login/auth/?type=twitter
+https://example.com/login/auth/?type=linkedin
+```
-From the root of your application, type the following command:
+Do not register wildcard callback URLs. If a provider normalises away the slash before `?`, verify the URL emitted by the running site and register that exact value.
- box install seakchiew/preside-ext-social-login
+## Preside configuration
-# Reference
+Open **System → Settings → Social login**.
-This script is based on multiple login script by cfjquery - https://github.com/cfjquery/multiLogin
+1. Enter the client ID and client secret for a provider.
+2. Enable that provider.
+3. Save the settings.
+4. Reload the login page and confirm the provider button appears.
+A provider button remains hidden when its toggle is enabled but either credential is blank. Restrict access to the settings category because provider secrets are stored in Preside system configuration.
-## Dependency
+## Facebook
-The Twitter API authentication uses Twitter4j java library
-Please download the library from http://twitter4j.org/en/index.html and place twitter4j-core.*.jar into /WEB-INF/lib/ folder in your application
+1. Open [Meta for Developers](https://developers.facebook.com/apps/) and create an app.
+2. Add the **Facebook Login** product and select the web platform.
+3. Under Facebook Login settings, add the Facebook callback URL to **Valid OAuth Redirect URIs**.
+4. Request the `email` and `public_profile` permissions. Complete App Review if Meta requires it for the app’s audience.
+5. Keep **Client OAuth Login**, **Web OAuth Login**, and strict redirect URI matching enabled.
+6. Copy the app ID and app secret into the Facebook tab in Preside.
+7. Enable Facebook login.
+The extension targets Graph API `v25.0`, sends PKCE and `state`, uses the bearer-token header for `/me`, and signs the profile request with `appsecret_proof`.
-
+## Google
+1. Open [Google Cloud Console](https://console.cloud.google.com/apis/credentials).
+2. Configure the OAuth consent screen.
+3. Create an **OAuth client ID** with application type **Web application**.
+4. Add the Google callback URL under **Authorized redirect URIs**.
+5. Add production domains and test users as required by the consent-screen publishing status.
+6. Copy the client ID and client secret into the Google tab in Preside.
+7. Enable Google login.
+The requested scopes are `openid email profile`. The extension uses Google’s current authorization, token, and OpenID Connect UserInfo endpoints.
+## X
+
+1. Open the [X Developer Portal](https://developer.x.com/) and create or select a project and app.
+2. Enable **OAuth 2.0** user authentication.
+3. Select a confidential **Web App** client.
+4. Add the X callback URL.
+5. Add the site URL as the website URL.
+6. Grant the `tweet.read` and `users.read` scopes.
+7. Copy the OAuth 2.0 client ID and client secret into the X tab in Preside.
+8. Enable X login.
+
+Legacy OAuth 1.0a API keys are not compatible. Upgrades must enter new OAuth 2.0 client credentials. X does not return an email for this flow; the extension uses the stable X user ID as the website-user login ID and a unique, non-deliverable `@social-login.invalid` address until the application collects and verifies a local address.
+
+## LinkedIn
+
+1. Open [LinkedIn Developers](https://www.linkedin.com/developers/apps) and create an app.
+2. Associate and verify the app with a LinkedIn Page.
+3. On **Products**, request **Sign in with LinkedIn using OpenID Connect**.
+4. Add the LinkedIn callback URL under **Authorized redirect URLs for your app**.
+5. Confirm the app has `openid`, `profile`, and `email`.
+6. Copy the client ID and client secret into the LinkedIn tab in Preside.
+7. Enable LinkedIn login.
+
+LinkedIn’s web OIDC endpoint does not consistently accept PKCE parameters. This provider therefore uses a confidential-client authorization-code flow with an exact callback URI, one-time `state`, a client secret, and a ten-minute local flow lifetime.
+
+## Explicitly link an existing account
+
+Social login never attaches an identity to an existing website user merely because email addresses match. The user must first sign in normally and submit a CSRF-protected link request.
+
+Add a form like this to the authenticated account page for each enabled provider:
+
+```cfm
+
+
+
+```
+
+Valid `type` values are `facebook`, `google`, `twitter`, and `linkedin`. Linking is rejected for anonymous, remember-me-only, and administrator-impersonated sessions. A social identity already linked to another website user cannot be moved.
+
+## Verification checklist
+
+Run this for each provider in a test environment:
+
+1. Disable the provider and confirm its button disappears and direct initiation is rejected.
+2. Enable it with valid credentials and complete a first-time login.
+3. Confirm one `social_account` record and one linked `website_user` record are created.
+4. Log out and repeat; confirm no duplicate records are created.
+5. Start login in one browser and alter `state` in the callback; confirm login fails.
+6. Reuse a successful callback; confirm login fails because flow state is single-use.
+7. Start a flow, wait more than ten minutes, and confirm callback rejection.
+8. Use a social account whose email already belongs to a website user; confirm it is not logged in or linked.
+9. Sign in normally and use the CSRF-protected linking form; confirm linking succeeds.
+10. Supply an external `postLoginUrl`; confirm the redirect remains on the application origin.
+11. Deny consent and confirm the user returns to the login page without provider details or tokens in logs.
+
+## Automated tests
+
+Security regression specs are under `tests/specs`. Run them from a Preside/TestBox test harness that maps this extension at the application root:
+
+```bash
+box testbox run
+```
+
+Provider end-to-end tests require real sandbox applications and secrets, so keep them in a protected deployment environment rather than the repository.
+
+## Upgrade notes from 0.1
+
+- X OAuth 1.0a and Twitter4j were removed.
+- Facebook and Google legacy credential names are read as fallbacks, but save credentials into the new fields.
+- Base URL, redirect URL, API key, and configurable Facebook field settings were removed because they were unused and unsafe.
+- Existing email-matched accounts are not changed automatically. Users must explicitly link future social identities.
+- Providers that do not return a verified email receive a unique `@social-login.invalid` placeholder because Preside requires `website_user.email_address`; applications should replace it through their normal verified-email workflow.
+
+See [SECURITY_AUDIT.md](SECURITY_AUDIT.md) for findings, remediation details, residual risks, and standards references.
diff --git a/SECURITY_AUDIT.md b/SECURITY_AUDIT.md
new file mode 100644
index 0000000..7650df2
--- /dev/null
+++ b/SECURITY_AUDIT.md
@@ -0,0 +1,147 @@
+# Social login security audit
+
+Audit date: 13 August 2026
+
+Scope:
+
+- `handlers/Login.cfc`
+- `services/SocialAuthService.cfc`
+- the removed OAuth 1.0a `services/TwitterAuthService.cfc`
+- social-account persistence and website-user linking
+- provider and Preside configuration
+- login UI, callback handling, secrets, errors, redirects, and tests
+
+The original code was written in 2016. It could not be considered secure or provider-compatible before this remediation.
+
+## Executive result
+
+| Severity | Original finding | Remediation |
+|---|---|---|
+| Critical | OAuth 2.0 `state` was generated but never checked | One-time, session-bound, provider-bound state is checked with a timing-safe comparison and expires after ten minutes |
+| Critical | Website users were silently selected and linked by matching email | Email matching no longer authenticates or links; collisions require a CSRF-protected flow initiated by the authenticated user |
+| Critical | Twitter callback tokens were not bound to the initiating OAuth 1.0a session | OAuth 1.0a implementation removed; X now uses OAuth 2.0 authorization code with PKCE |
+| High | No PKCE for authorization-code exchanges | S256 PKCE added for Facebook, Google, and X |
+| High | Legacy Google OAuth and UserInfo endpoints | Replaced with current OAuth v2 and OIDC UserInfo endpoints |
+| High | Facebook bearer token was placed in a query string | Bearer token moved to the Authorization header; `appsecret_proof` added |
+| High | Arbitrary `postLoginUrl` enabled an open redirect | Redirects are restricted to relative application paths or the current site root |
+| High | Disabled providers remained callable directly | Initiation checks both the provider toggle and required credentials |
+| High | Social identities were selected by `social_id` without provider namespace | Lookups and a database unique constraint now use `(type, social_id)` |
+| Medium | Provider parameters were concatenated without encoding | OAuth query and form parameters are encoded independently |
+| Medium | Provider access tokens remained in session | Tokens are held only in local variables and discarded after profile retrieval |
+| Medium | Predictable OAuth nonce and hard-coded GMT+8 | Obsolete custom OAuth 1.0a implementation removed |
+| Medium | HTTP success used a substring search for `200` | Responses require a numeric 2xx status and valid JSON |
+| Medium | Provider error bodies could be logged or exposed | Logs contain provider, request stage, and status only; secrets and response bodies are excluded |
+| Medium | Authentication responses could be cached | Initiation and callback handlers disable Preside page caching and browser caching |
+| Medium | Callback failure used undefined password variables | Failure handling now persists only a generic social-login failure message |
+| Medium | Link initiation could be triggered through GET | Explicit linking uses a POST action with `event.validateCsrfToken()` |
+| Low | Configuration exposed obsolete and unused endpoint fields | Unused base URL, redirect URL, API key, and field-list settings removed |
+| Low | No LinkedIn support | LinkedIn OIDC authorization code and UserInfo support added |
+| Low | No automated security tests | State expiry/mismatch and account-linking regression specs added |
+
+## Current control design
+
+### Authorization request
+
+1. The handler rejects unknown, disabled, or incompletely configured providers.
+2. A cryptographically random state value is generated per attempt.
+3. A 96-character verifier and S256 challenge are generated for providers supporting PKCE.
+4. State, provider, verifier, linking user ID, local return URL, and creation time are stored server-side.
+5. OAuth parameters are individually encoded.
+
+### Callback
+
+1. Provider errors, missing code, and missing state fail closed.
+2. Stored and returned provider/state values must match.
+3. Attempts expire after ten minutes.
+4. Flow state is deleted before token exchange, making callbacks single-use even if a later provider call fails.
+5. Codes are exchanged server-to-server over fixed HTTPS endpoints.
+6. Profile calls use bearer authorization headers.
+7. Access and refresh tokens are neither persisted nor logged.
+
+### Account identity and linking
+
+- Provider subject IDs are namespaced by provider.
+- Existing linked subjects authenticate only their linked website user.
+- A new subject may provision a new website user.
+- An email collision with an existing website user fails with `explicitLinkRequired`.
+- Explicit linking requires:
+ - a CSRF-protected POST;
+ - a fully authenticated, non-remember-me website session;
+ - no administrator impersonation;
+ - the same website-user session at initiation and callback;
+ - an identity not already linked to another user.
+
+This prevents login CSRF, pre-registration takeover, provider-ID collision, and confused-deputy account movement.
+
+## Standards comparison
+
+| Control | OAuth 2.0 Security BCP / provider guidance | Implementation |
+|---|---|---|
+| Authorization code | Required for server-side login | All providers |
+| Exact callback URI | Required | Handler-generated fixed route; setup requires exact registration |
+| State / CSRF binding | Required | All providers |
+| PKCE S256 | Recommended for all authorization-code clients | Facebook, Google, X |
+| One-time callback | Required | Server-side state deleted before exchange |
+| Short transaction lifetime | Recommended | Ten minutes |
+| Bearer token in header | RFC 6750 recommendation | All profile calls |
+| Minimal scopes | Recommended | Provider-specific identity/profile scopes only |
+| Stable issuer subject | OIDC identity key | `sub` for Google/LinkedIn; provider `id` for Facebook/X |
+| Email linking | Must not be trusted as sole proof of account ownership | Never automatic |
+| Secret handling | Confidential clients keep secrets server-side | Preside system configuration only |
+| Redirect allowlist | Prevent unvalidated redirects | Same-origin or root-relative only |
+| Token retention | Minimise access-token lifetime and storage | Discarded after profile lookup |
+
+LinkedIn exception: the web OIDC implementation has documented interoperability failures when PKCE parameters are sent to its standard web token endpoint. LinkedIn’s separate native PKCE flow uses different endpoints and is not the web OIDC product. The extension uses `state`, an exact redirect URI, a confidential client secret, and a short single-use flow for LinkedIn. Re-test this when LinkedIn publishes PKCE support for its web OIDC endpoint.
+
+## Residual risks and operational requirements
+
+### Live provider verification
+
+This repository is an extension, not a runnable Preside application. It contains no host application, database, hostname, or provider credentials. Static review and local regression specs cannot prove that a provider application is approved and correctly configured. Complete the provider-by-provider checklist in `README.md` against test applications before production release.
+
+### Session security inherited from Preside
+
+The extension relies on Preside’s session cookie, CSRF, and normal `websiteLoginService.login()` behavior. Social authentication uses `skipPasswordCheck=true` only after the provider identity has been validated; Preside still filters inactive users, records the login, promotes visitor actions, and rotates the session. The host application must:
+
+- enforce HTTPS and secure, HTTP-only cookies;
+- use an appropriate SameSite policy that permits top-level OAuth callbacks;
+- rotate/authenticate sessions according to the deployed Preside version;
+- remain on a supported Preside/Lucee release.
+
+### Secrets at rest
+
+Provider secrets are held by Preside’s system-configuration store. Restrict settings permissions, database access, backups, support exports, and logs. Rotate a secret immediately if it is exposed. A deployment-specific secret manager would be stronger, but no standard secret-manager abstraction exists in this extension.
+
+### Provider API lifecycle
+
+Meta versions Graph endpoints and X changes product access and pricing independently of code releases. Review provider deprecations at least quarterly and run live smoke tests after provider-console changes.
+
+### Profile data
+
+Only identity data required to create the local account is stored. Provider email claims are recorded with their verification indicator but are not used to attach an existing account. Because Preside requires a unique website-user email, providers without a verified email receive a deterministic, non-deliverable `@social-login.invalid` address. Applications should replace it through a separate local email-verification/update workflow.
+
+## Verification evidence
+
+Automated regression tests cover:
+
+- mismatched callback state;
+- expired callback state;
+- email-collision refusal;
+- refusal to move a linked identity.
+
+The manual matrix in `README.md` additionally covers provider cancellation, replay, disabled-provider access, first/repeat login, explicit linking, and redirect enforcement.
+
+## References
+
+- [RFC 6749: OAuth 2.0](https://www.rfc-editor.org/rfc/rfc6749)
+- [RFC 7636: Proof Key for Code Exchange](https://www.rfc-editor.org/rfc/rfc7636)
+- [RFC 8252: OAuth 2.0 for Native Apps](https://www.rfc-editor.org/rfc/rfc8252)
+- [RFC 9207: Authorization Server Issuer Identification](https://www.rfc-editor.org/rfc/rfc9207)
+- [RFC 9700: OAuth 2.0 Security Best Current Practice](https://www.rfc-editor.org/rfc/rfc9700)
+- [OpenID Connect Core 1.0](https://openid.net/specs/openid-connect-core-1_0.html)
+- [Google OpenID Connect](https://developers.google.com/identity/openid-connect/openid-connect)
+- [Meta manual login flow](https://developers.facebook.com/docs/facebook-login/guides/advanced/manual-flow/)
+- [Meta login security](https://developers.facebook.com/docs/facebook-login/security)
+- [X OAuth 2.0 authorization-code flow with PKCE](https://docs.x.com/fundamentals/authentication/oauth-2-0/authorization-code)
+- [Sign in with LinkedIn using OpenID Connect](https://learn.microsoft.com/en-us/linkedin/consumer/integrations/self-serve/sign-in-with-linkedin-v2)
+- [Preside CSRF protection](https://docs.preside.org/devguides/csrf.html)
diff --git a/forms/system-config/social-login.xml b/forms/system-config/social-login.xml
index e7dbe43..c307200 100644
--- a/forms/system-config/social-login.xml
+++ b/forms/system-config/social-login.xml
@@ -1,60 +1,40 @@
-