# Single sign-on

Inloggen met Microsoft en Google in Laravel met Socialite, en vanuit een Unity-game

Multimedia & Creatieve Technologie · 3.1 The Agency · Workshop · versie 1.1.0 · 2026-10-01

Tags: laravel, php, oauth, security, unity

Slides: <https://slides.mctantwerp.be/nl/the-agency/02-sso>

Speaker notes: <https://slides.mctantwerp.be/nl/the-agency/02-sso.notes.md>

<!-- slide 2 -->

## Agenda

1. Wat is SSO
2. Hoe het werkt
3. Laravel Socialite
4. Google
5. Microsoft
6. Redirect urls
7. Unity en C#
8. Als het misloopt

<!-- slide 3 -->

## 1. Wat is SSO

<!-- slide 4 -->

**Hoeveel wachtwoorden heb jij?**

<!-- slide 5 -->

### Zonder en met SSO

#### Zonder

#### Met

<!-- slide 6 -->

### Single sign-on

- Eén account, bij één partij die weet wie je bent
- Apps houden zelf geen wachtwoorden meer bij, ze vertrouwen die partij
- Je logt één keer in, de volgende app weet het al
- Account weg bij die partij, dan ben je overal buiten

<!-- slide 7 -->

### Je gebruikt het elke dag

- **Je KdG-account**: Outlook, Teams en OneDrive. Allemaal via Microsoft Entra ID, één login.
- **Log in met Google**: Figma, Notion, Miro. Geen nieuw wachtwoord, één klik.
- **Sign in with GitHub**: Vercel, Netlify en de meeste developer tools. Je GitHub-account is je identiteit.

<!-- slide 8 -->

### Wie is wie

**Identity provider (IdP)**: De partij die weet wie je bent. Microsoft Entra ID, Google, GitHub, itsme.

**Service provider**: Jouw app. Die vraagt aan de IdP wie er voor de deur staat, en gelooft het antwoord.

**Client ID en client secret**: De gebruikersnaam en het wachtwoord van jouw app bij de IdP. Niet van de gebruiker.

<!-- slide 9 -->

### Authenticatie en autorisatie

#### Authenticatie

Wie ben je?

Inloggen. De IdP bevestigt dat jij het bent, met wachtwoord en MFA.

#### Autorisatie

Wat mag je?

Rechten. Mag deze gebruiker facturen zien? Dat beslist jouw app.

<!-- slide 10 -->

### De protocols

|   | Sinds | Waarvoor | Wat je terugkrijgt |
| --- | --- | --- | --- |
| **SAML 2.0** | 2005 | Enterprise SSO, oudere intranetten | Een XML-document |
| **OAuth 2.0** | 2012 | Een app toegang geven tot je data | Een access token |
| **OpenID Connect** | 2014 | Inloggen, bovenop OAuth 2.0 | Een `id_token` (JWT) met wie je bent |

> [!NOTE]
> OAuth 2.0 is gemaakt voor toegang, niet voor login. OpenID Connect voegt het stukje "wie is dit" toe.

<!-- slide 11 -->

### Welke rol speelt Microsoft Entra ID als je inlogt op Teams?

- A. Identity provider
- B. Service provider
- C. Protocol
- D. Client secret

**Antwoord:** A. Identity provider

Entra ID weet wie je bent. Teams is de service provider, die vertrouwt wat Entra zegt.

<!-- slide 12 -->

## 2. Hoe het werkt

<!-- slide 13 -->

### Authorization code flow

1. Browser → Laravel, **Klik op "Log in"**: Een gewone link naar `/auth/microsoft/redirect`.
2. Laravel → Browser, **Redirect naar Microsoft**: Laravel stuurt de browser door met `client_id`, `redirect_uri`, `scope` en een willekeurige `state`. Die state bewaart Laravel in je sessie.
3. Browser → Microsoft, **Inloggen bij Microsoft**: Wachtwoord, MFA, eventueel toestemming geven. Jouw app ziet hier niets van.
4. Microsoft → Browser, **Redirect terug met een code**: Naar de `redirect_uri`, met `?code=…&state=…` erachter. Alleen urls die in de portal staan, worden aanvaard.
5. Browser → Laravel, **GET /callback**: Laravel checkt eerst of de state dezelfde is als die in de sessie. Zo niet: InvalidStateException.
6. Laravel → Microsoft, **Code + client secret**: Laravel ruilt de code in, rechtstreeks bij Microsoft. De browser komt er niet aan te pas, dus de secret lekt nooit naar de gebruiker.
7. Microsoft → Laravel, **Access token en profiel**: Met het access token haalt Socialite naam, e-mail en id op. Bij Microsoft komt er ook een id_token bij.
8. Laravel, **User zoeken of aanmaken**: `updateOrCreate` op provider en id, daarna `Auth::login()`.
9. Laravel → Browser, **Sessie-cookie**: Vanaf hier is het een gewone Laravel-sessie. Microsoft komt pas terug bij de volgende login.

