Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

OIDC + AAD support for SSO #1448

Merged
merged 1 commit into from
Jan 22, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
10 changes: 10 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ APP_NAME="MunkiReport" # Was: SITENAME
# - "LDAP": LDAP Authentication
# - "AD": Active Directory Authentication
# - "SAML": SAML2 Single Sign-On
# - "OIDC": OpenID Connect via Socialite (only Azure AD supported right now)
# - Any combination of the above, comma separated.
#
# Authentication providers are checked in this order:
Expand Down Expand Up @@ -133,6 +134,15 @@ AUTH_AD_RECURSIVE_GROUPSEARCH=FALSE # No similar option in Adldap2-laravel
#LDAP_LOGIN_FALLBACK=true # Allow users to use local accounts if AD/LDAP cannot be reached
#LDAP_PASSWORD_SYNC=false # Sync AD/LDAP passwords back to local users database. This likely makes you more vulnerable

# AZURE ACTIVE DIRECTORY AUTHENTICATION
# -------------------------------------
#
#AZURE_CLIENT_ID=
#AZURE_CLIENT_SECRET=
#AZURE_REDIRECT_URI=/oauth2/callback/azure # Generally dont change this unless Laravel cant figure out your public URL
#AZURE_TENANT_ID=


# RECAPTCHA (Deprecated, replaced by NOCAPTCHA)
# ---------
# Enable reCaptcha Support on the Authentication Form
Expand Down
81 changes: 81 additions & 0 deletions app/Http/Controllers/Auth/Oauth2Controller.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
<?php

namespace App\Http\Controllers\Auth;

use App\Http\Controllers\Controller;
use App\Providers\RouteServiceProvider;
use App\User;
use Illuminate\Http\RedirectResponse;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Str;
use Laravel\Socialite\Facades\Socialite;
use Symfony\Component\HttpFoundation\Exception\BadRequestException;

class Oauth2Controller extends Controller
{
/**
* Where to redirect users after login.
*
* @var string
*/
protected $redirectTo = RouteServiceProvider::HOME;

/**
* Initiate an OpenID Connect authentication flow by redirecting to the specified provider.
*
* The provider name must match the name of a Socialite provider, declared in config/services.php (for now).
* We reserve the right to change the string to a named set of configurations.
*
* @param string $provider The socialite driver name to invoke a redirect with.
* @return RedirectResponse|void Redirection to the Identity Provider, or void if an error occurred.
* @throws BadRequestException when an invalid provider name is given.
*/
public function redirect(string $provider)
{
$services = config('services');
if (!Arr::has($services, $provider)) {
return abort(400, "Invalid provider requested or unconfigured");
}

return Socialite::driver($provider)->redirect();
}

/**
* Receive a callback from an OAuth2 authorization code flow sign-on.
*
* The provider name must match the name of a Socialite provider, declared in config/services.php (for now).
* We reserve the right to change the string to a named set of configurations.
*
* @param string $provider The socialite driver that initiated the oauth2 flow.
* @return RedirectResponse Redirection to the configured home page (if successful)
*/
public function callback(string $provider)
{
$services = config('services');
if (!Arr::has($services, $provider)) {
return abort(400, "Invalid provider requested or unconfigured");
}

$user = Socialite::driver('azure')->user();

$databaseUser = User::where('objectguid', $user->getId())->first();
if ($databaseUser) {
$databaseUser->update([
'email' => $user->getEmail(),
'display_name' => $user->getName(),
]);
} else {
$databaseUser = User::create([
'name' => $user->getName(),
'objectguid' => $user->getId(),
'email' => $user->getEmail(),
'display_name' => $user->getName(),
'password' => Str::random(40),
]);
}

Auth::login($databaseUser);
return redirect($this->redirectTo);
}
}
3 changes: 3 additions & 0 deletions app/Providers/EventServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ class EventServiceProvider extends ServiceProvider
// should apply.
MachineGroupMembership::class, // decides which machinegroup memberships should apply.
],
\SocialiteProviders\Manager\SocialiteWasCalled::class => [
\SocialiteProviders\Azure\AzureExtendSocialite::class.'@handle',
],
];

/**
Expand Down
2 changes: 2 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
"laravel/sanctum": "^2.13",
"laravel/scout": "8.6",
"laravel/slack-notification-channel": "^2.3",
"laravel/socialite": "^5.3",
"laravel/tinker": "^2.5",
"laravel/ui": "^3.0",
"munkireport/applications": "dev-master",
Expand Down Expand Up @@ -107,6 +108,7 @@
"pragmarx/firewall": "^2.3",
"rodneyrehm/plist": "^2.0",
"rybakit/msgpack": "^0.9.0",
"socialiteproviders/microsoft-azure": "^5.0",
"symfony/yaml": "^5.1",
"teamtnt/laravel-scout-tntsearch-driver": "^11.3",
"wikimedia/composer-merge-plugin": "^2.0",
Expand Down
1 change: 1 addition & 0 deletions config/app.php
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@
/*
* Package Service Providers...
*/
\SocialiteProviders\Manager\ServiceProvider::class,
Aacotroneo\Saml2\Saml2ServiceProvider::class,
Adldap\Laravel\AdldapServiceProvider::class,
Adldap\Laravel\AdldapAuthServiceProvider::class,
Expand Down
12 changes: 12 additions & 0 deletions config/services.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,16 @@
'teams' => [
'webhook_url' => env('TEAMS_WEBHOOK_URL'),
],

