166 lines
6.2 KiB
PHP
166 lines
6.2 KiB
PHP
<?php
|
|
|
|
namespace App\Domain\Identity\Services;
|
|
|
|
use App\Domain\Audit\Models\AuditLog;
|
|
use App\Domain\Identity\Exceptions\MicrosoftAuthenticationException;
|
|
use App\Domain\Identity\Models\ExternalIdentity;
|
|
use App\Models\User;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
final class MicrosoftIdentityService
|
|
{
|
|
/** @param array{sub:string,oid:?string,tid:string,email:string,name:string,preferred_username:?string} $claims */
|
|
public function link(User $user, array $claims, Request $request): ExternalIdentity
|
|
{
|
|
if (! $user->active) {
|
|
throw new MicrosoftAuthenticationException('A conta local está desativada.');
|
|
}
|
|
|
|
return DB::transaction(function () use ($user, $claims, $request): ExternalIdentity {
|
|
$subjectIdentity = ExternalIdentity::query()
|
|
->where('provider', 'microsoft')
|
|
->where('provider_subject', $claims['sub'])
|
|
->lockForUpdate()
|
|
->first();
|
|
|
|
if ($subjectIdentity !== null && $subjectIdentity->user_id !== $user->id) {
|
|
throw new MicrosoftAuthenticationException('Esta identidade Microsoft já está associada a outra conta.');
|
|
}
|
|
|
|
$userIdentity = ExternalIdentity::query()
|
|
->where('provider', 'microsoft')
|
|
->where('user_id', $user->id)
|
|
->lockForUpdate()
|
|
->first();
|
|
|
|
if ($userIdentity !== null && $userIdentity->provider_subject !== $claims['sub']) {
|
|
throw new MicrosoftAuthenticationException('A conta local já tem outra identidade Microsoft associada.');
|
|
}
|
|
|
|
$identity = $subjectIdentity ?? $userIdentity ?? new ExternalIdentity([
|
|
'provider' => 'microsoft',
|
|
'provider_subject' => $claims['sub'],
|
|
]);
|
|
$identity->fill([
|
|
'user_id' => $user->id,
|
|
'tenant_id' => $claims['tid'],
|
|
'provider_email' => $claims['email'],
|
|
'claims' => $this->storedClaims($claims),
|
|
'last_login_at' => now(),
|
|
]);
|
|
$identity->save();
|
|
|
|
AuditLog::create([
|
|
'actor_id' => $user->id,
|
|
'event' => 'identity.microsoft_linked',
|
|
'auditable_type' => $user->getMorphClass(),
|
|
'auditable_id' => $user->id,
|
|
'ip_address' => $request->ip(),
|
|
'user_agent' => $request->userAgent(),
|
|
'after' => [
|
|
'provider' => 'microsoft',
|
|
'tenant_id' => $claims['tid'],
|
|
'provider_email' => $claims['email'],
|
|
],
|
|
]);
|
|
|
|
return $identity;
|
|
}, 3);
|
|
}
|
|
|
|
/** @param array{sub:string,oid:?string,tid:string,email:string,name:string,preferred_username:?string} $claims */
|
|
public function userForLogin(array $claims, Request $request): User
|
|
{
|
|
return DB::transaction(function () use ($claims, $request): User {
|
|
$identity = ExternalIdentity::query()
|
|
->where('provider', 'microsoft')
|
|
->where('provider_subject', $claims['sub'])
|
|
->lockForUpdate()
|
|
->first();
|
|
|
|
if ($identity === null) {
|
|
throw new MicrosoftAuthenticationException(
|
|
'Esta identidade ainda não está associada. Entre com a conta local e associe-a em Segurança.',
|
|
);
|
|
}
|
|
|
|
$user = User::query()->whereKey($identity->user_id)->lockForUpdate()->first();
|
|
if ($user === null || ! $user->active) {
|
|
throw new MicrosoftAuthenticationException('A conta local associada está desativada.');
|
|
}
|
|
|
|
if (! hash_equals(strtolower((string) $identity->tenant_id), strtolower($claims['tid']))) {
|
|
throw new MicrosoftAuthenticationException('A identidade não pertence ao tenant institucional associado.');
|
|
}
|
|
|
|
$identity->update([
|
|
'provider_email' => $claims['email'],
|
|
'claims' => $this->storedClaims($claims),
|
|
'last_login_at' => now(),
|
|
]);
|
|
$user->update(['last_login_at' => now()]);
|
|
|
|
AuditLog::create([
|
|
'actor_id' => $user->id,
|
|
'event' => 'identity.microsoft_login',
|
|
'auditable_type' => $user->getMorphClass(),
|
|
'auditable_id' => $user->id,
|
|
'ip_address' => $request->ip(),
|
|
'user_agent' => $request->userAgent(),
|
|
'metadata' => [
|
|
'provider' => 'microsoft',
|
|
'tenant_id' => $claims['tid'],
|
|
],
|
|
]);
|
|
|
|
return $user;
|
|
}, 3);
|
|
}
|
|
|
|
public function unlink(User $user, Request $request): void
|
|
{
|
|
DB::transaction(function () use ($user, $request): void {
|
|
$identity = ExternalIdentity::query()
|
|
->where('provider', 'microsoft')
|
|
->where('user_id', $user->id)
|
|
->lockForUpdate()
|
|
->first();
|
|
|
|
if ($identity === null) {
|
|
return;
|
|
}
|
|
|
|
$before = [
|
|
'provider' => 'microsoft',
|
|
'tenant_id' => $identity->tenant_id,
|
|
'provider_email' => $identity->provider_email,
|
|
];
|
|
$identity->delete();
|
|
|
|
AuditLog::create([
|
|
'actor_id' => $user->id,
|
|
'event' => 'identity.microsoft_unlinked',
|
|
'auditable_type' => $user->getMorphClass(),
|
|
'auditable_id' => $user->id,
|
|
'ip_address' => $request->ip(),
|
|
'user_agent' => $request->userAgent(),
|
|
'before' => $before,
|
|
]);
|
|
}, 3);
|
|
}
|
|
|
|
/** @param array{sub:string,oid:?string,tid:string,email:string,name:string,preferred_username:?string} $claims
|
|
* @return array<string, string|null>
|
|
*/
|
|
private function storedClaims(array $claims): array
|
|
{
|
|
return [
|
|
'oid' => $claims['oid'],
|
|
'name' => $claims['name'],
|
|
'preferred_username' => $claims['preferred_username'],
|
|
];
|
|
}
|
|
}
|