<!-- slide 14 -->

### De redirect naar Microsoft

```bash
https://login.microsoftonline.com/common/oauth2/v2.0/authorize
  ?client_id=6f1c2b8e-4a1d-4c55-9d1e-2f0b7a3c9e41
  &redirect_uri=https://agency.test/auth/microsoft/callback
  &response_type=code
  &scope=openid profile email User.Read
  &state=Xk29fQ7vLm0pRt3sWz8nYb1cHd5eJg6k
```

- `client_id` is publiek, die staat gewoon in de url
- `scope` zegt wat je app wil weten. Vraag niet meer dan je nodig hebt.
- De `client_secret` staat hier niet. Die gaat nooit via de browser.

<!-- slide 15 -->

### Waarom state

- Een willekeurige string die Laravel in je sessie zet, vlak voor de redirect
- Microsoft of Google stuurt hem ongewijzigd terug naar de callback
- Klopt hij niet, dan stopt Socialite met een InvalidStateException
- Zonder state kan iemand je een link sturen die jou inlogt op zijn account

<!-- slide 16 -->

### Welke stap gebeurt niet in de browser?

- A. De redirect naar de loginpagina
- B. Wachtwoord en MFA invullen
- C. De code inruilen voor een token
- D. De terugkeer naar /callback

**Antwoord:** C. De code inruilen voor een token

Dat is een request van jouw server naar die van Microsoft of Google, met de client secret erbij. Daarom is een gestolen code alleen niets waard.

<!-- slide 17 -->

## 3. Laravel Socialite

<!-- slide 18 -->

### Socialite

