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 */ private function storedClaims(array $claims): array { return [ 'oid' => $claims['oid'], 'name' => $claims['name'], 'preferred_username' => $claims['preferred_username'], ]; } }