<?php
declare(strict_types=1);
namespace PpPhp\Generated\SocialAuth;

/**
 * Microsoft identity platform (Entra ID, formerly Azure AD) — OpenID Connect.
 *
 * `tenant` decides who may sign in:
 *   common         — work/school AND personal Microsoft accounts (default)
 *   organizations  — work/school accounts in any tenant
 *   consumers      — personal Microsoft accounts only
 *   <tenant-guid>  — a single organisation
 *
 * The issuer check has to account for that: multi-tenant tokens carry the
 * signing tenant's own GUID in `iss`, so a fixed issuer string would reject
 * every legitimate token. We therefore accept the documented issuer shapes and
 * still bind the token to our client via `aud` and `nonce`.
 */
final class MicrosoftProvider extends AbstractProvider
{
    public function id(): string { return 'microsoft'; }

    private function tenant(): string
    {
        $t = $this->value('tenant', 'MICROSOFT_TENANT');
        return $t !== '' ? $t : 'common';
    }

    public function authorizationUrl(array $t): string
    {
        return $this->query(
            'https://login.microsoftonline.com/' . rawurlencode($this->tenant()) . '/oauth2/v2.0/authorize',
            [
                'client_id'             => $this->value('client_id', 'MICROSOFT_CLIENT_ID'),
                'redirect_uri'          => $this->callbackUri(),
                'response_type'         => 'code',
                'response_mode'         => 'query',
                'scope'                 => 'openid email profile User.Read',
                'state'                 => $t['state'],
                'nonce'                 => $t['nonce'],
                'code_challenge'        => rtrim(strtr(base64_encode(hash('sha256', $t['verifier'], true)), '+/', '-_'), '='),
                'code_challenge_method' => 'S256',
                'prompt'                => 'select_account',
            ]
        );
    }

    public function identity(array $r, array $t): array
    {
        $this->failOnProviderError($r);
        if (empty($r['code'])) {
            throw new \RuntimeException('Microsoft did not return an authorization code.');
        }
        $tokens = $this->token('https://login.microsoftonline.com/' . rawurlencode($this->tenant()) . '/oauth2/v2.0/token', [
            'code'          => $r['code'],
            'client_id'     => $this->value('client_id', 'MICROSOFT_CLIENT_ID'),
            'client_secret' => $this->value('client_secret', 'MICROSOFT_CLIENT_SECRET'),
            'redirect_uri'  => $this->callbackUri(),
            'grant_type'    => 'authorization_code',
            'code_verifier' => $t['verifier'],
        ]);

        $idToken = (string) ($tokens['id_token'] ?? '');
        $c = JwtVerifier::verify(
            $idToken,
            'https://login.microsoftonline.com/' . rawurlencode($this->tenant()) . '/discovery/v2.0/keys',
            $this->expectedIssuers($idToken),
            $this->value('client_id', 'MICROSOFT_CLIENT_ID'),
            $t['nonce']
        );

        // Microsoft puts the address in one of several claims depending on the
        // account type; `preferred_username` is the reliable fallback for work
        // accounts, which often carry no `email` claim at all.
        $email = (string) ($c['email'] ?? $c['preferred_username'] ?? $c['upn'] ?? '');
        return [
            'provider'       => 'microsoft',
            'subject'        => (string) ($c['oid'] ?? $c['sub'] ?? ''),
            'email'          => $email,
            // Entra does not emit email_verified. A work/school address is
            // controlled by the tenant, so treat a tenanted token as verified
            // and personal accounts as unverified.
            'email_verified' => $email !== '' && ($c['tid'] ?? '') !== '9188040d-6c67-4c5b-b112-36a304b66dad',
            'name'           => (string) ($c['name'] ?? ''),
            'picture'        => '',
            'raw'            => $c,
        ];
    }

    /**
     * JwtVerifier compares issuers with an exact match, so a multi-tenant app
     * cannot pass a fixed string: each tenant signs with its own GUID in `iss`.
     *
     * For common/organizations/consumers we therefore read the tenant id from
     * the token and require `iss` to be exactly the issuer that tenant must
     * use. Reading it before verification is safe because the signature, the
     * audience and the nonce are all still checked afterwards — a forged `tid`
     * only produces an issuer string the token then has to match anyway.
     *
     * NOTE: `common` admits users from ANY Microsoft tenant by design. Pin a
     * tenant GUID in the provider settings to restrict sign-in to one company.
     *
     * @return list<string>
     */
    private function expectedIssuers(string $idToken): array
    {
        $tenant = $this->tenant();
        if (!in_array($tenant, ['common', 'organizations', 'consumers'], true)) {
            return ['https://login.microsoftonline.com/' . $tenant . '/v2.0'];
        }
        $parts = explode('.', $idToken);
        $claims = count($parts) === 3
            ? (array) json_decode((string) base64_decode(strtr($parts[1], '-_', '+/')), true)
            : [];
        $tid = (string) ($claims['tid'] ?? '');
        if ($tid === '' || !preg_match('/^[0-9a-fA-F-]{36}$/', $tid)) {
            throw new \RuntimeException('Microsoft identity token is missing a valid tenant id.');
        }
        return ['https://login.microsoftonline.com/' . $tid . '/v2.0'];
    }
}
