Setup authentik sso support

This commit is contained in:
2025-11-01 15:47:28 +01:00
parent 32028ce3ce
commit b60165648d
18 changed files with 416 additions and 19 deletions
+6
View File
@@ -74,3 +74,9 @@ MFA_ENABLED=true
# Enable DAV support (beta feature)
DAV_ENABLED=true
# OIDC Configuration for Authentik
AUTHENTIK_BASE_URL=
AUTHENTIK_CLIENT_ID=
AUTHENTIK_CLIENT_SECRET=
AUTHENTIK_REDIRECT_URI=http://localhost:8080/auth/authentik/callback
+6
View File
@@ -142,6 +142,12 @@ DAV_ENABLED=true
PASSPORT_PASSWORD_GRANT_CLIENT_ID=
PASSPORT_PASSWORD_GRANT_CLIENT_SECRET=
# OIDC Authentication via Authentik
AUTHENTIK_BASE_URL=
AUTHENTIK_CLIENT_ID=
AUTHENTIK_CLIENT_SECRET=
AUTHENTIK_REDIRECT_URI="${APP_URL}/auth/authentik/callback"
# Allow to access general statistics about your instance through a public API
# call
ALLOW_STATISTICS_THROUGH_PUBLIC_API_ACCESS=false
@@ -40,7 +40,7 @@ class OAuthController extends Controller
{
$this->encrypter = $encrypter;
if (config('app.debug')) {
if (config('app.debug') && class_exists('Barryvdh\Debugbar\Facades\Debugbar')) {
Debugbar::disable();
}
}
@@ -0,0 +1,143 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Models\User\User;
use App\Models\Account\Account;
use App\Helpers\RequestHelper;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Laravel\Socialite\Facades\Socialite;
use Illuminate\Support\Str;
class OIDCController extends Controller
{
/**
* Redirect the user to the Authentik authentication page.
*/
public function redirectToProvider()
{
$driver = Socialite::driver('authentik');
\Log::info('OIDC Redirect scopes', ['scopes' => $driver->getScopes()]);
return $driver
->redirect();
}
/**
* Obtain the user information from Authentik.
*/
public function handleProviderCallback(Request $request)
{
\Log::info('OIDC Callback started', ['url' => $request->fullUrl(), 'params' => $request->all()]);
// Add early error handling
if (!$request->has('code')) {
\Log::error('OIDC Callback missing authorization code', ['params' => $request->all()]);
return redirect('/login')->withErrors(['msg' => 'Authorization code missing. Please try again.']);
}
try {
$authentikUser = Socialite::driver('authentik')->user();
\Log::info('OIDC User retrieved', [
'email' => $authentikUser->email,
'id' => $authentikUser->id,
'name' => $authentikUser->name,
'raw_user' => $authentikUser->user,
'all_data' => $authentikUser->getRaw()
]);
} catch (\Exception $e) {
\Log::error('OIDC Authentication failed', ['error' => $e->getMessage()]);
return redirect('/login')->withErrors(['msg' => 'Authentication failed. Please try again.']);
}
// Check if user already exists by OIDC subject ID
$user = User::where('oidc_sub', $authentikUser->id)->first();
if ($user) {
\Log::info('OIDC User found by subject ID', ['user_id' => $user->id]);
// Update user info from OIDC provider
$this->updateUserOIDCInfo($user, $authentikUser);
Auth::login($user);
\Log::info('OIDC User logged in successfully', ['user_id' => $user->id]);
return redirect()->intended('/dashboard');
}
// Check if user exists by email (if email is available)
if (!empty($authentikUser->email)) {
$existingUser = User::where('email', $authentikUser->email)->first();
if ($existingUser) {
\Log::info('OIDC User found by email', ['user_id' => $existingUser->id]);
// Link existing user to OIDC provider
$this->updateUserOIDCInfo($existingUser, $authentikUser);
Auth::login($existingUser);
\Log::info('OIDC Existing user logged in successfully', ['user_id' => $existingUser->id]);
return redirect()->intended('/dashboard');
}
}
// Create new user
\Log::info('OIDC Creating new user', ['email' => $authentikUser->email]);
try {
$user = $this->createUser($authentikUser);
Auth::login($user);
\Log::info('OIDC New user created and logged in successfully', ['user_id' => $user->id]);
return redirect()->intended('/dashboard');
} catch (\Exception $e) {
\Log::error('OIDC User creation failed', ['error' => $e->getMessage()]);
return redirect('/login')->withErrors(['msg' => 'User creation failed. Please try again.']);
}
} /**
* Create a new user from OIDC provider data.
*/
private function createUser($authentikUser)
{
// Generate a unique username if name is not provided
$firstName = $authentikUser->user['given_name'] ?? explode(' ', $authentikUser->name)[0] ?? 'User';
$lastName = $authentikUser->user['family_name'] ?? (explode(' ', $authentikUser->name)[1] ?? '');
// Require email from OIDC provider
if (empty($authentikUser->email)) {
\Log::error('OIDC User missing email', ['user_id' => $authentikUser->id, 'raw_data' => $authentikUser->getRaw()]);
throw new \Exception('Email not provided by OIDC provider');
}
$email = $authentikUser->email;
// Create account and user using Monica's default method
// Generate a secure random password for OIDC users (they won't use it)
$randomPassword = Str::random(32);
$account = \App\Models\Account\Account::createDefault(
$firstName,
$lastName,
$email,
$randomPassword,
\App\Helpers\RequestHelper::ip(),
'en' // Default locale
);
// Get the created user
$user = $account->users()->first();
// Mark email as verified for OIDC users (since they're authenticated via OIDC provider)
$user->markEmailAsVerified();
// Update user with OIDC info
$this->updateUserOIDCInfo($user, $authentikUser);
return $user;
}
/**
* Update user with OIDC provider information.
*/
private function updateUserOIDCInfo(User $user, $authentikUser)
{
$user->update([
'oidc_sub' => $authentikUser->id,
'oidc_email' => $authentikUser->email,
]);
}
}
+2
View File
@@ -49,6 +49,8 @@ class User extends Authenticatable implements MustVerifyEmail, HasLocalePreferen
'temperature_scale',
'name_order',
'google2fa_secret',
'oidc_sub',
'oidc_provider',
];
/**
+5
View File
@@ -29,6 +29,11 @@ class AppServiceProvider extends ServiceProvider
*/
public function boot()
{
// Register Authentik Socialite Provider
\Illuminate\Support\Facades\Event::listen(function (\SocialiteProviders\Manager\SocialiteWasCalled $event) {
$event->extendSocialite('authentik', \SocialiteProviders\Authentik\Provider::class);
});
if (App::runningInConsole()) {
Command::macro('exec', function (string $message, string $commandline) {
// @codeCoverageIgnoreStart
+3
View File
@@ -18,6 +18,9 @@ class EventServiceProvider extends ServiceProvider
\Illuminate\Auth\Events\PasswordReset::class => [
\App\Listeners\LogoutUserDevices::class,
],
\SocialiteProviders\Manager\SocialiteWasCalled::class => [
\SocialiteProviders\Authentik\AuthentikExtendSocialite::class.'@handle',
],
];
/**
@@ -0,0 +1,29 @@
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use Laravel\Socialite\Facades\Socialite;
use SocialiteProviders\Authentik\Provider;
class SocialiteServiceProvider extends ServiceProvider
{
/**
* Register services.
*/
public function register(): void
{
//
}
/**
* Bootstrap services.
*/
public function boot(): void
{
Socialite::extend('authentik', function ($app) {
$config = $app['config']['services.authentik'];
return Socialite::buildProvider(Provider::class, $config);
});
}
}
+2
View File
@@ -47,6 +47,8 @@
"rinvex/countries": "^8.1",
"sabre/dav": "^4.0",
"sentry/sentry-laravel": "^2.0",
"socialiteproviders/authentik": "^5.2.0",
"socialiteproviders/manager": "^4.8.1",
"spatie/macroable": "^2.0",
"stevebauman/location": "^6.1",
"symfony/http-client": "^6.0",
Generated
+129 -5
View File
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "cf135499df1575ba5a5747a5fcf4289c",
"content-hash": "08b68a4ecc65c514bc69610bc708dfef",
"packages": [
{
"name": "asbiin/laravel-adorable",
@@ -8601,6 +8601,130 @@
],
"time": "2022-10-13T09:21:29+00:00"
},
{
"name": "socialiteproviders/authentik",
"version": "5.2.0",
"source": {
"type": "git",
"url": "https://github.com/SocialiteProviders/Authentik.git",
"reference": "4cf129cf04728a38e0531c54454464b162f0fa66"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/SocialiteProviders/Authentik/zipball/4cf129cf04728a38e0531c54454464b162f0fa66",
"reference": "4cf129cf04728a38e0531c54454464b162f0fa66",
"shasum": ""
},
"require": {
"ext-json": "*",
"php": "^8.0",
"socialiteproviders/manager": "^4.4"
},
"type": "library",
"autoload": {
"psr-4": {
"SocialiteProviders\\Authentik\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "rf152",
"email": "git@rf152.co.uk"
}
],
"description": "Authentik OAuth2 Provider for Laravel Socialite",
"keywords": [
"authentik",
"laravel",
"oauth",
"provider",
"socialite"
],
"support": {
"docs": "https://socialiteproviders.com/authentik",
"issues": "https://github.com/socialiteproviders/providers/issues",
"source": "https://github.com/socialiteproviders/providers"
},
"time": "2023-11-07T22:21:16+00:00"
},
{
"name": "socialiteproviders/manager",
"version": "v4.8.1",
"source": {
"type": "git",
"url": "https://github.com/SocialiteProviders/Manager.git",
"reference": "8180ec14bef230ec2351cff993d5d2d7ca470ef4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/SocialiteProviders/Manager/zipball/8180ec14bef230ec2351cff993d5d2d7ca470ef4",
"reference": "8180ec14bef230ec2351cff993d5d2d7ca470ef4",
"shasum": ""
},
"require": {
"illuminate/support": "^8.0 || ^9.0 || ^10.0 || ^11.0 || ^12.0",
"laravel/socialite": "^5.5",
"php": "^8.1"
},
"require-dev": {
"mockery/mockery": "^1.2",
"phpunit/phpunit": "^9.0"
},
"type": "library",
"extra": {
"laravel": {
"providers": [
"SocialiteProviders\\Manager\\ServiceProvider"
]
}
},
"autoload": {
"psr-4": {
"SocialiteProviders\\Manager\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Andy Wendt",
"email": "andy@awendt.com"
},
{
"name": "Anton Komarev",
"email": "a.komarev@cybercog.su"
},
{
"name": "Miguel Piedrafita",
"email": "soy@miguelpiedrafita.com"
},
{
"name": "atymic",
"email": "atymicq@gmail.com",
"homepage": "https://atymic.dev"
}
],
"description": "Easily add new or override built-in providers in Laravel Socialite.",
"homepage": "https://socialiteproviders.com",
"keywords": [
"laravel",
"manager",
"oauth",
"providers",
"socialite"
],
"support": {
"issues": "https://github.com/socialiteproviders/manager/issues",
"source": "https://github.com/socialiteproviders/manager"
},
"time": "2025-02-24T19:33:30+00:00"
},
{
"name": "spatie/macroable",
"version": "2.0.0",
@@ -19666,8 +19790,8 @@
"aliases": [],
"minimum-stability": "stable",
"stability-flags": {
"werk365/etagconditionals": 20,
"roave/security-advisories": 20
"roave/security-advisories": 20,
"werk365/etagconditionals": 20
},
"prefer-stable": false,
"prefer-lowest": false,
@@ -19679,6 +19803,6 @@
"ext-intl": "*",
"ext-redis": "*"
},
"platform-dev": [],
"plugin-api-version": "2.2.0"
"platform-dev": {},
"plugin-api-version": "2.6.0"
}
+1
View File
@@ -199,6 +199,7 @@ return [
Vluzrmos\LanguageDetector\Providers\LanguageDetectorServiceProvider::class,
App\Providers\RouteServiceProvider::class,
Laravel\Socialite\SocialiteServiceProvider::class,
\SocialiteProviders\Manager\ServiceProvider::class,
Intervention\Image\ImageServiceProvider::class,
Laravel\Cashier\CashierServiceProvider::class,
Laravel\Passport\PassportServiceProvider::class,
+7
View File
@@ -40,4 +40,11 @@ return [
],
],
'authentik' => [
'base_url' => env('AUTHENTIK_BASE_URL'),
'client_id' => env('AUTHENTIK_CLIENT_ID'),
'client_secret' => env('AUTHENTIK_CLIENT_SECRET'),
'redirect' => env('AUTHENTIK_REDIRECT_URI')
],
];
@@ -0,0 +1,23 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddOidcFieldsToUsersTable extends Migration
{
public function up()
{
Schema::table('users', function (Blueprint $table) {
$table->string('oidc_sub')->nullable()->index()->comment('OIDC Subject ID from provider');
$table->string('oidc_provider')->nullable()->comment('OIDC Provider name (e.g., authentik)');
});
}
public function down()
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn(['oidc_sub', 'oidc_provider']);
});
}
}
+1 -1
View File
@@ -24,10 +24,10 @@ services:
- ./database:/var/www/html/database
- ./resources:/var/www/html/resources
- ./routes:/var/www/html/routes
- ./config:/var/www/html/config
mysql:
image: mysql:8
command: --default-authentication-plugin=mysql_native_password
environment:
- MYSQL_ROOT_PASSWORD=sekret_root_password
- MYSQL_DATABASE=monica
+5 -1
View File
@@ -45,9 +45,13 @@ return [
'password_forget' => 'Forget your password?',
'password_reset' => 'Reset your password',
'use_recovery' => 'Or you can use a <a href=":url">recovery code</a>',
'signup_no_account' => 'Dont have an account?',
'signup_no_account' => 'Don\'t have an account?',
'signup' => 'Sign up',
'create_account' => 'Create the first account by <a href=":url">signing up</a>',
'or' => 'or',
'login_with_oidc' => 'Login with Single Sign-On',
'change_language_title' => 'Change language:',
'change_language' => 'Change language to :lang',
+14
View File
@@ -57,6 +57,20 @@
</label>
</div>
</form>
@if(config('services.authentik.base_url'))
<div class="mt-4 mb-4 text-center">
<div class="divider">
<span class="divider-text">{{ trans('auth.or') }}</span>
</div>
<a href="{{ route('auth.oidc.redirect') }}" class="btn btn-outline-secondary btn-block mt-3">
<i class="fa fa-sign-in-alt"></i>&nbsp;{{ trans('auth.login_with_oidc') }}
</a>
</div>
@endif
<form style="display: none;">
<div class="form-group links">
<ul>
<li>{{ trans('auth.password_forget') }}&nbsp;<a href="{{ route('password.request') }}">{{ trans('auth.password_reset') }}</a></li>
+4
View File
@@ -20,6 +20,10 @@ Route::get('/', 'Auth\LoginController@showLoginOrRegister')->name('loginRedirect
Auth::routes(['verify' => true]);
// OIDC Authentication routes
Route::get('/auth/authentik', 'Auth\OIDCController@redirectToProvider')->name('auth.oidc.redirect');
Route::get('/auth/authentik/callback', 'Auth\OIDCController@handleProviderCallback')->name('auth.oidc.callback');
// Redirect .well-known urls (https://en.wikipedia.org/wiki/List_of_/.well-known/_services_offered_by_webservers)
Route::permanentRedirect('/.well-known/carddav', '/dav/');
Route::permanentRedirect('/.well-known/caldav', '/dav/');
+30 -6
View File
@@ -6,7 +6,7 @@
### This file is based off of the `apache` variant in the above mentioned repo
###
FROM php:8.1-apache
FROM php:8.2-apache
# opencontainers annotations https://github.com/opencontainers/image-spec/blob/master/annotations.md
LABEL org.opencontainers.image.authors="Alexis Saettler <alexis@saettler.org>" \
@@ -75,13 +75,22 @@ RUN set -ex; \
# reset apt-mark's "manual" list so that "purge --auto-remove" will remove all build dependencies
apt-mark auto '.*' > /dev/null; \
apt-mark manual $savedAptMark; \
\
# find and mark runtime dependencies as manual to prevent their removal
runDeps="$( \
ldd "$(php -r 'echo ini_get("extension_dir");')"/*.so \
| awk '/=>/ { print $3 }' \
| sort -u \
| xargs -r dpkg-query -S \
| xargs -r dpkg-query -S 2>/dev/null \
| cut -d: -f1 \
| sort -u \
| xargs -rt apt-mark manual; \
)"; \
if [ -n "$runDeps" ]; then \
apt-mark manual $runDeps; \
fi; \
\
# determine current library versions and mark them manual
apt list --installed | grep -E "lib(png|icu|zip|memcached|freetype|jpeg|webp|gmp|xml|magick)" | cut -d/ -f1 | xargs -r apt-mark manual 2>/dev/null || true; \
\
apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false; \
rm -rf /var/lib/apt/lists/*
@@ -150,8 +159,17 @@ RUN set -ex; \
COPY --chown=www-data:www-data .env.example .env
# Composer installation
COPY scripts/docker/install-composer.sh /usr/local/sbin/
RUN install-composer.sh
RUN set -ex; \
EXPECTED_SIGNATURE=$(curl -sS https://composer.github.io/installer.sig); \
curl -sS -o composer-setup.php https://getcomposer.org/installer; \
ACTUAL_SIGNATURE=$(openssl sha384 composer-setup.php | cut -d' ' -f2); \
if [ "$EXPECTED_SIGNATURE" != "$ACTUAL_SIGNATURE" ]; then \
>&2 echo 'ERROR: Invalid installer signature'; \
rm composer-setup.php; \
exit 1; \
fi; \
php composer-setup.php --quiet --install-dir=/usr/local/bin --filename=composer; \
rm composer-setup.php
# Install composer dependencies
RUN set -ex; \
@@ -164,7 +182,7 @@ RUN set -ex; \
# Install node dependencies
RUN set -ex; \
\
curl -fsSL https://deb.nodesource.com/setup_18.x | bash -; \
curl -fsSL https://deb.nodesource.com/setup_20.x | bash -; \
apt-get install -y nodejs; \
npm install -g yarn; \
yarn run inst; \
@@ -177,5 +195,11 @@ COPY scripts/docker/entrypoint.sh \
scripts/docker/queue.sh \
/usr/local/bin/
# Fix Windows line endings and make scripts executable
RUN apt-get update && apt-get install -y dos2unix && \
dos2unix /usr/local/bin/entrypoint.sh /usr/local/bin/cron.sh /usr/local/bin/queue.sh && \
chmod +x /usr/local/bin/entrypoint.sh /usr/local/bin/cron.sh /usr/local/bin/queue.sh && \
apt-get purge -y dos2unix && apt-get autoremove -y && rm -rf /var/lib/apt/lists/*
ENTRYPOINT ["entrypoint.sh"]
CMD ["apache2-foreground"]