Skip to content

Commit 4336445

Browse files
authored
Merge pull request #67 from CornellCustomDev/cu-auth-install
CU Auth package
2 parents 2a8344c + 59ff5c2 commit 4336445

20 files changed

+1052
-13
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ A Cornell University CIT Custom Development starter kit and library for Laravel.
1111

1212
## Usage
1313

14-
The Starter Kit can be used as a starter kit for a new site or as a library for an existing site.
14+
The Starter Kit can be used [as a starter kit for a new site](#as-a-starter-kit-for-a-new-site) or [as a library for an existing site](#as-a-library-for-an-existing-site).
1515

1616
### As a Starter Kit for a New Site
1717

@@ -84,7 +84,7 @@ For an existing Laravel site, this package can be composer-required to provide t
8484
The libraries included in the Starter Kit are documented in their respective README files:
8585
8686
- [Contact/PhoneNumber](src/Contact/README.md): A library for parsing and formatting a phone number.
87-
87+
- [CUAuth](src/CUAuth/README.md): A middleware for authorizing Laravel users, mostly for Apache mod_shib authentication.
8888
8989
## Deploying a site
9090
Once a Media3 site has been created, you have confirmed you can reach the default site via a web browser, and you have access to the site login by command line, the code can be deployed.

composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@
2929
"extra": {
3030
"laravel": {
3131
"providers": [
32-
"CornellCustomDev\\LaravelStarterKit\\StarterKitServiceProvider"
32+
"CornellCustomDev\\LaravelStarterKit\\StarterKitServiceProvider",
33+
"CornellCustomDev\\LaravelStarterKit\\CUAuth\\CUAuthServiceProvider"
3334
]
3435
}
3536
},

config/cu-auth.php

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<?php
2+
3+
return [
4+
/*
5+
|--------------------------------------------------------------------------
6+
| ApacheShib Configuration
7+
|--------------------------------------------------------------------------
8+
|
9+
| ApacheShib retrieves user data from server variables populated by the
10+
| Apache shibboleth module (mod_shib).
11+
|
12+
| The default user variable is "REMOTE_USER", but this may differ depending
13+
| on how mod_shib is configured.
14+
|
15+
| For local development without shibboleth, you can add
16+
| REMOTE_USER=<netid> to your project .env file to log in as that user.
17+
|
18+
| To require a local user be logged in based on the remote user, set
19+
| REQUIRE_LOCAL_USER to true.
20+
|
21+
*/
22+
'apache_shib_user_variable' => env('APACHE_SHIB_USER_VARIABLE', 'REMOTE_USER'),
23+
'remote_user_override' => env('REMOTE_USER'),
24+
25+
'require_local_user' => env('REQUIRE_LOCAL_USER', false),
26+
27+
'shibboleth_login_url' => env('SHIBBOLETH_LOGIN_URL', '/Shibboleth.sso/Login'),
28+
'shibboleth_logout_url' => env('SHIBBOLETH_LOGOUT_URL', '/Shibboleth.sso/Logout'),
29+
30+
/*
31+
|--------------------------------------------------------------------------
32+
| AppTesters Configuration
33+
|--------------------------------------------------------------------------
34+
|
35+
| Comma-separated list of users to allow in development environments.
36+
| APP_TESTERS_FIELD is the field on the user model to compare against.
37+
|
38+
*/
39+
'app_testers' => env('APP_TESTERS', ''),
40+
'app_testers_field' => env('APP_TESTERS_FIELD', 'netid'),
41+
42+
/*
43+
|--------------------------------------------------------------------------
44+
| Allow Local Login
45+
|--------------------------------------------------------------------------
46+
|
47+
| Allow Laravel password-based login? Typically, this would only be used
48+
| for local or automated testing.
49+
|
50+
*/
51+
'allow_local_login' => boolval(env('ALLOW_LOCAL_LOGIN', false)),
52+
];

phpunit.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
bootstrap="vendor/autoload.php"
55
colors="true"
66
testdox="true"
7-
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
7+
xsi:noNamespaceSchemaLocation="./vendor/phpunit/phpunit/phpunit.xsd"
88
cacheDirectory=".phpunit.cache"
99
>
1010
<source>
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
namespace CornellCustomDev\LaravelStarterKit\CUAuth;
4+
5+
use CornellCustomDev\LaravelStarterKit\StarterKitServiceProvider;
6+
use Illuminate\Support\ServiceProvider;
7+
8+
class CUAuthServiceProvider extends ServiceProvider
9+
{
10+
const INSTALL_CONFIG_TAG = 'cu-auth-config';
11+
12+
public function register(): void
13+
{
14+
$this->mergeConfigFrom(
15+
path: __DIR__.'/../../config/cu-auth.php',
16+
key: 'cu-auth',
17+
);
18+
}
19+
20+
public function boot(): void
21+
{
22+
if ($this->app->runningInConsole()) {
23+
$this->publishes([
24+
__DIR__.'/../../config/cu-auth.php' => config_path('cu-auth.php'),
25+
], StarterKitServiceProvider::PACKAGE_NAME.':'.self::INSTALL_CONFIG_TAG);
26+
}
27+
$this->loadRoutesFrom(__DIR__.'/routes.php');
28+
}
29+
}
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
<?php
2+
3+
namespace CornellCustomDev\LaravelStarterKit\CUAuth\DataObjects;
4+
5+
use Illuminate\Http\Request;
6+
7+
class ShibIdentity
8+
{
9+
// Shibboleth fields generally available from either cit or weill IdPs.
10+
public const SHIB_FIELDS = [
11+
'Shib_Application_ID', // <vhost|applicationId>
12+
'Shib_Authentication_Instant', // YYYY-MM-DDT00:00:00.000Z
13+
'Shib_Identity_Provider', // https://shibidp.cit.cornell.edu/idp/shibboleth|https://login.weill.cornell.edu/idp
14+
'Shib_Session_Expires', // timestamp
15+
'Shib_Session_Inactivity', // timestamp
16+
'displayName', // John Doe
17+
'eduPersonAffiliations', // employee;member;staff
18+
'eduPersonPrincipalName', // [email protected]|[email protected]
19+
'eduPersonScopedAffiliation', // employee@[med.]cornell.edu;member@[med.]cornell.edu;[email protected]
20+
'givenName', // John
21+
'mail', // alias email
22+
'sn', // Doe
23+
'uid', // netid|cwid
24+
];
25+
26+
public function __construct(
27+
public readonly string $idp,
28+
public readonly string $uid,
29+
public readonly string $displayName = '',
30+
public readonly string $email = '',
31+
public readonly array $serverVars = [],
32+
) {}
33+
34+
/**
35+
* Shibboleth server variables will be retrieved from the request if not provided.
36+
*/
37+
public static function fromServerVars(?array $serverVars = null): self
38+
{
39+
if (empty($serverVars)) {
40+
$serverVars = app('request')->server();
41+
}
42+
43+
return new ShibIdentity(
44+
idp: $serverVars['Shib_Identity_Provider'] ?? '',
45+
uid: $serverVars['uid'] ?? '',
46+
displayName: $serverVars['displayName']
47+
?? $serverVars['cn']
48+
?? trim(($serverVars['givenName'] ?? '').' '.($serverVars['sn'] ?? '')),
49+
email: $serverVars['eduPersonPrincipalName']
50+
?? $serverVars['mail'] ?? '',
51+
serverVars: $serverVars,
52+
);
53+
}
54+
55+
public static function getRemoteUser(?Request $request = null): ?string
56+
{
57+
if (empty($request)) {
58+
$request = app('request');
59+
}
60+
61+
// If this is a local development environment, allow the local override.
62+
$remote_user_override = self::getRemoteUserOverride();
63+
64+
// Apache mod_shib populates the remote user variable if someone is logged in.
65+
return $request->server(config('cu-auth.apache_shib_user_variable')) ?: $remote_user_override;
66+
}
67+
68+
public static function getRemoteUserOverride(): ?string
69+
{
70+
// If this is a local development environment, allow the local override.
71+
return app()->isLocal() ? config('cu-auth.remote_user_override') : null;
72+
}
73+
74+
public function isCornellIdP(): bool
75+
{
76+
return str_contains($this->idp, 'cit.cornell.edu');
77+
}
78+
79+
public function isWeillIdP(): bool
80+
{
81+
return str_contains($this->idp, 'weill.cornell.edu');
82+
}
83+
84+
/**
85+
* Provides a uid that is unique across Cornell IdPs.
86+
*/
87+
public function uniqueUid(): string
88+
{
89+
return match (true) {
90+
$this->isCornellIdP() => $this->uid,
91+
$this->isWeillIdP() => $this->uid.'_w',
92+
};
93+
}
94+
95+
/**
96+
* Returns the primary email ([email protected]|[email protected]) if available, otherwise the alias email.
97+
*/
98+
public function email(): string
99+
{
100+
return $this->email;
101+
}
102+
103+
/**
104+
* Returns the display name if available, otherwise the common name, fallback is "givenName sn".
105+
*/
106+
public function name(): string
107+
{
108+
return $this->displayName;
109+
}
110+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php
2+
3+
namespace CornellCustomDev\LaravelStarterKit\CUAuth\Events;
4+
5+
use Illuminate\Foundation\Events\Dispatchable;
6+
use Illuminate\Queue\SerializesModels;
7+
8+
class CUAuthenticated
9+
{
10+
use Dispatchable, SerializesModels;
11+
12+
public function __construct(
13+
public readonly string $remoteUser,
14+
) {}
15+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<?php
2+
3+
namespace CornellCustomDev\LaravelStarterKit\CUAuth\Http\Controllers;
4+
5+
use CornellCustomDev\LaravelStarterKit\CUAuth\DataObjects\ShibIdentity;
6+
use Illuminate\Http\Request;
7+
use Illuminate\Routing\Controller as BaseController;
8+
use Illuminate\Support\Facades\Auth;
9+
10+
class AuthController extends BaseController
11+
{
12+
public function shibbolethLogin(Request $request)
13+
{
14+
$redirectUri = $request->query('redirect_uri', '/');
15+
16+
if (ShibIdentity::getRemoteUser($request)) {
17+
// Already logged in so redirect to the originally intended URL
18+
return redirect()->to($redirectUri);
19+
}
20+
21+
// Use the Shibboleth login URL
22+
return redirect(config('cu-auth.shibboleth_login_url').'?target='.urlencode($redirectUri));
23+
}
24+
25+
public function shibbolethLogout(Request $request)
26+
{
27+
Auth::logout();
28+
$request->session()->invalidate();
29+
$request->session()->regenerateToken();
30+
31+
$returnUrl = $request->query('return', '/');
32+
33+
if (ShibIdentity::getRemoteUserOverride()) {
34+
// If using locally configured remote user, there is no Shibboleth logout
35+
return redirect()->to($returnUrl);
36+
}
37+
38+
// Use the Shibboleth logout URL
39+
return redirect(config('cu-auth.shibboleth_logout_url').'?return='.urlencode($returnUrl));
40+
}
41+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php
2+
3+
namespace CornellCustomDev\LaravelStarterKit\CUAuth\Listeners;
4+
5+
use CornellCustomDev\LaravelStarterKit\CUAuth\DataObjects\ShibIdentity;
6+
use CornellCustomDev\LaravelStarterKit\CUAuth\Events\CUAuthenticated;
7+
use Illuminate\Support\Facades\Log;
8+
use Illuminate\Support\Str;
9+
10+
class AuthorizeUser
11+
{
12+
public function handle(CUAuthenticated $event, ?array $serverVars = null): void
13+
{
14+
$shibboleth = ShibIdentity::fromServerVars($serverVars);
15+
16+
// Look for a matching user.
17+
$userModel = config('auth.providers.users.model');
18+
$user = $userModel::firstWhere('email', $shibboleth->email());
19+
20+
if (empty($user)) {
21+
// User does not exist, so create them.
22+
$user = new $userModel;
23+
$user->name = $shibboleth->name();
24+
$user->email = $shibboleth->email();
25+
$user->password = Str::random(32);
26+
$user->save();
27+
Log::info("AuthorizeUser: Created user $user->email with ID $user->id.");
28+
}
29+
30+
auth()->login($user);
31+
Log::info("AuthorizeUser: Logged in user $user->email.");
32+
}
33+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
<?php
2+
3+
namespace CornellCustomDev\LaravelStarterKit\CUAuth\Middleware;
4+
5+
use Closure;
6+
use CornellCustomDev\LaravelStarterKit\CUAuth\DataObjects\ShibIdentity;
7+
use CornellCustomDev\LaravelStarterKit\CUAuth\Events\CUAuthenticated;
8+
use Illuminate\Http\Request;
9+
use Symfony\Component\HttpFoundation\Response;
10+
11+
class ApacheShib
12+
{
13+
public function handle(Request $request, Closure $next): Response
14+
{
15+
// If local login is allowed and someone is authenticated, let them through.
16+
if (config('cu-auth.allow_local_login') && auth()->check()) {
17+
return $next($request);
18+
}
19+
20+
// Shibboleth login route is allowed to pass through.
21+
if ($request->path() == route('cu-auth.shibboleth-login')) {
22+
return $next($request);
23+
}
24+
25+
// remoteUser will be set for authenticated users.
26+
$remoteUser = ShibIdentity::getRemoteUser($request);
27+
28+
// Unauthenticated get redirected to Shibboleth login.
29+
if (empty($remoteUser)) {
30+
return redirect()->route('cu-auth.shibboleth-login', [
31+
'redirect_uri' => $request->fullUrl(),
32+
]);
33+
}
34+
35+
// If requiring a local user, attempt to log in the user.
36+
if (config('cu-auth.require_local_user') && ! auth()->check()) {
37+
event(new CUAuthenticated($remoteUser));
38+
39+
// If the authenticated user is still not logged in, return a 403.
40+
if (! auth()->check()) {
41+
if (app()->runningInConsole()) {
42+
return response('Forbidden', Response::HTTP_FORBIDDEN);
43+
}
44+
abort(403);
45+
}
46+
}
47+
48+
return $next($request);
49+
}
50+
}

0 commit comments

Comments
 (0)