PHPackages                             lexwebdev/laravel-siwx - 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. [Authentication &amp; Authorization](/categories/authentication)
4. /
5. lexwebdev/laravel-siwx

ActiveLibrary[Authentication &amp; Authorization](/categories/authentication)

lexwebdev/laravel-siwx
======================

Sign-In With X for Laravel: EVM and Solana wallet auth, One-Click Auth ready

v0.1.0(today)00MITPHPPHP ^8.2CI passing

Since Jul 30Pushed todayCompare

[ Source](https://github.com/LexWebDev/laravel-siwx)[ Packagist](https://packagist.org/packages/lexwebdev/laravel-siwx)[ RSS](/packages/lexwebdev-laravel-siwx/feed)WikiDiscussions main Synced today

READMEChangelog (1)Dependencies (6)Versions (2)Used By (0)

Laravel SIWX
============

[](#laravel-siwx)

[![tests](https://github.com/LexWebDev/laravel-siwx/actions/workflows/tests.yml/badge.svg?branch=main&event=push)](https://github.com/LexWebDev/laravel-siwx/actions/workflows/tests.yml?query=branch%3Amain)[![license](https://camo.githubusercontent.com/7013272bd27ece47364536a221edb554cd69683b68a46fc0ee96881174c4214c/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6c6963656e73652d4d49542d626c75652e737667)](LICENSE)

Server-side Sign-In With X verification for Laravel. Verifies wallet signatures over [EIP-4361](https://eips.ethereum.org/EIPS/eip-4361) messages for **EVM** and **Solana**accounts, and survives WalletConnect One-Click Auth.

Why this exists
---------------

[](#why-this-exists)

A naive SIWE verifier works on the desktop happy path and breaks the moment a real wallet shows up:

- **The header is not fixed.** AppKit builds the first line as `${domain} wants you to sign in with your ${networkName} account:`, where `networkName`comes from the network config — `Solana`, `Polygon`, anything. A parser that greps for the word `Ethereum` rejects every non-EVM login.
- **`Chain ID` is not a number.** AppKit passes a CAIP-2 id (`eip155:1`, `solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp`), while the One-Click Auth path builds the message through WalletConnect's `formatAuthMessage` and emits a plain integer per EIP-4361. Both arrive at the same endpoint. This package accepts both and normalises to CAIP-2.
- **One-Click Auth appends a `Resources:` section** with a `urn:recap:` capability. It is part of the signed bytes, so it must be preserved for verification and ignored for field parsing.
- **Address casing is chain-specific.** Lowercasing a Solana base58 address turns it into a different, non-existent account.

Each of those is covered by a test against a real signature vector.

Supported chains
----------------

[](#supported-chains)

CAIP-2 namespaceChainsSignature scheme`eip155`every EVM chainsecp256k1 + keccak256, EIP-191, optional EIP-1271`solana`Solana mainnet/devneted25519 via `ext-sodium`, base58 and base64 signaturesBitcoin (`bip122`) is intentionally out of 0.1.0 — BIP-322 requires building and validating virtual transactions, and there is no PHP library for it yet. The architecture is ready for it: add your own verifier without touching the core.

```
use LexWebDev\Siwx\Contracts\SignatureVerifier;
use LexWebDev\Siwx\VerifierRegistry;

// In a service provider's boot():
app(VerifierRegistry::class)->register(new MyBip122Verifier);
```

Also add the namespace to `config('siwx.namespaces')` — the registry refuses namespaces that are not explicitly enabled.

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

[](#installation)

```
composer require lexwebdev/laravel-siwx
php artisan vendor:publish --tag=siwx-config
```

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

[](#configuration)

`SIWX_ALLOWED_DOMAINS` is **required**. With an empty allow list every verification fails with `siwx_invalid_domain`. That is deliberate: an empty allow list is safer than an open one, and the domain binding is what stops a signature obtained on another site from working on yours.

```
SIWX_ALLOWED_DOMAINS=app.example,www.app.example
SIWX_NONCE_TTL=600
SIWX_CLOCK_SKEW=300
SIWX_CACHE_STORE=
SIWX_ROUTES_ENABLED=true
SIWX_ROUTES_PREFIX=siwx
SIWX_EIP1271_ENABLED=false
SIWX_RPC_URL=
```

List each domain **once**, exactly as the wallet will send it in the `domain` line — including a port if your app runs on one, e.g. `localhost:3000` during development.

The `URI` field is not matched against the allow list separately. Instead its authority (host, plus port when present) must equal the `domain` line, which is what EIP-4361 requires of a well-formed message. That is stricter than checking both against the list: with two allowed hosts, a message could otherwise name one domain and point its URI at the other.

Usage
-----

[](#usage)

The package ships `GET /siwx/nonce`, which returns `{"nonce": "..."}`. Nonces are single use and expire after `SIWX_NONCE_TTL` seconds. Disable the route with `SIWX_ROUTES_ENABLED=false`if you want to issue nonces yourself.

Authentication routes are deliberately not included — they depend on your user model and token strategy. A minimal controller:

```
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use LexWebDev\Siwx\SiwxVerifier;

public function auth(Request $request, SiwxVerifier $verifier): JsonResponse
{
    $data = $request->validate([
        'message' => ['required', 'string', 'max:4000'],
        'signature' => ['required', 'string', 'max:2000'],
    ]);

    $session = $verifier->verify($data['message'], $data['signature']);

    $user = User::firstOrCreate([
        'wallet_address' => $session->address,
        'wallet_namespace' => $session->namespace,
    ]);

    return response()->json(['access_token' => $user->createToken('siwx')->plainTextToken]);
}
```

`verify()` throws `LexWebDev\Siwx\Exceptions\SiwxException` on any failure and returns a `VerifiedSiwxSession` on success:

```
$session->address;    // normalised per chain
$session->namespace;  // 'eip155' | 'solana'
$session->chainId;    // always CAIP-2, e.g. 'eip155:137'
$session->domain;
$session->issuedAt;   // CarbonImmutable
$session->message;    // the parsed SiwxMessage
```

### Storing the address

[](#storing-the-address)

`$session->address` is normalised **per chain**: EVM addresses are lowercased, Solana base58 addresses are returned byte-for-byte because base58 is case-sensitive.

Never store the address alone. The same string can be a valid account on more than one namespace, so make the unique index cover the pair:

```
$table->string('wallet_namespace');
$table->string('wallet_address');
$table->unique(['wallet_namespace', 'wallet_address']);
```

Error codes
-----------

[](#error-codes)

`SiwxException::code()` returns a machine-readable string. Nothing about the internals leaks into it — you get the code, not a "recovered 0xabc, expected 0xdef" diagnostic.

CodeMeaning`siwx_invalid_message`malformed EIP-4361 message, bad version, or a failed timestamp check`siwx_invalid_domain``domain` or `URI` host is not in the allow list, or the list is empty`siwx_invalid_nonce`nonce was never issued, already used, or expired`siwx_invalid_signature`signature does not belong to the claimed address`siwx_unsupported_namespace`chain namespace is unknown or disabled in configThe nonce is consumed **after** the signature check passes, so an invalid signature cannot be used to burn somebody else's nonce.

Frontend
--------

[](#frontend)

Use `SIWXConfig` from `@reown/appkit` — there is no separate `@reown/appkit-siwx` package to install.

```
import { createAppKit } from '@reown/appkit'

createAppKit({
  // ...
  siwx: {
    async getNonce() {
      const { nonce } = await fetch('/siwx/nonce').then((r) => r.json())
      return nonce
    },
    async addSession({ message, signature }) {
      await fetch('/api/auth', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ message, signature }),
      })
    },
    // getSessions, revokeSession, setSessions per your app
  },
})
```

On the One-Click Auth path `createMessage` is called without an address, so the nonce cannot be derived from the wallet — fetch it from `GET /siwx/nonce` first.

### One-Click Auth only fires in a single-namespace app

[](#one-click-auth-only-fires-in-a-single-namespace-app)

AppKit attempts One-Click Auth — `wc_sessionAuthenticate`, which returns a CACAO and a message built by WalletConnect rather than by your `createMessage` — only when the networks you register belong to **exactly one** namespace, and that namespace is `eip155`. The check lives in `SIWXUtil.universalProviderAuthenticate`:

```
const namespaces = new Set(chains.map(chain => chain.split(':')[0]))
if (!siwx || namespaces.size !== 1 || !namespaces.has('eip155')) {
  return false
}
```

So an app that registers EVM chains **and** Solana in the same AppKit instance never produces a CACAO: every sign-in falls back to a plain `personal_sign` of the message your own code composed. That is worth knowing before you go looking for a bug in your `Resources` handling — there simply will not be a `Resources` section to handle.

This package verifies both shapes either way, so nothing here needs configuring. The note is about what you should expect to see on the wire.

Two things follow for testing. A wallet that does not implement `wc_sessionAuthenticate` also falls back silently, so an absent CACAO does not by itself tell you which side declined. And the request is sent during connection, not at signing time: if you see a signature prompt arrive after the session is already established, One-Click Auth did not happen.

Smart contract wallets (EIP-1271)
---------------------------------

[](#smart-contract-wallets-eip-1271)

Off by default. When enabled, a signature that fails `ecrecover` gets a second chance through an `eth_call` to `isValidSignature` on the claimed address:

```
SIWX_EIP1271_ENABLED=true
SIWX_RPC_URL=https://your-rpc-endpoint
```

EOA logins never pay for this — the RPC call only happens after recovery fails. An RPC error is treated as an invalid signature, not an exception. This is the only place the package touches the network; swap the implementation by rebinding `LexWebDev\Siwx\Contracts\ContractSignatureChecker`.

Compatibility
-------------

[](#compatibility)

PHP8.2+ for Laravel 12, 8.3+ for Laravel 13 (framework requirement)Laravel12, 13Extensions`ext-sodium` for ed25519, `ext-gmp` for secp256k1 (pulled in by `simplito/elliptic-php`)Laravel 11 is not supported. Every release in that branch, from v11.7.0 through the final v11.55.0, is affected by unpatched security advisories, and Composer 2.10+ refuses to install them under its default advisory policy — so the framework itself is unavailable there, independently of this package.

Contributing
------------

[](#contributing)

Bug reports and pull requests are welcome. [CONTRIBUTING.md](CONTRIBUTING.md) covers how to run the suite and the rules around verification code — worth reading before you touch a verifier, since a few of them are not obvious. Participation is under the [Code of Conduct](CODE_OF_CONDUCT.md).

Security
--------

[](#security)

See [SECURITY.md](SECURITY.md). Please do not report vulnerabilities in public issues.

License
-------

[](#license)

MIT. See [LICENSE](LICENSE).

###  Health Score

36

—

LowBetter than 79% of packages

Maintenance100

Actively maintained with recent releases

Popularity0

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity35

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.

###  Release Activity

Cadence

Unknown

Total

1

Last Release

0d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/07ca55d5a8d69ad14f34dd7d987d4b3998633a1eba9414d6b085c5c349ddf268?d=identicon)[LexWebDev](/maintainers/LexWebDev)

---

Top Contributors

[![LexWebDev](https://avatars.githubusercontent.com/u/40832840?v=4)](https://github.com/LexWebDev "LexWebDev (17 commits)")

---

Tags

authenticationeip-4361ethereumlaravelphpsiwesiwxsolanawalletwalletconnectweb3laravelauthethereumweb3solanasiwesiwx

###  Code Quality

TestsPest

### Embed Badge

![Health badge](/badges/lexwebdev-laravel-siwx/health.svg)

```
[![Health](https://phpackages.com/badges/lexwebdev-laravel-siwx/health.svg)](https://phpackages.com/packages/lexwebdev-laravel-siwx)
```

###  Alternatives

[php-open-source-saver/jwt-auth

JSON Web Token Authentication for Laravel and Lumen

84611.1M64](/packages/php-open-source-saver-jwt-auth)[directorytree/ldaprecord-laravel

LDAP Authentication &amp; Management for Laravel.

5752.3M21](/packages/directorytree-ldaprecord-laravel)[jurager/teams

Laravel package to manage team functionality and operate with user permissions.

23822.5k](/packages/jurager-teams)

PHPackages © 2026

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