- Officieel package van Laravel
- Doet de hele flow: redirect, state, code inruilen, profiel ophalen
- Ingebouwd: Google, GitHub, GitLab, Bitbucket, LinkedIn, Facebook, X, Slack
- De rest via adapters van [SocialiteProviders](https://socialiteproviders.com). Microsoft is er één van.

```console
$ composer require laravel/socialite
$ composer require socialiteproviders/microsoft
```

<!-- slide 19 -->

### De users-tabel

```php
Schema::table('users', function (Blueprint $table) {
    $table->string('provider')->nullable();
    $table->string('provider_id')->nullable();
    $table->string('password')->nullable()->change();

    $table->unique(['provider', 'provider_id']);
});
```

> [!TIP]
> Mag één gebruiker inloggen met Google én Microsoft? Maak dan een aparte tabel `social_accounts` met een `user_id`.

<!-- slide 20 -->

### Routes

```php
use App\Http\Controllers\SocialLoginController;

Route::get('auth/{provider}/redirect', [SocialLoginController::class, 'redirect'])
    ->whereIn('provider', ['google', 'microsoft'])
    ->name('socialLogin.redirect');

Route::get('auth/{provider}/callback', [SocialLoginController::class, 'callback'])
    ->whereIn('provider', ['google', 'microsoft'])
    ->name('socialLogin.callback');
```

Eén controller voor alle providers. Zonder `whereIn` kan iedereen `/auth/eender-wat/redirect` proberen.

<!-- slide 21 -->

### Redirect

```php
use Laravel\Socialite\Facades\Socialite;
use Symfony\Component\HttpFoundation\RedirectResponse;

class SocialLoginController
{
    public function redirect(string $provider): RedirectResponse
    {
        return Socialite::driver($provider)->redirect();
    }
}
```

Socialite bouwt de url uit stap 2, zet de state in je sessie en stuurt de browser door.

<!-- slide 22 -->

### Callback

```php
public function callback(string $provider): RedirectResponse
{
    $socialUser = Socialite::driver($provider)->user();

    $user = User::updateOrCreate([
        'provider' => $provider,
        'provider_id' => $socialUser->getId(),
    ], [
        'name' => $socialUser->getName(),
        'email' => $socialUser->getEmail(),
    ]);

    Auth::login($user, remember: true);

    return redirect()->intended('dashboard');
}
```

<!-- slide 23 -->

### Koppel op id, niet op e-mail

- **Fout**

  `User::firstWhere('email', …)`

  Iemand die jouw e-mailadres kan instellen bij een provider, zit in jouw account.
- **Goed**

  `provider` + `provider_id`

  Die id kiest de provider zelf. Hij is uniek en verandert nooit, ook niet als de naam of het adres wijzigt.

> [!WARNING]
> Zelfde e-mail via twee providers? Dan botst de unique index op `email`. Kies één provider per project, of ga voor `social_accounts`.

<!-- slide 24 -->

### De login-knoppen

```html
<a href="{{ route('socialLogin.redirect', 'microsoft') }}">
    Log in met Microsoft
</a>

<a href="{{ route('socialLogin.redirect', 'google') }}">
    Log in met Google
</a>
```

Gewone links, geen formulier. Beide hebben huisregels voor hun knop: [Google](https://developers.google.com/identity/branding-guidelines) en [Microsoft](https://learn.microsoft.com/en-us/entra/identity-platform/howto-add-branding-in-apps).

<!-- slide 25 -->

### Wat je terugkrijgt

```php
$socialUser->getId();      // uniek per provider
$socialUser->getName();
$socialUser->getEmail();
$socialUser->getAvatar();
$socialUser->token;        // het access token
$socialUser->getRaw();     // alles wat de provider stuurde
```

Hetzelfde object voor elke provider. Wat specifiek is voor Google of Microsoft, zit in `getRaw()`.

<!-- slide 26 -->

## 4. Google

<!-- slide 27 -->

### Google Cloud Console

1. **Project**: Nieuw project in de Cloud Console, één per klant
2. **Branding**: Naam van de app, supportmail, logo mag later
3. **Audience**: External, en voeg jezelf toe als test user
4. **Clients**: Create client, type Web application, redirect url erbij

- **[Google Auth Platform](https://console.cloud.google.com/auth/overview)**
- **[Socialite docs](https://laravel.com/docs/socialite)**

<!-- slide 28 -->

### Van console naar .env

Google toont een nieuwe client secret maar één keer. Kwijt? Maak een nieuwe aan in dezelfde client.

<!-- slide 29 -->

### Config

#### config/services.php

```php
'google' => [
    'client_id' => env('GOOGLE_CLIENT_ID'),
    'client_secret' => env('GOOGLE_CLIENT_SECRET'),
    'redirect' => env('GOOGLE_REDIRECT_URI'),
],
```

#### .env

```ini
APP_URL=http://localhost:8000

GOOGLE_CLIENT_ID=123456789012-abc123.apps...
GOOGLE_CLIENT_SECRET=GOCSPX-7hT2kQ9...
GOOGLE_REDIRECT_URI="${APP_URL}/auth/google/callback"
```

<!-- slide 30 -->

### Testing en in productie

- Een nieuwe app staat op Testing: alleen test users die je zelf toevoegt, kunnen inloggen
- Publish app zet hem open voor iedereen met een Google-account
- Alleen openid, email en profile? Dan hoeft Google niets na te kijken.
- Wil je in iemands Gmail of Drive, dan moet je app eerst door een review bij Google

<!-- slide 31 -->

### Alleen je eigen domein

- *redirect*

  ```php
  return Socialite::driver('google')
      ->with(['hd' => 'klant.be'])
      ->redirect();
  ```
- *callback*

  ```php
  $user = Socialite::driver('google')->user();

  if (($user->user['hd'] ?? null) !== 'klant.be') {
      abort(403);
  }
  ```

> [!WARNING]
> `hd` in de redirect is een hint voor het loginscherm, geen controle. Check het domein altijd in de callback.

<!-- slide 32 -->

## 5. Microsoft

<!-- slide 33 -->

### Microsoft Entra ID

- Vroeger Azure Active Directory, sinds 2023 Microsoft Entra ID
- De identity provider achter elk Microsoft 365-account, ook dat van KdG
- Je app registreer je in het [Entra admin center](https://entra.microsoft.com) of in de Azure portal. Zelfde scherm.
- Niet ingebouwd in Socialite, daarvoor is de adapter

<!-- slide 34 -->

### App registration

1. **New registration**: App registrations, New registration, een naam voor je app
2. **Account types**: Wie mag er inloggen? Zie de volgende slide.
3. **Redirect URI**: Platform Web, je callback url
4. **Client secret**: Certificates & secrets, New client secret

- **[Entra admin center](https://entra.microsoft.com)**
- **[Azure portal](https://portal.azure.com)**

<!-- slide 35 -->

### Wie mag er binnen

| Supported account types | tenant in .env | Typisch voor |
| --- | --- | --- |
| Alleen deze organisatie (single tenant) | `Directory (tenant) ID` | Een intern tool van één klant |
| Elke organisatie (multitenant) | `organizations` | Een SaaS voor bedrijven en scholen |
| Elke organisatie en persoonlijke accounts | `common` | Iedereen met een Microsoft-account |
| Alleen persoonlijke accounts | `consumers` | Zelden wat je wil |

<!-- slide 36 -->

### Van portal naar .env

<!-- slide 37 -->

### De client secret

- Kopieer de kolom Value. De Secret ID ernaast is een ander getal en werkt niet.
- Je ziet hem één keer. Pagina weg, secret weg, maak een nieuwe.
- Hij vervalt, na maximaal twee jaar
- Zet die datum in de agenda van de klant, anders ligt de login op een dag gewoon plat

<!-- slide 38 -->

### De adapter

In `app/Providers/AppServiceProvider.php`:

```php
use Illuminate\Support\Facades\Event;
use SocialiteProviders\Manager\SocialiteWasCalled;
use SocialiteProviders\Microsoft\Provider;

public function boot(): void
{
    Event::listen(function (SocialiteWasCalled $event) {
        $event->extendSocialite('microsoft', Provider::class);
    });
}
```

<!-- slide 39 -->

### Config

*config/services.php*

```php
'microsoft' => [
    'client_id' => env('MICROSOFT_CLIENT_ID'),
    'client_secret' => env('MICROSOFT_CLIENT_SECRET'),
    'redirect' => env('MICROSOFT_REDIRECT_URI'),
    'tenant' => env('MICROSOFT_TENANT_ID', 'common'),
],
```

*.env*

```ini
MICROSOFT_CLIENT_ID=6f1c2b8e-4a1d-...
MICROSOFT_CLIENT_SECRET=Q8x8Q~Vb3.nK1pR...
MICROSOFT_TENANT_ID=b3d9f0a2-7c1e-...
MICROSOFT_REDIRECT_URI="${APP_URL}/auth/microsoft/callback"
```

<!-- slide 40 -->

### Je app is single tenant, maar in .env staat de tenant op common. Wat gebeurt er?

- A. Het werkt, common betekent iedereen
- B. Microsoft weigert met een AADSTS-fout
- C. Alleen persoonlijke accounts kunnen inloggen
- D. Laravel gooit een InvalidStateException

**Antwoord:** B. Microsoft weigert met een AADSTS-fout

AADSTS50194: de app is niet ingesteld als multitenant, dus /common mag niet. Zet de Directory (tenant) ID in .env, of maak de app multitenant.

<!-- slide 41 -->

## 6. Redirect urls

<!-- slide 42 -->

**Tot op de laatste slash.**

Scheme, host, poort en pad moeten letter voor letter kloppen met wat in de console staat.

<!-- slide 43 -->

### Redirect url tester

<!-- slide 44 -->

### Lokaal en online

|   | Google | Microsoft |
| --- | --- | --- |
| http | Alleen op localhost | Alleen op localhost |
| `.test` | Nee, geen publieke domeinnaam | Ja, met https (`herd secure`) |
| Wildcards | Nee | Nee |
| Meerdere urls | Ja, per client | Ja, per app registration |

<!-- slide 45 -->

### Elke omgeving apart

- Lokaal, staging en productie hebben elk hun eigen redirect url
- Beter nog: een aparte client per omgeving. Een secret uit je lokale .env komt dan nooit in productie.
- Zet `APP_URL` juist, want je redirect url wordt ermee gebouwd
- Achter een proxy (Cloud, Forge, ngrok)? Zet `trustProxies`, anders maakt Laravel http van je https.

<!-- slide 46 -->

### Je surft naar 127.0.0.1:8000, maar in .env staat localhost:8000. Wat krijg je?

- A. Het werkt gewoon
- B. redirect_uri_mismatch bij Google
- C. InvalidStateException in Laravel
- D. Google blokkeert 127.0.0.1

**Antwoord:** C. InvalidStateException in Laravel

In de console staat dezelfde localhost-url, dus Google aanvaardt de redirect en stuurt je naar localhost. Maar je sessie-cookie hoort bij 127.0.0.1, dus op localhost vindt Laravel geen state terug. Surf naar dezelfde host als in APP_URL.

<!-- slide 47 -->

## 7. Unity en C#

<!-- slide 48 -->

### Een build is publiek

- Wat in een build zit, kan iemand teruglezen. Een Mono-build open je met ILSpy, IL2CPP vraagt wat meer geduld.
- Een client secret in je game is dus geen secret meer
- Inloggen gebeurt in de browser van het systeem, niet in een webview in je game
- De code komt terug naar `http://127.0.0.1`, op een poort waar je game zelf luistert

<!-- slide 49 -->

### Twee routes

#### Met een Laravel-backend

De game laat Laravel de login doen. Socialite, de client secret en de redirect url blijven waar ze zijn.

De game krijgt een Sanctum-token en praat daarmee met je API.

#### Zonder backend

De game praat zelf met Microsoft of Google, als public client met PKCE.

Geen client secret, wel een eigen registratie als desktop app.

<!-- slide 50 -->

### Inloggen vanuit Unity

1. Unity, **Luisteren op 127.0.0.1**: De game start een HttpListener op een vrije poort en verzint een willekeurige state.
2. Unity → Browser, **OpenURL**: De browser opent `/auth/microsoft/redirect?port=…&state=…` op je Laravel-app.
3. Browser → Laravel, **GET /redirect**: Laravel zet de poort en de state van de game in de sessie, en start de gewone Socialite-flow.
4. Browser → Microsoft, **Inloggen bij Microsoft**: Stap 2 tot 4 van het schema in hoofdstuk 2. De game ziet hier niets van.
5. Browser → Laravel, **GET /callback**: Socialite ruilt de code in en Laravel zoekt of maakt de user. Daarna maakt Laravel een eenmalige code voor de game.
6. Laravel → Browser, **Naar 127.0.0.1**: Naar `http://127.0.0.1:port/?code=…&state=…`. Die code werkt één keer, en maar een minuut lang.
7. Browser → Unity, **GET 127.0.0.1**: De game vangt de request op, checkt de state en zegt in de browser dat het tabblad dicht mag.
8. Unity → Laravel, **POST /api/game/token**: De game ruilt de eenmalige code in, rechtstreeks bij Laravel.
9. Laravel → Unity, **Sanctum-token**: Vanaf nu stuurt de game `Authorization: Bearer …` mee met elke request naar je API.

<!-- slide 51 -->

### Laravel onthoudt de game

```php
public function redirect(Request $request, string $provider): RedirectResponse
{
    $request->session()->forget('game');

    if ($request->has('port')) {
        $request->session()->put('game', $request->validate([
            'port' => ['required', 'integer', 'between:1024,65535'],
            'state' => ['required', 'string', 'size:32'],
        ]));
    }

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

Een login via de website stuurt geen port mee. Daar verandert niets.

<!-- slide 52 -->

### Terug naar de game

*callback(), na updateOrCreate*

```php
if ($game = session()->pull('game')) {
    $code = Str::random(40);
    Cache::put("game-login:{$code}", $user->id, now()->addMinute());

    return redirect()->away(
        "http://127.0.0.1:{$game['port']}/?code={$code}&state={$game['state']}"
    );
}
```

Geen token in de url, maar een code die na één keer of na een minuut verloopt. `pull` haalt de game uit de sessie, zodat de volgende login weer een gewone is.

<!-- slide 53 -->

### Code inruilen voor een token

*routes/api.php*

```php
Route::post('game/token', function (Request $request) {
    $userId = Cache::pull('game-login:'.$request->string('code'));

    abort_if($userId === null, 401);

    return [
        'token' => User::findOrFail($userId)->createToken('game')->plainTextToken,
    ];
})->middleware('throttle:10,1');
```

`Cache::pull` leest en verwijdert in één keer, dus een tweede poging met dezelfde code geeft 401. Sanctum installeer je met `php artisan install:api`.

<!-- slide 54 -->

### Luisteren en de browser openen

*SocialLogin.cs*

```csharp
public async Task<string> Login(string provider)
{
    var port = FreePort();
    var state = Guid.NewGuid().ToString("N");

    using var listener = new HttpListener();
    listener.Prefixes.Add($"http://127.0.0.1:{port}/");
    listener.Start();

    Application.OpenURL($"{Api}/auth/{provider}/redirect?port={port}&state={state}");
```

`Application.OpenURL` opent de standaardbrowser. Daar is de speler misschien al ingelogd bij Microsoft, en dan duurt de login één klik.

<!-- slide 55 -->

### Een vrije poort

*SocialLogin.cs*

```csharp
static int FreePort()
{
    var tcp = new TcpListener(IPAddress.Loopback, 0);
    tcp.Start();
    var port = ((IPEndPoint)tcp.LocalEndpoint).Port;
    tcp.Stop();
    return port;
}
```

Poort 0 laat het besturingssysteem een vrije poort kiezen. Een vaste poort mag ook, tot een andere app op die computer hem al gebruikt.

<!-- slide 56 -->

### De code opvangen

*SocialLogin.cs, vervolg van Login()*

```csharp
    var context = await listener.GetContextAsync();
    var code = context.Request.QueryString["code"];
    var returned = context.Request.QueryString["state"];

    context.Response.ContentType = "text/plain; charset=utf-8";
    context.Response.Close(Encoding.UTF8.GetBytes("Ingelogd, dit tabblad mag dicht."), false);

    if (returned != state)
        throw new InvalidOperationException("State klopt niet");

    return await ExchangeCode(code);
}
```

<!-- slide 57 -->

### Code inruilen in Unity

*SocialLogin.cs*

```csharp
[Serializable] class TokenResponse { public string token; }

async Task<string> ExchangeCode(string code)
{
    var form = new Dictionary<string, string> { ["code"] = code };
    using var request = UnityWebRequest.Post($"{Api}/api/game/token", form);
    request.SetRequestHeader("Accept", "application/json");
    await request.SendWebRequest();

    if (request.result != UnityWebRequest.Result.Success)
        throw new InvalidOperationException(request.error);

    return JsonUtility.FromJson<TokenResponse>(request.downloadHandler.text).token;
}
```

<!-- slide 58 -->

### Het token gebruiken

```csharp
[Serializable] class ApiUser { public string name; public string email; }

using var request = UnityWebRequest.Get($"{Api}/api/user");
request.SetRequestHeader("Authorization", $"Bearer {token}");
request.SetRequestHeader("Accept", "application/json");
await request.SendWebRequest();

var user = JsonUtility.FromJson<ApiUser>(request.downloadHandler.text);
nameLabel.text = user.name;
```

> [!WARNING]
> `PlayerPrefs` bewaart alles leesbaar op de schijf, op Windows in de registry. Een token mag daar, het kan je intrekken. Een secret nooit.

<!-- slide 59 -->

### PKCE

- Proof Key for Code Exchange, uitgesproken als "pixie". Voor een game zonder backend.
- De game verzint per login een geheim, de code verifier, en stuurt alleen de hash ervan mee in de redirect
- Bij het inruilen van de code stuurt hij het geheim zelf. Microsoft of Google checkt of de hash klopt.
- Wie de code onderschept, kent het geheim niet, en heeft dus niets

<!-- slide 60 -->

### PKCE in C#

```csharp
var bytes = new byte[32];
using (var rng = RandomNumberGenerator.Create()) rng.GetBytes(bytes);
var verifier = Base64Url(bytes);

using var sha = SHA256.Create();
var challenge = Base64Url(sha.ComputeHash(Encoding.ASCII.GetBytes(verifier)));

static string Base64Url(byte[] data) =>
    Convert.ToBase64String(data).TrimEnd('=').Replace('+', '-').Replace('/', '_');
```

- In de redirect: `code_challenge={challenge}&code_challenge_method=S256`
- Bij het inruilen: `code_verifier={verifier}`

<!-- slide 61 -->

### Registreren als desktop app

|   | Microsoft Entra | Google |
| --- | --- | --- |
| Platform | Mobile and desktop applications | Client type **Desktop app** |
| Redirect url | `http://localhost`, de poort telt niet mee | Niets invullen, elke poort op localhost mag |
| Client secret | Geen | Krijg je toch, en is niet geheim |

- **[OAuth voor desktop apps, Google](https://developers.google.com/identity/protocols/oauth2/native-app)**
- **[Redirect urls in Entra](https://learn.microsoft.com/en-us/entra/identity-platform/reply-url)**

<!-- slide 62 -->

### Waarom zit de client secret niet in je Unity-build?

- A. Iedereen met de build kan hem eruit halen
- B. Unity kan geen HTTPS-requests doen
- C. Microsoft aanvaardt geen requests uit C#
- D. Hij past niet in PlayerPrefs

**Antwoord:** A. Iedereen met de build kan hem eruit halen

Een build staat op de computer van de speler, en wat daar staat, kan je uitlezen. Daarom houdt Laravel de secret op de server, of werkt de game als public client met PKCE.

<!-- slide 63 -->

## 8. Als het misloopt

<!-- slide 64 -->

### Foutmeldingen

| Je ziet | Het betekent |
| --- | --- |
| `redirect_uri_mismatch` | Google: de url in .env staat niet in de console |
| `AADSTS50011` | Microsoft: hetzelfde, de redirect url klopt niet |
| `AADSTS7000215` | Je plakte de Secret ID in plaats van de Value |
| `AADSTS700016` | Client ID of tenant klopt niet, de app wordt niet gevonden |
| `403 access_denied` | Google-app op Testing, en je account is geen test user |
| `InvalidStateException` | Sessie kwijt tussen redirect en callback |

<!-- slide 65 -->

### Secrets

- `.env` staat in `.gitignore`. Hou dat zo.
- Secret toch gepusht? Maak meteen een nieuwe en verwijder de oude. Een revert helpt niet, de geschiedenis onthoudt alles.
- Een secret gaat niet in Discord, niet in Notion en niet in een screenshot
- Deel ze via de password manager van het project

<!-- slide 66 -->

### Opdracht: Log in met Microsoft en Google

*Individueel*

In een nieuw Laravel-project, lokaal. Beide providers via dezelfde controller.

1. Installeer Socialite en de Microsoft-adapter
2. Maak een client in Google Cloud en een app registration in Entra, met je lokale redirect url
3. Voeg provider en provider_id toe aan users en schrijf de redirect en de callback
4. Klaar als je met allebei kan inloggen en uitloggen, en een tweede login geen nieuwe user maakt

<!-- slide 67 -->

### Opdracht: Log in vanuit Unity

Op het Laravel-project van de vorige opdracht. Een webstudent in je team kan de Laravel-kant doen.

1. Installeer Sanctum met php artisan install:api en voeg de route game/token toe
2. Laat redirect de poort en de state onthouden, en callback terugsturen naar 127.0.0.1
3. Bouw in Unity de login: luisteren, de browser openen, de code inruilen
4. Klaar als je game na de login de naam uit /api/user toont

## Begrippen

- **Single sign-on**: Eén account bij één partij die weet wie je bent, en apps die die partij vertrouwen in plaats van zelf wachtwoorden bij te houden. Zet IT dat ene account uit, dan ben je overal buiten. (slide 6)
- **Authenticatie**: Wie ben je? Inloggen: de identity provider bevestigt dat jij het bent, met wachtwoord en MFA. Dat deel lost SSO op. (slide 9)
- **Autorisatie**: Wat mag je? Rollen en rechten, zoals wie facturen mag zien. Dat blijft het werk van jouw app, ook met SSO. (slide 9)
- **SAML 2.0**: Het oudste SSO-protocol, uit 2005, met een XML-document als antwoord. Je komt het tegen bij grote klanten met een eigen IT-afdeling. (slide 10)
- **OAuth 2.0**: Een protocol om een app toegang te geven tot je data, met een access token als resultaat. Gemaakt voor toegang, niet voor login. (slide 10)
- **OpenID Connect**: Een laag bovenop OAuth 2.0 die er login van maakt. Je krijgt er een id_token bij, een JWT met wie de gebruiker is. Socialite werkt met OAuth 2.0 en OpenID Connect. (slide 10)
- **Authorization code flow**: De flow achter Log in met Google of Microsoft. De browser gaat naar de identity provider en komt terug met een code, en jouw server ruilt die code met de client secret in voor een access token, buiten de browser om. (slide 13)
- **Authorization code**: De code die de identity provider in de url meegeeft naar je callback. Hij mag zichtbaar zijn: zonder client secret is hij waardeloos, en hij werkt maar één keer. (slide 13)
- **Access token**: Wat je app krijgt in ruil voor de code. Socialite haalt er naam, e-mail en id mee op, en je vindt het terug in $socialUser->token. (slide 13)
- **Scope**: De parameter in de redirect die zegt wat je app wil weten, zoals openid profile email. Vraag niet meer dan je nodig hebt. (slide 14)
- **State**: Een willekeurige string die Laravel voor de redirect in je sessie zet, en die ongewijzigd terugkomt naar de callback. Zonder state kan iemand je een link sturen die jou inlogt op zijn account. (slide 15)
- **InvalidStateException**: De fout van Socialite als de state in de callback niet klopt met die in je sessie. Meestal is de sessie kwijt, bijvoorbeeld omdat je op 127.0.0.1 surft terwijl APP_URL op localhost staat. (slide 64)
- **Callback**: De route waar de identity provider de gebruiker terugstuurt met de code. Daar checkt Socialite::driver($provider)->user() de state en ruilt hij de code in voor het profiel. (slide 22)
- **Redirect url**: De url van je callback, zoals die in de console van Google of in Entra staat. Scheme, host, poort en pad moeten letter voor letter kloppen, tot de laatste slash. Klopt hij niet: redirect_uri_mismatch of AADSTS50011. (slide 43)
- **Socialite**: Het officiële Laravel-package dat de hele flow doet: redirect, state, code inruilen, profiel ophalen. Google en GitHub zitten erin, Microsoft komt via een adapter. (slide 18)
- **Adapter**: Een package van SocialiteProviders dat Socialite een extra driver bijleert, zoals microsoft. Je registreert hem met een listener op SocialiteWasCalled in AppServiceProvider. (slide 38)
- **provider_id**: Het id dat de provider zelf aan een gebruiker geeft. Uniek en het verandert nooit, dus je koppelt je users op provider en provider_id, niet op e-mail. (slide 23)
- **Google Cloud Console**: Waar je een app aanmeldt bij Google. In Google Auth Platform vul je Branding en Audience in en maak je een client van het type Web application, met je redirect url erbij. (slide 27)
- **Testing**: De status van een nieuwe Google-app: alleen test users die je zelf toevoegt, kunnen inloggen. Een ander account krijgt 403 access_denied. Publish app zet hem open voor iedereen. (slide 30)
- **hd**: Een parameter die het loginscherm van Google naar één domein stuurt, zoals klant.be. Een hint, geen controle: check het domein altijd in de callback. (slide 31)
- **Microsoft Entra ID**: De identity provider achter elk Microsoft 365-account, ook dat van KdG. Heette vroeger Azure Active Directory, daarom beginnen de foutcodes nog met AADSTS. (slide 33)
- **App registration**: Je app aanmelden bij Microsoft, in het Entra admin center of de Azure portal. Daar haal je de client ID, de tenant ID en de client secret, en vul je de redirect URI in. (slide 34)
- **Tenant**: Een organisatie in Microsoft Entra, zoals KdG. Een single tenant app laat alleen die organisatie binnen, met de Directory (tenant) ID in .env. Met organizations of common zet je de deur verder open. (slide 35)
- **trustProxies**: De instelling in Laravel voor een app achter een proxy, zoals Laravel Cloud, Forge of ngrok. Zonder maakt Laravel http van je https, en klopt je redirect url niet meer. (slide 45)
- **Public client**: Een app die geen geheim kan bewaren, omdat hij op het toestel van de gebruiker draait: een game, een desktop app, een mobiele app. Zo een app krijgt geen client secret, hij gebruikt PKCE. (slide 48)
- **Loopback redirect**: Een redirect url op http://127.0.0.1 of http://localhost, waar een app op de computer van de gebruiker zelf luistert. Zo komt de login uit de browser terug in een game of desktop app. (slide 54)
- **PKCE**: Proof Key for Code Exchange. De app verzint per login een geheim, stuurt de hash mee in de redirect en het geheim zelf bij het inruilen van de code. Een onderschepte code is zo niets waard. (slide 59)

## Links en bronnen

### In de presentatie

- [SocialiteProviders](https://socialiteproviders.com) (slide 18)
- [Google](https://developers.google.com/identity/branding-guidelines) (slide 24)
- [Microsoft](https://learn.microsoft.com/en-us/entra/identity-platform/howto-add-branding-in-apps) (slide 24)
- [Google Auth Platform](https://console.cloud.google.com/auth/overview) (slide 27)
- [Socialite docs](https://laravel.com/docs/socialite) (slide 27)
- [Entra admin center](https://entra.microsoft.com) (slide 33, slide 34)
- [Azure portal](https://portal.azure.com) (slide 34)
- [OAuth 2.0 for iOS & Desktop Apps](https://developers.google.com/identity/protocols/oauth2/native-app) (slide 61): Client type Desktop app, loopback op 127.0.0.1
- [Redirect URI best practices and limitations](https://learn.microsoft.com/en-us/entra/identity-platform/reply-url) (slide 61): Localhost exceptions: de poort telt niet mee

### Bronnen

- [OAuth 2.0 for Native Apps (RFC 8252)](https://www.rfc-editor.org/rfc/rfc8252): Systeembrowser, loopback redirect en PKCE voor apps zonder server
- [Proof Key for Code Exchange (RFC 7636)](https://www.rfc-editor.org/rfc/rfc7636)
- [Laravel Sanctum](https://laravel.com/docs/sanctum)
- [UnityWebRequest](https://docs.unity3d.com/ScriptReference/Networking.UnityWebRequest.html)
