diff --git a/README.md b/README.md new file mode 100644 index 0000000..50aac98 --- /dev/null +++ b/README.md @@ -0,0 +1,34 @@ +# authkit2 + +Drop-in OIDC single-sign-on solution for PHP and Laravel projects. + +## Purpose + +Provide a low-impact way to integrate new and existing Laravel projects with a +single-sign-on solution providing identities for users and software across an +entire ecosystem of projects. + +## Features + +* Drop-in, almost zero-config solution. +* Integrates with existing Laravel user and authentication systems. +* Exposes core functionality in a framework-agnostic way to allow use outside + of Laravel. + +## Getting Started + +These documents generally reference Keycloak as the OpenID Connect (OIDC) +provider, however it should ostensibly work with any provider. + +### Laravel + +* [Installation](docs/LARAVEL_INSTALL.md) +* [Configuration](docs/LARAVEL_CONFIG.md) +* [Usage](docs/LARAVEL_USAGE.md) + +### Other + +* [Installation](docs/OTHER_INSTALL.md) +* [Usage](docs/OTHER_USAGE.md) + + diff --git a/config/authkit.php b/config/authkit.php index 16a4548..62cd0a2 100644 --- a/config/authkit.php +++ b/config/authkit.php @@ -14,7 +14,9 @@ return [ */ 'enable' => true, - // Scopes to request from the OIDC service + /** + * Scopes to request from the OIDC provider + */ 'scopes' => ['email'], /* @@ -61,7 +63,7 @@ return [ * * Currently authorization is not implemented. */ - 'enable' => true, + 'enable' => false, 'provider' => [ 'url' => env('AUTHZ_HOST', 'http://localhost:4466/') diff --git a/docs/LARAVEL_CONFIG.md b/docs/LARAVEL_CONFIG.md new file mode 100644 index 0000000..0d9a61a --- /dev/null +++ b/docs/LARAVEL_CONFIG.md @@ -0,0 +1,57 @@ +# authkit2 - Laravel Configuration + +How to configure authkit2 in your Laravel project. + +This requires that you have [installed](LARAVEL_INSTALL.md) authkit2. + +# Credentials + +There are two ways to configure the OIDC client: + +* With a JSON configuration file +* With environment variables + +## JSON Configuration + +In Keycloak, under `Clients -> Your Client -> Installation`, you can select the +"Keycloak OIDC JSON" and receive a configuration file in a format such as: + +```js +{ + "realm": "Test", + "auth-server-url": "http://localhost:8080/auth/", + "ssl-required": "external", + "resource": "test", + "credentials": { + "secret": "a0ee4936-ef77-4e4b-87f3-8b4a2706d53a" + }, + "confidential-port": 0 +} +``` + +Put this file in your application at `storage/app/auth.json`. You're done! + +## Environment Variables + +* `AUTHKIT_CLIENT_ID`: Client ID (`resource` in the above JSON) +* `AUTHKIT_CLIENT_SECRET`: Client Secret (`credentials.secret` in the above JSON) +* `AUTHKIT_ENDPOINT`: URL to the OIDC provider + +# Customization + +There are several other customizable values that are not initially exposed. If +you want to customize these values, you can publish the default configuration +and customize it. + +To publish the configuration, from your project's folder: +``` +$ php artisan vendor:publish --tag authkit2_config +``` + +You can then edit the configuration at: `config/authkit.php` + +Documentation for those options exists in the comments alongside them. + +# Next Steps + +See [Usage](LARAVEL_USAGE.md). diff --git a/docs/LARAVEL_INSTALL.md b/docs/LARAVEL_INSTALL.md new file mode 100644 index 0000000..e24a98a --- /dev/null +++ b/docs/LARAVEL_INSTALL.md @@ -0,0 +1,61 @@ +# authkit2 - Laravel Installation + +How to set up authkit2 in your Laravel project. + +# Install + +Add the authkit2 repository to your composer.json, e.g.,: + +```js + "repositories": [ + { + "type": "path", + "url": "../path/to/authkit2/repo/" + } + ] +``` + +// TODO: Update to VCS for release. + +Then install the package with composer: + +```js +$ composer require cmg/authkit2 +``` + +Laravel will automatically discover and register the appropriate service +providers. + +# Migrations + +There are two sets of migrations: + +* New Project: Removes the `password` and `email_verified_at` columns from the + users table. +* Existing Project: Makes the `password` column nullable. + +Both migrations are based around the default Laravel user table. If your table +is heavily customized, you will need to modify the migrations before running +them. + +## New Project + +In your project folder, run: + +``` +$ php artisan vendor:publish --tag=authkit2_migrate_fresh_project +$ php artisan migrate +``` + +## Existing Project + +In your project folder, run: + +``` +$ php artisan vendor:publish --tag=authkit2_migrate_existing_project +$ php artisan migrate +``` + +# Next Steps + +See [Configuration](LARAVEL_CONFIG.md). diff --git a/docs/LARAVEL_USAGE.md b/docs/LARAVEL_USAGE.md new file mode 100644 index 0000000..86b2fc6 --- /dev/null +++ b/docs/LARAVEL_USAGE.md @@ -0,0 +1,113 @@ +# authkit2 - Laravel Usage + +How to use authkit2 in your Laravel project. + +This requires that you have [installed](LARAVEL_INSTALL.md) and +[configured](LARAVEL_CONFIG.md) authkit2. + +# Basic Usage + +For basic usage in a new application there's nothing more to do. authkit2 +integrates with the default Laravel authentication system and will work out of +the box to sign users in and out of your application. + +You can explicitly trigger a login or logout by redirecting to: + +* `/auth/login` and +* `/auth/logout` + + +# Events + +Your application will be notified of logins, logins, and new users (to your +application) through [Laravel Events](https://laravel.com/docs/master/events). + +* `UserRegistration` +* `UserLogin` +* `UserLogout` + +## UserRegistration + +This event is fired when a user authenticated that has _not_ previously +authenticated through the OIDC provider. The event is passed the fields +returned by the OIDC provider (e.g., email, name). + +If a listener is registered, it is expected to return an instance of your User +model, initialized and saves, that will be tied to the OIDC ID the user has +authenticated with. + +For example, a minimal implementation to recreate the default behaviour would +be: + +```php + public function handle($event) + { + $user = new \App\Models\User(); + $user->name = $event->fields['name']; + $user->email = $event->fields['email']; + $user->save(); + return $user; + } +``` + +If you wanted an implementation to help migrate existing users to OIDC users, +something like the following may work: + +```php + public function handle($event) + { + // Try and load an existing user with the given email address + $user = \App\Models\User::where('email', $event->fields['email'])->first(); + + if (!isset($user)) + { + // If that user wasn't found, this is an entirely new user + $user = new \App\Models\User(); + $user->name = $event->fields['name']; + $user->email = $event->fields['email']; + $user->save(); + return $user; + } + else + { + // If the user was found, then we can tie them to the OIDC + // user. + $user->name = $event->fields['name']; + $user->email = $event->fields['email']; + // Clear the user's password to prevent non-OIDC logins going + // forward. + $user->password = null; + $user->save(); + return $user; + } + } +``` + +## UserLogin + +This event is fired when a user is authenticated (whether an existing user, or +after a UserRegistration event). The event is passed the user model and the +fields returned by the OIDC provider (e.g., email, name). + +If a listener is registered, it is expected to update the user model with any +updated fields returned by the OIDC provider. + +For example, a minimal implementation to recreate the default behaviour would +be: + +```php + public function handle($event) + { + $user = $event->user; + $user->name = $event->fields['name']; + $user->email = $event->fields['email']; + $user->save(); + } +``` + +## UserLogout + +This event is fired when the user tries to log out of your application. The +event is fired _before_ the user is logged out of the Laravel authentication +system or redirected to the OIDC provider to logout. + diff --git a/docs/OTHER_INSTALL.md b/docs/OTHER_INSTALL.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/OTHER_USAGE.md b/docs/OTHER_USAGE.md new file mode 100644 index 0000000..e69de29 diff --git a/src/Events/UserInfoEvent.php b/src/Events/UserInfoEvent.php new file mode 100644 index 0000000..557417d --- /dev/null +++ b/src/Events/UserInfoEvent.php @@ -0,0 +1,30 @@ + $fields + */ + public function __construct($user, array $fields) + { + parent::__construct($user); + $this->fields = $fields; + } +} diff --git a/src/Events/UserRegistration.php b/src/Events/UserRegistration.php index 88eab64..0b0d10c 100644 --- a/src/Events/UserRegistration.php +++ b/src/Events/UserRegistration.php @@ -8,6 +8,6 @@ namespace authkit2\Events; * Notification that a user logging in is brand new * to this app. */ -class UserRegistration extends UserEvent +class UserRegistration extends UserInfoEvent { } diff --git a/src/Http/Controllers/AuthenticationController.php b/src/Http/Controllers/AuthenticationController.php index 9e63125..db072d2 100644 --- a/src/Http/Controllers/AuthenticationController.php +++ b/src/Http/Controllers/AuthenticationController.php @@ -44,6 +44,8 @@ class AuthenticationController extends Controller */ public function callback(Request $request) { + $user_class = config('auth.providers.users.model'); + // Verify the passed in state value $this->user_flow->validateState($request->state); @@ -60,19 +62,33 @@ class AuthenticationController extends Controller if (!isset($token)) { // No token for that user, either create them or migrate - $register_event_result = event(new \authkit2\Events\UserRegistration($user_info)); - $user = null; - foreach ($register_event_result as $result) + $register_event_result = event(new \authkit2\Events\UserRegistration(null, $user_info)); + + if (sizeof($register_event_result) == 0) { - if (is_object($result)) + // If there were no register event handlers then just assume we're using laravel default + // stuff and create a user for them. + $user = new $user_class(); + $user->name = $user_info['name']; + $user->email = $user_info['email']; + $user->save(); + } + else + { + // Otherwise, expect one returned by the event handlers. + $user = null; + foreach ($register_event_result as $result) { - $user = $result; + if (is_object($result)) + { + $user = $result; + } } } if (!isset($user)) { - // TODO: Log a useful error + // TODO: Log a useful error message abort(500); } @@ -82,11 +98,23 @@ class AuthenticationController extends Controller $token->refresh_token = $oidc_token->getRefreshToken(); $token->user_id = $user->{$user->getAuthIdentifierName()}; $token->save(); - + } + else + { + $user = new $user_class(); + $user = $user_class::where($user->getAuthIdentifierName(), $token->user_id)->first(); } \Auth::login($user); - event(new \authkit2\Events\UserLogin($user, $user_info)); + $login_event_result = event(new \authkit2\Events\UserLogin($user, $user_info)); + if (!sizeof($login_event_result)) + { + // If nothing handled the login event, assume we're using laravel default + // everything and just go ahead and update the name/email + $user->name = $user_info['name']; + $user->email = $user_info['email']; + $user->save(); + } return redirect(url(config('authkit.authn.urls.post_login'))); }