Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
24bdaf7
Address principalName / email details
woodseowl May 19, 2026
e373e1c
Align composer.json and configurations
woodseowl May 19, 2026
38cd0d6
Fix test deprecations
woodseowl May 19, 2026
8c6d1af
Deprecate usage of uniqueUid
woodseowl May 19, 2026
b49bcf7
Use mail instead of email internally for RemoteIdentity clarity
woodseowl May 19, 2026
5010c05
RemoteIdentity->principleName shouldn't use uid
woodseowl May 19, 2026
794fa46
Github workflow extension adjustments
woodseowl May 19, 2026
79372b1
Add LivewireAuth
woodseowl May 19, 2026
6cdbffd
Deprecate IdentityManager::hasIdentity(), renamed to hasRemoteIdentity()
woodseowl May 20, 2026
52ed165
Add ChecksLocalLogin trait and update LivewireAuth, CUAuth to support…
woodseowl May 20, 2026
1e2534d
CLAUDE.md file
woodseowl May 20, 2026
55d1034
Consolidate requireLivewireAuth() out of CUAuthServiceProvider
woodseowl May 20, 2026
4e98275
Linting
woodseowl May 20, 2026
ee85a6d
Better documentation for hasIdentity() deprecation
woodseowl May 20, 2026
28f5be1
Clarify RemoteIdentity properties
woodseowl Jun 1, 2026
478e997
Address PR feedback
woodseowl Jun 1, 2026
4933a9a
Security fix version for php-saml
woodseowl Jun 1, 2026
f79e013
Deprecate RemoteIdentity->email()
woodseowl Jun 1, 2026
d2b5c3b
Merge branch 'updates-may2026' into livewire-auth
woodseowl Jun 1, 2026
517087d
Deprecate RemoteIdentity->email()
woodseowl Jun 1, 2026
4b6776a
AuthorizeUser example manages email alias
woodseowl Jun 1, 2026
09133e8
Deprecate RemoteIdentity->email()
woodseowl Jun 1, 2026
9e5cf06
Merge branch 'updates-may2026' into livewire-auth
woodseowl Jun 1, 2026
22561fc
README for LivewireAuth
woodseowl Jun 1, 2026
aeed711
Merge pull request #4 from CornellCustomDev/livewire-auth
woodseowl Jun 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,3 @@ trim_trailing_whitespace = false

[*.{yml,yaml}]
indent_size = 2

[*.json]
indent_size = 2
1 change: 0 additions & 1 deletion .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
/.github export-ignore
/.gitattributes export-ignore
/.gitignore export-ignore
/.lando.yml export-ignore
/phpunit.xml export-ignore
/pint.json export-ignore
/tests export-ignore
2 changes: 1 addition & 1 deletion .github/workflows/laravel-pint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v6

- name: Run Laravel Pint
uses: aglipanci/laravel-pint-action@latest
Expand Down
31 changes: 14 additions & 17 deletions .github/workflows/phpunit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,36 +6,33 @@ jobs:
strategy:
fail-fast: true
matrix:
php: [8.2, 8.3, 8.4, 8.5]
laravel: [11.*, 12.*]
stability: [prefer-stable]
php: [8.3, 8.4, 8.5]
laravel: [12.*, 13.*]
include:
- laravel: 11.*
testbench: 9.*
- laravel: 12.*
testbench: 10.*
testbench: "^10.0"
- laravel: 13.*
testbench: "^11.0"
exclude:
Comment thread
woodseowl marked this conversation as resolved.
- php: 8.3
laravel: 13.*

name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.stability }}
name: P${{ matrix.php }} - L${{ matrix.laravel }}
steps:
- name: Checkout code
uses: actions/checkout@v3
uses: actions/checkout@v6

Comment thread
woodseowl marked this conversation as resolved.
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
extensions: dom, curl, libxml, mbstring, zip, pdo, sqlite3, pdo_sqlite, bcmath, soap, intl, fileinfo
extensions: dom, curl, mbstring, pdo, pdo_sqlite, sqlite3, bcmath, fileinfo
coverage: none

