|
| 1 | +# Entra ID Federated Authentication Provider |
| 2 | + |
| 3 | +## Overview |
| 4 | + |
| 5 | +The `EntraFederatedProvider` is a custom Laravel Socialite driver included in the `GServerlessSupportLaravel` library. It enables secure, secretless authentication against Microsoft Entra ID (formerly Azure AD) for Laravel applications running on Google Cloud Platform (GCP). |
| 6 | + |
| 7 | +This provider leverages **Workload Identity Federation**, allowing your GCP workload (e.g., Cloud Run, GKE, GCE) to authenticate with Microsoft's APIs using its own GCP service account identity, eliminating the need to manage and store client secrets. |
| 8 | + |
| 9 | +## How It Works |
| 10 | + |
| 11 | +Instead of a traditional client secret, this provider uses a short-lived OIDC token generated by the GCP metadata server. This token is sent to Entra ID as a `client_assertion` in the OAuth 2.0 token exchange. Entra ID, having been configured to trust your GCP service account, validates the token and issues an access token in return. |
| 12 | + |
| 13 | +This approach significantly enhances security by removing long-lived credentials from your application's environment. |
| 14 | + |
| 15 | +## Requirements |
| 16 | + |
| 17 | +Before you begin, ensure you have: |
| 18 | +1. A Laravel 11+ application using the `GServerlessSupportLaravel` library. |
| 19 | +2. The application is hosted on a GCP service with an attached service account. |
| 20 | +3. Administrative access to a Microsoft Entra ID tenant to configure an App Registration. |
| 21 | +4. The **Unique ID** of the GCP service account your application uses. You can find this in the GCP Console under `IAM & Admin` > `Service Accounts`. |
| 22 | + |
| 23 | +--- |
| 24 | + |
| 25 | +## Setup and Configuration |
| 26 | + |
| 27 | +### Step 1: Microsoft Entra ID Configuration |
| 28 | + |
| 29 | +First, you must register an application in Entra ID and configure it to trust your GCP service account's identity. |
| 30 | + |
| 31 | +1. **Navigate to App Registrations** |
| 32 | + * Log in to the [Microsoft Entra admin center](https://entra.microsoft.com/). |
| 33 | + * Go to **Identity** > **Applications** > **App registrations**. |
| 34 | + * Click **+ New registration**. |
| 35 | + |
| 36 | +2. **Register the Application** |
| 37 | + * Give your application a descriptive **Name** (e.g., "My GCP Web App"). |
| 38 | + * For **Supported account types**, choose the option that fits your needs (typically "Accounts in this organizational directory only"). |
| 39 | + * Click **Register**. |
| 40 | + |
| 41 | +3. **Configure Redirect URIs** |
| 42 | + * On your app registration's page, go to the **Authentication** tab. |
| 43 | + * Click **+ Add a platform** and select **Web**. |
| 44 | + * Under **Redirect URIs**, add the full callback URL for each domain your application uses. The path must match the one defined in your Laravel routes. |
| 45 | + * Example for production: `https://app.your-domain.com/auth/callback` |
| 46 | + * Example for local development: `http://localhost:8000/auth/callback` |
| 47 | + * Click **Configure**. |
| 48 | + |
| 49 | +4. **Note Your IDs** |
| 50 | + * Go to the **Overview** page and copy the **Application (client) ID** and the **Directory (tenant) ID**. These will be used as environment variables. |
| 51 | + |
| 52 | +5. **Create the Federated Credential** |
| 53 | + * Navigate to the **Certificates & secrets** tab. |
| 54 | + * Click the **Federated credentials** tab and then **+ Add credential**. |
| 55 | + * From the **Federated credential scenario** dropdown, select **Other issuer**. |
| 56 | + * Complete the form with the following details: |
| 57 | + * **Issuer**: `https://accounts.google.com` |
| 58 | + * **Subject identifier**: The numeric **Unique ID** of your GCP Service Account. |
| 59 | + * **Name**: A descriptive name for the credential (e.g., `gcp-workload-identity`). |
| 60 | + * **Audience**: `api://AzureADTokenExchange` |
| 61 | + * Click **Add**. |
| 62 | + |
| 63 | +### Step 2: Laravel Application Configuration |
| 64 | + |
| 65 | +Now, configure your Laravel application to use the `EntraFederatedProvider`. |
| 66 | + |
| 67 | +1. **Configure `config/services.php`** |
| 68 | + Add an `entra` key to your `services` configuration file to read the environment variables. |
| 69 | + |
| 70 | + ```php |
| 71 | + // config/services.php |
| 72 | + 'entra' => [ |
| 73 | + 'client_id' => env('ENTRA_CLIENT_ID'), |
| 74 | + 'tenant' => env('ENTRA_TENANT_ID'), |
| 75 | + ], |
| 76 | + ``` |
| 77 | + |
| 78 | +2. **Set Environment Variables** |
| 79 | + You must provide the `client_id` and `tenant` you copied from Entra ID as environment variables to your application. For cloud environments like Google Cloud Run or App Engine, it is best practice to set these directly in the service's configuration instead of using a `.env` file. |
| 80 | + |
| 81 | + **Required Variables:** |
| 82 | + ``` |
| 83 | + ENTRA_CLIENT_ID=your-application-client-id |
| 84 | + ENTRA_TENANT_ID=your-directory-tenant-id |
| 85 | + ``` |
| 86 | + *In your local development environment, you can add these to your `.env` file for convenience.* |
| 87 | + |
| 88 | +3. **Register the Provider in `bootstrap/app.php`** |
| 89 | + To enable the `entra` Socialite driver, you must extend the Socialite factory. The ideal place for this in a Laravel 11+ application is the `booted` callback in `bootstrap/app.php`. |
| 90 | + |
| 91 | + ```php |
| 92 | + // bootstrap/app.php |
| 93 | + |
| 94 | + // ... (existing Application::configure code) |
| 95 | + ->withExceptions(function (Exceptions $exceptions) { |
| 96 | + // |
| 97 | + }) |
| 98 | + |
| 99 | + // ---- ADD THIS CODE ---- |
| 100 | + // Extend the Socialite service to add our custom Entra driver. |
| 101 | + ->booted(function ($app) { |
| 102 | + // This check prevents errors when running artisan commands where the |
| 103 | + // request context and URL generator might not be available. |
| 104 | + if (!$app->has('url')) { |
| 105 | + return; |
| 106 | + } |
| 107 | + |
| 108 | + $socialite = $app->make(\Laravel\Socialite\Contracts\Factory::class); |
| 109 | + |
| 110 | + $socialite->extend('entra', function ($app) use ($socialite) { |
| 111 | + $config = $app['config']['services.entra']; |
| 112 | + |
| 113 | + // Dynamically set the redirect URI using its name. |
| 114 | + $config['redirect'] = route('auth.callback'); // <-- Use your callback route's name here |
| 115 | + |
| 116 | + // This provider is secretless, but Socialite's builder requires the key. |
| 117 | + $config['client_secret'] = null; |
| 118 | + |
| 119 | + // Build the provider and set the tenant ID. |
| 120 | + return $socialite->buildProvider( |
| 121 | + \AffordableMobiles\GServerlessSupportLaravel\Integration\Socialite\EntraFederatedProvider::class, |
| 122 | + $config |
| 123 | + )->setTenant($config['tenant']); |
| 124 | + }); |
| 125 | + }) |
| 126 | + // ---- END OF ADDED CODE ---- |
| 127 | + ->create() |
| 128 | + ``` |
| 129 | + |
| 130 | +--- |
| 131 | + |
| 132 | +## Usage Example |
| 133 | + |
| 134 | +With the configuration complete, you can use the `entra` driver like any other Socialite provider. |
| 135 | + |
| 136 | +### Routes |
| 137 | + |
| 138 | +Define the routes for your authentication flow in `routes/web.php`. |
| 139 | + |
| 140 | +```php |
| 141 | +// routes/web.php |
| 142 | +use App\Http\Controllers\AuthController; |
| 143 | +use Illuminate\Support\Facades\Route; |
| 144 | + |
| 145 | +// Routes for guests (not logged in) |
| 146 | +Route::middleware('guest')->group(function () { |
| 147 | + Route::get('/login', [AuthController::class, 'login'])->name('login'); |
| 148 | + Route::get('/auth/redirect', [AuthController::class, 'redirect'])->name('auth.redirect'); |
| 149 | + Route::get('/auth/callback', [AuthController::class, 'callback'])->name('auth.callback'); |
| 150 | +}); |
| 151 | + |
| 152 | +// Routes for authenticated users |
| 153 | +Route::middleware('auth')->group(function () { |
| 154 | + Route::get('/dashboard', fn() => 'Welcome, ' . auth()->user()->name)->name('dashboard'); |
| 155 | + Route::post('/logout', [AuthController::class, 'logout'])->name('logout'); |
| 156 | +}); |
| 157 | +``` |
| 158 | + |
| 159 | +### Controller |
| 160 | +Create a controller to handle the logic. Remember to add a nullable `entra_id` column to your `users` table to store the unique identifier from Microsoft. |
| 161 | + |
| 162 | +```php |
| 163 | +<?php |
| 164 | + |
| 165 | +namespace App\Http\Controllers; |
| 166 | + |
| 167 | +use App\Models\User; |
| 168 | +use Illuminate\Http\RedirectResponse; |
| 169 | +use Illuminate\Http\Request; |
| 170 | +use Illuminate\Support\Facades\Auth; |
| 171 | +use Laravel\Socialite\Facades\Socialite; |
| 172 | + |
| 173 | +class AuthController extends Controller |
| 174 | +{ |
| 175 | + /** |
| 176 | + * Redirect the user to the Microsoft Entra authentication page. |
| 177 | + */ |
| 178 | + public function redirect(): RedirectResponse |
| 179 | + { |
| 180 | + return Socialite::driver('entra')->redirect(); |
| 181 | + } |
| 182 | + |
| 183 | + /** |
| 184 | + * Obtain the user information from Microsoft Entra ID after authentication. |
| 185 | + */ |
| 186 | + public function callback(Request $request): RedirectResponse |
| 187 | + { |
| 188 | + try { |
| 189 | + $entraUser = Socialite::driver('entra')->user(); |
| 190 | + |
| 191 | + // Find or create a user in your local database. |
| 192 | + $user = User::updateOrCreate( |
| 193 | + ['entra_id' => $entraUser->getId()], |
| 194 | + ['name' => $entraUser->getName(), 'email' => $entraUser->getEmail()] |
| 195 | + ); |
| 196 | + |
| 197 | + Auth::login($user); |
| 198 | + $request->session()->regenerate(); |
| 199 | + |
| 200 | + return redirect()->intended(route('dashboard')); |
| 201 | + |
| 202 | + } catch (\Throwable $e) { |
| 203 | + report($e); |
| 204 | + return redirect()->route('login')->with('error', 'Login failed. Please try again.'); |
| 205 | + } |
| 206 | + } |
| 207 | + |
| 208 | + // ... other methods like login() and logout() |
| 209 | +} |
| 210 | +``` |
0 commit comments