Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions src/OneID/OneIDExtendSocialite.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

namespace Aslnbxrz\OneID;

use SocialiteProviders\Manager\SocialiteWasCalled;

class OneIDExtendSocialite
{
public function handle(SocialiteWasCalled $event): void
{
$event->extendSocialite('oneid', Provider::class);
}
}


57 changes: 57 additions & 0 deletions src/OneID/OneIDLogout.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<?php

namespace Aslnbxrz\OneID;

use GuzzleHttp\Client;
use GuzzleHttp\RequestOptions;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Log;
use Throwable;

final class OneIDLogout
{
public function handle($accessTokenOrSessionId): void
{
$client = new Client();
try {
$res = $client->post(rtrim($this->getConfig('base_url', 'https://sso.egov.uz'), '/') . '/sso/oauth/Authorization.do', [
RequestOptions::FORM_PARAMS => [
'grant_type' => 'one_log_out',
'client_id' => $this->getConfig('client_id'),
'client_secret' => $this->getConfig('client_secret'),
'access_token' => $accessTokenOrSessionId,
'scope' => $this->getConfig('scope', 'one_code'),
],
'headers' => ['Accept' => 'application/json'],
]);
Log::info('OneIDSocialiteLogout', [
'status_code' => $res->getStatusCode(),
'res' => $res->getBody()->getContents(),
'accessTokenOrSessionId' => $accessTokenOrSessionId,
]);
} catch (Throwable $e) {
Log::error('OneIDSocialiteThrow', [
'throw' => $e->getMessage(),
'config' => $this->getConfig(),
]);
}
}

/**
* @param string|null $key
* @param mixed|null $default
* @return mixed|array
*/
protected function getConfig(?string $key = null, mixed $default = null): mixed
{
$config = Config::get('services.oneid');
// check manually if a key is given and if it exists in the config
// this has to be done to check for spoofed additional config keys so that null isn't returned
if (!empty($key) && empty($config[$key])) {
return $default;
}

return $key ? Arr::get($config, $key, $default) : $config;
}
}
43 changes: 43 additions & 0 deletions src/OneID/OneIDUser.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php

namespace Aslnbxrz\OneID;

use SocialiteProviders\Manager\OAuth2\User as OAuth2User;

class OneIDUser extends OAuth2User
{
/** Return PINFL (citizen ID) */
public function getPinfl(): ?string
{
return $this->attributes['pinfl'] ?? null;
}

/** Return OneID session id */
public function getSessionId(): ?string
{
return $this->attributes['sess_id'] ?? null;
}

/** Return passport number */
public function getPassport(): ?string
{
return $this->attributes['passport'] ?? null;
}

/** Return normalized phone */
public function getPhone(): ?string
{
return $this->attributes['phone'] ?? null;
}

/** Lightweight gender guess based on PINFL first digit (if numeric) */
public function getGender(): ?string
{
$pin = $this->getPinfl();
if (empty($pin) || !ctype_digit($pin)) {
return null; // or 'unknown'
}
// Odd => male, Even => female
return ((int)$pin[0]) % 2 ? 'male' : 'female';
}
}
91 changes: 91 additions & 0 deletions src/OneID/Provider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
<?php

namespace Aslnbxrz\OneID;

use GuzzleHttp\RequestOptions;
use SocialiteProviders\Manager\OAuth2\AbstractProvider;

class Provider extends AbstractProvider
{
public const IDENTIFIER = 'ONEID';

protected string $scope = 'one_code';

protected function getAuthUrl($state): string
{
return $this->buildAuthUrlFromBase(rtrim($this->getBaseUrl(), '/') . '/sso/oauth/Authorization.do', $state);
}

protected function getTokenUrl(): string
{
return rtrim($this->getBaseUrl(), '/') . '/sso/oauth/Authorization.do';
}

protected function getUserByToken($token)
{
$response = $this->getHttpClient()->post($this->getTokenUrl(), [
RequestOptions::FORM_PARAMS => [
'grant_type' => 'one_access_token_identify',
'client_id' => $this->clientId,
'client_secret' => $this->clientSecret,
'access_token' => $token,
'scope' => $this->getScope(),
],
'headers' => ['Accept' => 'application/json'],
]);

return json_decode($response->getBody()->getContents(), true);
}

protected function getCodeFields($state = null): array
{
$fields = parent::getCodeFields($state);
$fields['response_type'] = 'one_code';
$fields['scope'] = $this->getScope();
$fields['state'] = $state;
return $fields;
}

protected function getTokenFields($code): array
{
$fields = parent::getTokenFields($code);
$fields['grant_type'] = 'one_authorization_code';
return $fields;
}

protected function mapUserToObject(array $user): OneIDUser
{
// Build fallback name if full_name is missing
$name = $user['full_name'] ?? trim(implode(' ', array_filter([
$user['first_name'] ?? null,
$user['sur_name'] ?? null,
$user['mid_name'] ?? null,
])));

return (new OneIDUser())->setRaw($user)->map([
// Standard Socialite fields
'id' => $user['user_id'] ?? $user['pin'] ?? $user['sess_id'] ?? null,
'name' => $name ?: null,
'email' => $user['email'] ?? null,
'avatar' => $user['avatar'] ?? null,

// Custom fields (use consistent keys!)
'pinfl' => $user['pin'] ?? null, // citizen PIN/INN
'sess_id' => $user['sess_id'] ?? null, // OneID session id
'passport' => $user['pport_no'] ?? null, // passport number
'phone' => $user['mob_phone_no'] ?? $user['phone'] ?? null, // prefer mob_phone_no
]);
}

protected function getBaseUrl(): string
{
return $this->getConfig('base_url', 'https://sso.egov.uz');
}

protected function getScope(): string
{
return (string)($this->getConfig('scope', $this->scope));
}
}


Loading