- name: Setup problem matchers
run: |
echo "::add-matcher::${{ runner.tool_cache }}/php.json"
echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json"

- name: Install dependencies
run: |
composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" --no-interaction --no-update
composer update --${{ matrix.stability }} --prefer-dist --no-interaction
uses: ramsey/composer-install@v4
with:
composer-options: "--no-cache --with=orchestra/testbench:${{ matrix.testbench }}"

- name: List Installed Dependencies
run: composer show -D
Expand Down
63 changes: 63 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Commands

```bash
# Run all tests
composer test

# Run a single test file
vendor/bin/phpunit tests/Feature/AppTestersTest.php

# Run a single test by name
vendor/bin/phpunit --filter testMethodName

# Lint / fix code style
vendor/bin/pint

# Full CI (prepare + test)
composer ci
```

## Architecture

This is a **Laravel package** (not an application) providing SSO authentication middleware for Cornell University apps. The namespace is `CornellCustomDev\LaravelStarterKit\CUAuth\`.

### Core abstraction: `IdentityManager`

`src/Managers/IdentityManager.php` is the central interface. `CUAuthServiceProvider` binds one concrete implementation as a singleton based on `CU_AUTH_IDENTITY_MANAGER`:

- **`ShibIdentityManager`** (`apache-shib`) — reads Shibboleth attributes from Apache server variables (`$_SERVER`). For local development, falls back to `REMOTE_USER` env var when `APP_ENV != production`.
- **`SamlIdentityManager`** (`php-saml`) — uses the OneLogin PHP-SAML toolkit for SAML SP flows.

Both return a `RemoteIdentity` data object (readonly class) that normalizes identity data from either IdP.

### Authentication flow

1. **`CUAuth` middleware** guards routes. It checks `IdentityManager::hasRemoteIdentity()` and redirects to `cu-auth.sso-login` if not authenticated. If `allow_local_login = true`, a locally-authenticated Laravel user bypasses the SSO check entirely (`isLoggedInLocally()` in the `ChecksLocalLogin` trait, shared with `LivewireAuth`).
2. If `require_local_user = true`, the middleware fires the `CUAuthenticated` event after SSO auth. The consuming app must listen for this event to log in or create a local Laravel user.
3. **`AppTesters` middleware** can be stacked after `CUAuth` to restrict non-production access to users listed in `APP_TESTERS`.

### Routes registered by the package

- `GET /sso/login` → `AuthController::login` (redirects to IdP)
- `GET /sso/logout` → `AuthController::logout` (SLO)
- `GET/POST /sso/acs` → `AuthController::acs` (SAML assertion consumer / Shib return; CSRF-exempt)
- `GET /sso/metadata` → `AuthController::metadata` (SAML SP metadata)

### `RemoteIdentity` key methods

- `id()` — NetID or CWID (unique within the IdP)
- `principalName()` — `eduPersonPrincipalName` (e.g., `netid@cornell.edu`); unique across Cornell and Weill IdPs
- `email()` — returns `principalName` if set, else alias mail
- `uniqueUid()` — **deprecated**; use `principalName()` for cross-IdP uniqueness

### Livewire support

Setting `REQUIRE_LIVEWIRE_AUTH=true` makes the service provider override Livewire's update route to require `LivewireAuth` middleware, blocking unauthenticated POSTs to `/livewire/update`.

### Testing

Tests use Orchestra Testbench. `FeatureTestCase` sets up an in-memory SQLite database and loads Laravel's default migrations. Unit tests extend `UnitTestCase`. The package has no database migrations of its own.
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ Middleware for authorizing Laravel users.
- Apache mod_shib integration
- [AppTesters](#apptesters) - Limit access to users in the `APP_TESTERS` environment variable
- [Local Login](#local-login) - Allow Laravel users to log in with a local username and password
- [Livewire Auth](#livewire-auth) - Block unauthenticated Livewire update requests

## Use Cases

Expand Down Expand Up @@ -163,4 +164,16 @@ For testing purposes, the environment variable "ALLOW_LOCAL_LOGIN" can be set to
```dotenv
# File: .env
ALLOW_LOCAL_LOGIN=true
```


## Livewire Auth

Blocks unauthenticated POST requests to `/livewire/update`, preventing anonymous users from interacting with Livewire components.

### Usage

```dotenv
# File: .env
REQUIRE_LIVEWIRE_AUTH=true
```
78 changes: 45 additions & 33 deletions composer.json
Original file line number Diff line number Diff line change
@@ -1,35 +1,47 @@
{
"name": "cornell-custom-dev/laravel-cu-auth",
"description": "A Laravel package for authentication and identity management at Cornell University",
"license": "MIT",
"type": "library",
"require": {
"php": "^8.2",
"ext-openssl": "*",
"illuminate/support": "^11.0|^12.0",
"onelogin/php-saml": "^4.3.1"
},
"require-dev": {
"laravel/pint": "^1.20",
"orchestra/testbench": "^9.0|^10.0",
"phpunit/phpunit": "^10.5|^11.5"
},
"autoload": {
"psr-4": {
"CornellCustomDev\\LaravelStarterKit\\CUAuth\\": "src"
}
},
"autoload-dev": {
"psr-4": {
"CornellCustomDev\\LaravelStarterKit\\CUAuth\\Tests\\": "tests"
}
},
"extra": {
"laravel": {
"providers": [
"CornellCustomDev\\LaravelStarterKit\\CUAuth\\CUAuthServiceProvider"
]
}
},
"prefer-stable": true
"name": "cornell-custom-dev/laravel-cu-auth",
"description": "A Laravel package for authentication and identity management at Cornell University",
"license": "MIT",
"type": "library",
"require": {
"php": "^8.3",
"ext-openssl": "*",
"illuminate/support": "^12.0|^13.0",
"onelogin/php-saml": "^4.3.2"
},
"require-dev": {
"laravel/pint": "^1.20",
"orchestra/testbench": "^10.0|^11.0",
"phpunit/phpunit": "^11.5|^12.5"
},
"autoload": {
"psr-4": {
"CornellCustomDev\\LaravelStarterKit\\CUAuth\\": "src"
}
},
"autoload-dev": {
"psr-4": {
"CornellCustomDev\\LaravelStarterKit\\CUAuth\\Tests\\": "tests"
}
},
"scripts": {
"post-autoload-dump": [
"@clear",
"@prepare"
],
"clear": "@php vendor/bin/testbench package:purge-skeleton --ansi",
"prepare": "@php vendor/bin/testbench package:discover --ansi",
"test": "@php vendor/bin/phpunit -c ./ --color"
},
"config": {
"sort-packages": true
},
"extra": {
"laravel": {
"providers": [
"CornellCustomDev\\LaravelStarterKit\\CUAuth\\CUAuthServiceProvider"
]
}
},
"prefer-stable": true
}
11 changes: 11 additions & 0 deletions config/cu-auth.php
Original file line number Diff line number Diff line change
Expand Up @@ -79,4 +79,15 @@
|
*/
'allow_local_login' => boolval(env('ALLOW_LOCAL_LOGIN', false)),

/*
|--------------------------------------------------------------------------
| Restrict Access for Livewire Updates
|--------------------------------------------------------------------------
|
| Add global middleware protection against unauthenticated posting to
| /livewire/update.
|
*/
'require_livewire_auth' => boolval(env('REQUIRE_LIVEWIRE_AUTH', false)),
];
11 changes: 7 additions & 4 deletions phpunit.xml
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
bootstrap="vendor/autoload.php"
backupGlobals="false"
beStrictAboutTestsThatDoNotTestAnything="false"
colors="true"
testdox="true"
xsi:noNamespaceSchemaLocation="./vendor/phpunit/phpunit/phpunit.xsd"
processIsolation="false"
stopOnError="false"
stopOnFailure="false"
cacheDirectory=".phpunit.cache"
backupStaticProperties="false"
testdox="true"
>
<source>
<include>
Expand Down
5 changes: 5 additions & 0 deletions src/CUAuthServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use CornellCustomDev\LaravelStarterKit\CUAuth\Managers\IdentityManager;
use CornellCustomDev\LaravelStarterKit\CUAuth\Managers\SamlIdentityManager;
use CornellCustomDev\LaravelStarterKit\CUAuth\Managers\ShibIdentityManager;
use CornellCustomDev\LaravelStarterKit\CUAuth\Middleware\LivewireAuth;
use Illuminate\Support\ServiceProvider;

class CUAuthServiceProvider extends ServiceProvider
Expand Down Expand Up @@ -51,5 +52,9 @@ public function boot(): void
]);
}
$this->loadRoutesFrom(__DIR__.'/../routes/cu-auth.php');

if (config('cu-auth.require_livewire_auth')) {
LivewireAuth::requireLivewireAuth();
}
}
}
35 changes: 29 additions & 6 deletions src/DataObjects/RemoteIdentity.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@
public function __construct(
public string $idp,
public string $uid,
public string $principalName = '',
public string $displayName = '',
public string $email = '',
private string $mail = '',
public array $data = [],
Comment thread
woodseowl marked this conversation as resolved.
Comment thread
woodseowl marked this conversation as resolved.
) {}

Expand All @@ -26,12 +28,11 @@ public static function fromData(
return new RemoteIdentity(
idp: $idp,
uid: $uid,
principalName: trim($eduPersonPrincipalName ?? ''),
displayName: $displayName
?? $cn
?? trim(($givenName ?? '').' '.($sn ?? '')),
email: $eduPersonPrincipalName
?? $mail
?? '',
mail: trim($mail ?? ''),
data: $data,
);
}
Expand All @@ -46,6 +47,9 @@ public function id(): string

/**
* Provides an id that is unique across Cornell IdPs.
*
* @deprecated Use of uniqueUid() should be replaced with use of principalName() for a unique identifier
* across IdPs, and id() for an identifier within the IdP.
*/
public function uniqueUid(): string
{
Expand All @@ -55,12 +59,29 @@ public function uniqueUid(): string
};
}

public function principalName(): string
{
return $this->principalName;
}

public function primaryEmail(): string
{
return $this->principalName;
}

public function emailAlias(): string
{
return $this->mail;
}

/**
* Returns the primary email (netid@cornell.edu|cwid@med.cornell.edu) if available, otherwise the alias email.
*
* @deprecated Use of email() should be replaced with primaryEmai() or emailAlias(), as appropriate.
*/
public function email(): string
{
return $this->email;
return $this->primaryEmail() ?: $this->emailAlias();
}

/**
Expand All @@ -73,11 +94,13 @@ public function name(): string

public function isCornellIdP(): bool
{
return str_contains($this->idp, 'cit.cornell.edu');
return str_contains($this->idp, 'cit.cornell.edu')
|| str_contains($this->principalName, '@cornell.edu');
}

public function isWeillIdP(): bool
{
return str_contains($this->idp, 'weill.cornell.edu');
return str_contains($this->idp, 'weill.cornell.edu')
|| str_contains($this->principalName, '@med.cornell.edu');
}
}
4 changes: 2 additions & 2 deletions src/Listeners/AuthorizeUser.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,13 @@ public function handle(CUAuthenticated $event, ?RemoteIdentity $remoteIdentity =

// Look for a matching user.
$userModel = config('auth.providers.users.model');
$user = $userModel::firstWhere('email', $remoteIdentity->email());
$user = $userModel::firstWhere('email', $remoteIdentity->primaryEmail());

if (empty($user)) {
// User does not exist, so create them.
$user = new $userModel;
$user->name = $remoteIdentity->name();
$user->email = $remoteIdentity->email();
$user->email = $remoteIdentity->primaryEmail();
$user->password = Str::random(32);
$user->save();
Log::info("AuthorizeUser: Created user $user->email with ID $user->id.");
Expand Down
Loading