diff --git a/app/Services/Applications/OIDC/OidcConfigResolver.php b/app/Services/Applications/OIDC/OidcConfigResolver.php
index 364338a..627db3f 100644
--- a/app/Services/Applications/OIDC/OidcConfigResolver.php
+++ b/app/Services/Applications/OIDC/OidcConfigResolver.php
@@ -33,4 +33,14 @@ public function needSignOutUris(): bool
return true;
// return ApplicationTypeEnum::Machine !== $this->applicationType;
}
+
+ public function isConfidential(\Laravel\Passport\Client $client): bool
+ {
+ return $client->confidential();
+ }
+
+ public function requiresPkce(\Laravel\Passport\Client $client): bool
+ {
+ return ! $client->confidential();
+ }
}
diff --git a/app/Services/Applications/OIDC/OidcService.php b/app/Services/Applications/OIDC/OidcService.php
index 600d42c..e1c49a4 100644
--- a/app/Services/Applications/OIDC/OidcService.php
+++ b/app/Services/Applications/OIDC/OidcService.php
@@ -7,11 +7,12 @@
use App\Data\Application\{CreatedApplicationData, StoreOIDCAppData};
use App\Data\ConnectedApp\ConnectAppRequest;
use App\Enums\{ConnectionProtocolEnum, ConnectionStatusEnum};
-use App\Models\{ConnectionProtocol, ConnectionStatus};
+use App\Models\{ConnectedApp, ConnectionProtocol, ConnectionStatus};
use App\Services\{ApplicationLogoUploader, ConnectedAppService};
use App\Services\Applications\OIDC\Factory\ClientCreationFactory;
use Illuminate\Support\Facades\{DB, Log};
use Illuminate\Support\Str;
+use Laravel\Passport\Client;
use Throwable;
readonly class OidcService
@@ -43,6 +44,7 @@ public function createApp(StoreOIDCAppData $data): CreatedApplicationData
connectionStatusId: ConnectionStatus::query()->where('name', ConnectionStatusEnum::Connected->value)->first()->id,
settings: [
'access_url' => $data->accessUrl,
+ 'sign_out_uris' => $data->signOutUris,
],
logo: $logo,
)
@@ -74,4 +76,33 @@ public function createApp(StoreOIDCAppData $data): CreatedApplicationData
throw $e;
}
}
+
+ public function getPassportClient(ConnectedApp $app): ?Client
+ {
+ return Client::where('name', $app->name)->first();
+ }
+
+ public function generateNewSecret(Client $client): string
+ {
+ $plainSecret = Str::random(40);
+ $client->forceFill(['secret' => bcrypt($plainSecret)])->save();
+
+ return $plainSecret;
+ }
+
+ public function toggleStatus(ConnectedApp $app): void
+ {
+ $statusEnum = $app->status->name === ConnectionStatusEnum::Connected->value
+ ? ConnectionStatusEnum::Disconnected
+ : ConnectionStatusEnum::Connected;
+
+ $newStatus = ConnectionStatus::where('name', $statusEnum->value)->firstOrFail();
+ $app->connection_status_id = $newStatus->id;
+ $app->save();
+ }
+
+ public function getSignOutUris(ConnectedApp $app): array
+ {
+ return data_get($app, 'settings.sign_out_uris', []);
+ }
}
diff --git a/resources/views/pages/apps/oidc/⚡show.blade.php b/resources/views/pages/apps/oidc/⚡show.blade.php
new file mode 100644
index 0000000..a220792
--- /dev/null
+++ b/resources/views/pages/apps/oidc/⚡show.blade.php
@@ -0,0 +1,479 @@
+oidcService = $oidcService;
+ $this->configResolver = $configResolver;
+ }
+
+ public function mount(int $id): void
+ {
+ $this->authorize(AppPermissionEnum::Read->value);
+ $this->appId = $id;
+ $this->loadAppData();
+ }
+
+ public function loadAppData(): void
+ {
+ $this->app = ConnectedApp::with(['protocol', 'status', 'roles'])->findOrFail($this->appId);
+ $this->passportClient = $this->oidcService->getPassportClient($this->app);
+ $this->signOutUris = $this->oidcService->getSignOutUris($this->app);
+ }
+
+ public function generateNewSecret(): void
+ {
+ $this->authorize(AppPermissionEnum::Update->value);
+
+ if (!$this->passportClient) {
+ $this->error('No corresponding Passport Client found to update.');
+ return;
+ }
+
+ $this->attempt(
+ action: function () {
+ $plainSecret = $this->oidcService->generateNewSecret($this->passportClient);
+ $this->newSecretPlain = $plainSecret;
+ $this->success('A new client secret has been generated! Make sure to copy it now.');
+ $this->loadAppData();
+ },
+ successMessage: 'New client secret generated successfully!',
+ showSuccess: false
+ );
+ }
+
+ public function toggleStatus(): void
+ {
+ $this->authorize(AppPermissionEnum::Update->value);
+
+ $this->attempt(
+ action: function () {
+ $this->oidcService->toggleStatus($this->app);
+ $this->loadAppData();
+ },
+ successMessage: 'Application status updated successfully!'
+ );
+ }
+
+ public function goBack(): void
+ {
+ $this->redirectRoute('apps.index', navigate: true);
+ }
+
+ public function editApp(): void
+ {
+ $this->redirectRoute('apps.edit', ['id' => $this->appId], navigate: true);
+ }
+
+ public function requiresPkce(Client $client)
+ {
+ return $this->configResolver->requiresPkce($client);
+ }
+};
+?>
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ $app->name }}
+
+
+
Protocol: {{ strtoupper($app->protocol->name) }}
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ @if($newSecretPlain)
+
+
+
+
+
+
+ @endif
+
+ @if(!$passportClient)
+
+ @endif
+
+
+
+
+
Client Credentials
+
+
+
+
+
Client ID
+
+
+ @if($passportClient)
+ id }}'); alert('Client ID copied!');"
+ tooltip="Copy Client ID"
+ />
+ @endif
+
+
+
+
+
+ Public identifier for the client that is required for all OAuth flows.
+
+
+
+
+
+
+
+
+
Proof Key for Code Exchange (PKCE)
+
+
+
+
+
+
+
+
+
+
+
Client Secrets
+ @if($passportClient)
+
+ @endif
+
+
+
+
+
+ | Creation date |
+ Secret |
+ Status |
+
+
+
+ @if($passportClient)
+
+ |
+ {{ $passportClient->updated_at?->format('M j, Y H:i:s') ?? 'N/A' }}
+ |
+
+ ••••••••••••••••••••••••••••••••••••••••
+ |
+
+
+ {{ !$passportClient->revoked ? 'Active' : 'Revoked' }}
+
+ |
+
+ @else
+
+ | No secrets
+ available
+ |
+
+ @endif
+
+
+
+
+
+
+
+
+
+
+
+
+
Sign In Redirect URIs
+
+
+ @if(!empty($passportClient?->redirect_uris))
+ @foreach($passportClient->redirect_uris as $uri)
+
+
+ {{ $uri }}
+
+ @endforeach
+ @else
+
No Sign In Redirect URIs configured
+ @endif
+
+
+
+
+
+
+
Sign Out Redirect URIs
+
+
+ @if(!empty($signOutUris))
+ @foreach($signOutUris as $uri)
+
+
+ {{ $uri }}
+
+ @endforeach
+ @else
+
No Sign Out Redirect URIs configured
+ @endif
+
+
+
+
+
+
+
+
+
+
+
Assigned Roles
+
+
+
+
+
+ | Role Name |
+ Priority |
+ Access Type |
+
+
+
+ @forelse($app->roles as $role)
+
+ |
+ {{ $role->name }}
+ |
+
+ {{ $role->priority }}
+ |
+
+
+ SSO Access
+
+ |
+
+ @empty
+
+ |
+ No specific roles assigned. Accessible by everyone depending on user access
+ setting.
+ |
+
+ @endforelse
+
+
+
+
+
+
+
+
+
+
+
+
+
Granted OAuth/OIDC
+ Scopes
+
+
+
+
+
+
+
+
+
openid
+
Allows the application to fetch ID
+ tokens for authenticating users.
+
+
+
+
+
+
+
+
profile
+
Allows reading default profile
+ fields like name, picture, and locale.
+
+
+
+
+
+
+
+
email
+
Grants access to the user's primary
+ email address.
+
+
+
+
+
+
+
+
offline_access
+
Allows requesting refresh tokens to
+ maintain access offline.
+
+
+
+
+
+
+
+
+
+
+
diff --git a/resources/views/pages/apps/⚡index.blade.php b/resources/views/pages/apps/⚡index.blade.php
index 60fafad..a4dc812 100644
--- a/resources/views/pages/apps/⚡index.blade.php
+++ b/resources/views/pages/apps/⚡index.blade.php
@@ -7,6 +7,7 @@
use App\Enums\ApplicationTypeEnum;
use App\Enums\ConnectionProtocolEnum;
use App\Livewire\Forms\CreateAppSelectionForm;
+use App\Models\ConnectedApp;
use App\Services\ConnectedAppService;
use Livewire\Attributes\Computed;
use Livewire\Attributes\Title;
@@ -61,6 +62,18 @@ public function edit(int $appId): void
$this->redirectRoute('apps.edit', ['id' => $appId], navigate: true);
}
+ public function viewApp(int $appId): void
+ {
+ $app = ConnectedApp::with('protocol')->findOrFail($appId);
+ $protocol = mb_strtolower($app->protocol->name ?? '');
+
+ if ($protocol === 'oidc') {
+ $this->redirectRoute('apps.oidc.show', ['id' => $appId], navigate: true);
+ } else {
+ $this->redirectRoute('apps.edit', ['id' => $appId], navigate: true);
+ }
+ }
+
public function delete(int $appId): void
{
$this->attempt(
@@ -101,8 +114,8 @@ public function createApp(): void
@scope('actions', $row)
-
+
middleware(PermissionMiddleware::using(AppPermissionEnum::Update))->name('edit');
+
+ Route::livewire(
+ 'oidc/{id}/view',
+ 'pages::apps.oidc.show'
+ )->middleware(PermissionMiddleware::using(AppPermissionEnum::Read))->name('oidc.show');
+
Route::livewire('/', 'pages::apps.index')->name('index');
});
diff --git a/tests/Feature/OidcAppViewTest.php b/tests/Feature/OidcAppViewTest.php
new file mode 100644
index 0000000..4939806
--- /dev/null
+++ b/tests/Feature/OidcAppViewTest.php
@@ -0,0 +1,89 @@
+seed(Database\Seeders\PermissionSeeder::class);
+ $this->seed(Database\Seeders\DefaultRoleWithPermissionSeeder::class);
+ $this->seed(Database\Seeders\ConnectionProtocolSeeder::class);
+ $this->seed(Database\Seeders\ConnectionProviderSeeder::class);
+ $this->seed(Database\Seeders\ConnectionStatusSeeder::class);
+
+ $this->oidcProtocol = ConnectionProtocol::query()->where('name', 'oidc')->firstOrFail();
+ $this->status = ConnectionStatus::query()->where('name', 'connected')->firstOrFail();
+
+ // Create a mock OIDC application in database
+ $this->connectedApp = ConnectedApp::query()->create([
+ 'name' => 'Test OIDC App',
+ 'slug' => 'test-oidc-app',
+ 'connection_protocol_id' => $this->oidcProtocol->id,
+ 'connection_provider_id' => 0,
+ 'connection_status_id' => $this->status->id,
+ 'settings' => [
+ 'access_url' => 'https://test-oidc-app.example.com',
+ 'signOutUris' => ['https://test-oidc-app.example.com/logout'],
+ ],
+ ]);
+
+ // Create corresponding Passport Client
+ $this->passportClient = Client::query()->create([
+ 'id' => (string) Str::uuid(),
+ 'name' => 'Test OIDC App',
+ 'secret' => bcrypt('old-secret-value'),
+ 'provider' => 'users',
+ 'redirect_uris' => ['https://test-oidc-app.example.com/callback'],
+ 'grant_types' => ['authorization_code'],
+ 'revoked' => false,
+ ]);
+});
+
+it('authorizes only admin users to access the OIDC view page', function (): void {
+ $user = User::factory()->create();
+ $user->assignRole('user'); // standard user
+
+ $this->actingAs($user);
+
+ // Should fail with 403 Forbidden
+ Livewire::actingAs($user)
+ ->test('pages::apps.oidc.show', ['id' => $this->connectedApp->id])
+ ->assertForbidden();
+
+ $admin = User::factory()->create();
+ $admin->assignRole('admin'); // admin user
+
+ $this->actingAs($admin);
+
+ // Should load successfully
+ Livewire::actingAs($admin)
+ ->test('pages::apps.oidc.show', ['id' => $this->connectedApp->id])
+ ->assertOk();
+});
+
+it('displays client credentials and allows generating a new client secret', function (): void {
+ $admin = User::factory()->create();
+ $admin->assignRole('admin');
+
+ $this->actingAs($admin);
+
+ $component = Livewire::test('pages::apps.oidc.show', ['id' => $this->connectedApp->id])
+ ->assertSet('appId', $this->connectedApp->id)
+ ->assertSet('passportClient.id', $this->passportClient->id);
+
+ // Call generateNewSecret
+ $component->call('generateNewSecret');
+
+ // Get plain secret set in component
+ $newSecretPlain = $component->get('newSecretPlain');
+ expect($newSecretPlain)->toBeString()
+ ->and(mb_strlen($newSecretPlain))->toBe(40);
+
+ // Verify it is updated in DB
+ $this->passportClient->refresh();
+ expect(Hash::check($newSecretPlain, $this->passportClient->secret))->toBeTrue();
+});