/*
* Single Sign-On Providers (via Socialite)
*/
'azure' => [
'client_id' => env('AZURE_CLIENT_ID'),
'client_secret' => env('AZURE_CLIENT_SECRET'),
'redirect' => env('AZURE_REDIRECT_URI'),
'tenant' => env('AZURE_TENANT_ID'),
'logout_url' => 'https://login.microsoftonline.com/'.env('AZURE_TENANT_ID').'/oauth2/v2.0/logout?post_logout_redirect_uri=',
'proxy' => env('HTTPS_PROXY') // optionally
]
];
73 changes: 73 additions & 0 deletions docs/authentication/azure-ad.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# Authentication with Azure AD #

This guide describes setting up MunkiReport-PHP with Azure AD for single sign-on.

## Step 1 - App Registration ##

Prerequisites:

- Must be logged in to the Azure AD portal.
- Must have access to see and create "App Registrations".

1. From the Azure AD Portal, navigate to the **App Registrations** section.
2. Click on **New registration** to register a new Application.
3. Enter the following details:

- **name:** MunkiReport-PHP
- **supported account types:** Accounts in this organizational directory only.
- **redirect application type:** Choose "Web".
- **redirect uri:** (The URL to your MunkiReport installation, with the path `/oauth2/redirect/azure` appended), eg.
`https://munkireport.local/oauth2/redirect/azure` if you access munkireport at `https://munkireport.local`.

_You should now be redirected to your newly registered "MunkiReport-PHP" application, if not: you can access the registration
detail at any time by visiting the **App Registrations** page, and clicking on the "MunkiReport-PHP" registration._

4. In the registration overview you will need to record a couple of details which will become MunkiReport configuration:
- **Application (client) ID**: This value will be added to `AZURE_CLIENT_ID`.
- **Directory (tenant) ID**: This value will be added to `AZURE_TENANT_ID`.

5. Create a new secret for the application to use. From the app registration overview, navigate to **Certificates & secrets**.
6. Choose the **Client secrets** tab.
7. Click the button **New client secret**.
8. Describe the secret, it's not important for this application to provide a detailed description.
9. Select an expiry length.
10. Click **Add**, you will be shown the secret value **ONLY ONCE**, so make a copy of it now.

## Step 2 - MunkiReport PHP Configuration ##

Prerequisites:

- MunkiReport PHP should already be running and configured with an SSL certificate. Azure AD will complain if you are
trying to perform a secure auth code login over a connection that could be captured.

1. Now you will need the **Application (client) ID** and **Directory (tenant) ID** from item 4 in the app registration step,
and the **Client secret** from item 10.
2. Depending on how you configure MunkiReport (`.env` file, environment variables in the web server configuration, or docker `-e` variables),
you will need to provide the following (the angle brackets are not literally required):

```
AZURE_CLIENT_ID=<Application (client) ID>
AZURE_TENANT_ID=<Directory (tenant) ID>
AZURE_REDIRECT_URI=/oauth2/callback/azure
AZURE_CLIENT_SECRET=<Client Secret>
```

## Step 3 - Test ##

You should now see an extra sign-in button on the MunkiReport-PHP login page which takes you to Azure AD to authenticate.

The first time you sign-in, you may see a consent screen, asking you to consent that MunkiReport-PHP can read your information.

This is normal and necessary for MunkiReport-PHP to authenticate and fill user details as part of the sign-in process. You can
provide global admin consent to prevent other users from seeing this screen. Global Admin consent is outside of the scope of this
guide.

## In depth ##

### Token Claims ###

By default, the Microsoft Azure AD Socialite Provider only processes the token claims for `id`, `email`, and `name`.

It will not retrieve any other claims listed in the token by default.

Support for Azure AD App Roles and/or AAD Group ID membership may be added in a future version.
25 changes: 25 additions & 0 deletions docs/illuminate/v6-upgrade-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,4 +120,29 @@ which we will refer to as the v6 module spec.
The current module style still works through a compatibility layer, so no changes are needed immediately
to make older modules work.

## New Features ##

### Application Logging ###

All application errors are logged to the default application log, which lives in `storage/logs/laravel.log`.

### API Keys ###

The MunkiReport interface now provides you with a way to manage API Keys that you can use to interact with the REST API
or GraphQL API without using your user credentials. This is the only way to interact with the API if you are signing in
with an sso federated user.

### OpenID Connect (OIDC) via Laravel Socialite ###

MunkiReport-PHP now has support for OpenID Connect.

At this stage, we only test with Azure AD as the identity platform, but you are welcome to try others.

You can enable OIDC sign-in for Azure AD by including this environment variable in .env or the web application environment:

AUTH_METHODS="OIDC"

You can still provide a fallback to local database authentication (in case Azure AD is not available) like so:

AUTH_METHODS="OIDC,LOCAL"

4 changes: 4 additions & 0 deletions resources/views/auth/login.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,10 @@
@if (Str::contains(config('auth.methods'), 'SAML'))
<a class="btn btn-primary" href="{{ route('saml2_login', 'default') }}">Sign in with SAML</a>
@endif

@if (Str::contains(config('auth.methods'), 'OIDC'))
<a class="btn btn-primary" href="{{ route('oauth2_redirect', 'azure') }}">Sign in with OIDC</a>
@endif
</div>
</div>
</div>
Expand Down
5 changes: 5 additions & 0 deletions routes/web.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@
// Users cannot self-register
Auth::routes(['register' => false]);

Route::get('/oauth2/redirect/{provider}',
[\App\Http\Controllers\Auth\Oauth2Controller::class, 'redirect'])->name('oauth2_redirect');
Route::get('/oauth2/callback/{provider}',
[\App\Http\Controllers\Auth\Oauth2Controller::class, 'callback'])->name('oauth2_callback');

Route::redirect('/', '/show/dashboard/default');

Route::middleware(['auth'])->group(function () {
Expand Down