• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

daycry / auth / 22527357078

28 Feb 2026 07:22PM UTC coverage: 63.267% (+0.7%) from 62.568%
22527357078

push

github

daycry
Remove PHP 8.1 from PHPUnit CI matrix

Update .github/workflows/phpunit.yml to drop PHP 8.1 from the test matrix. CI will now run PHPUnit only on PHP 8.2 and 8.3, reducing the matrix to current supported versions.

3064 of 4843 relevant lines covered (63.27%)

41.52 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

69.16
/src/Libraries/Oauth/OauthManager.php
1
<?php
2

3
declare(strict_types=1);
4

5
/**
6
 * This file is part of Daycry Auth.
7
 *
8
 * (c) Daycry <daycry9@proton.me>
9
 *
10
 * For the full copyright and license information, please view
11
 * the LICENSE file that was distributed with this source code.
12
 */
13

14
namespace Daycry\Auth\Libraries\Oauth;
15

16
use CodeIgniter\HTTP\RedirectResponse;
17
use CodeIgniter\I18n\Time;
18
use Daycry\Auth\Config\Auth as AuthConfig;
19
use Daycry\Auth\Entities\User;
20
use Daycry\Auth\Entities\UserIdentity;
21
use Daycry\Auth\Exceptions\AuthenticationException;
22
use Daycry\Auth\Models\UserIdentityModel;
23
use League\OAuth2\Client\Grant\RefreshToken;
24
use League\OAuth2\Client\Provider\AbstractProvider;
25
use League\OAuth2\Client\Provider\Exception\IdentityProviderException;
26
use League\OAuth2\Client\Provider\Facebook;
27
use League\OAuth2\Client\Provider\GenericProvider;
28
use League\OAuth2\Client\Provider\Github;
29
use League\OAuth2\Client\Provider\Google;
30
use League\OAuth2\Client\Provider\ResourceOwnerInterface;
31
use League\OAuth2\Client\Token\AccessTokenInterface;
32
use TheNetworg\OAuth2\Client\Provider\Azure;
33
use TheNetworg\OAuth2\Client\Provider\AzureResourceOwner;
34

35
class OauthManager
36
{
37
    protected AuthConfig $config;
38
    protected AbstractProvider $provider;
39
    protected string $providerName;
40

41
    /**
42
     * Map of provider alias → FQCN for well-known providers.
43
     * Any provider not listed here falls back to GenericProvider.
44
     *
45
     * @var array<string, class-string<AbstractProvider>>
46
     */
47
    protected array $providerMap = [
48
        'azure'    => Azure::class,
49
        'google'   => Google::class,
50
        'facebook' => Facebook::class,
51
        'github'   => Github::class,
52
    ];
53

54
    public function __construct(AuthConfig $config)
5✔
55
    {
56
        $this->config = $config;
5✔
57
    }
58

59
    /**
60
     * Set the provider instance directly (testing).
61
     */
62
    public function setProviderInstance(AbstractProvider $provider, string $name = 'test'): self
3✔
63
    {
64
        $this->provider     = $provider;
3✔
65
        $this->providerName = $name;
3✔
66

67
        return $this;
3✔
68
    }
69

70
    /**
71
     * Resolve and instantiate the OAuth provider for the given alias.
72
     */
73
    public function setProvider(string $providerName): self
1✔
74
    {
75
        if (! isset($this->config->providers[$providerName])) {
1✔
76
            throw new AuthenticationException(lang('Auth.unknownOauthProvider', [$providerName]));
×
77
        }
78

79
        $this->providerName = $providerName;
1✔
80
        $providerConfig     = $this->config->providers[$providerName];
1✔
81

82
        // Azure needs tenant-specific URLs when a tenant is given
83
        if ($providerName === 'azure' && ! empty($providerConfig['tenant'])) {
1✔
84
            $tenant                           = $providerConfig['tenant'];
×
85
            $providerConfig['urlAuthorize']   = 'https://login.microsoftonline.com/' . $tenant . '/oauth2/v2.0/authorize';
×
86
            $providerConfig['urlAccessToken'] = 'https://login.microsoftonline.com/' . $tenant . '/oauth2/v2.0/token';
×
87
        }
88

89
        $class          = $this->providerMap[$providerName] ?? GenericProvider::class;
1✔
90
        $this->provider = new $class($providerConfig);
1✔
91

92
        return $this;
1✔
93
    }
94

95
    /**
96
     * Refresh the stored access token for a user via the refresh-token grant.
97
     */
98
    public function refreshAccessToken(User $user): ?AccessTokenInterface
×
99
    {
100
        if (! isset($this->provider)) {
×
101
            throw new AuthenticationException(lang('Auth.unknownOauthProvider', [$this->providerName ?? 'null']));
×
102
        }
103

104
        /** @var UserIdentityModel $identityModel */
105
        $identityModel = model(UserIdentityModel::class);
×
106

107
        $type = 'oauth_' . $this->providerName;
×
108

109
        /** @var UserIdentity|null $identity */
110
        $identity = $identityModel->where('user_id', $user->id)
×
111
            ->where('type', $type)
×
112
            ->first();
×
113

114
        if (! $identity || empty($identity->extra)) {
×
115
            return null;
×
116
        }
117

118
        try {
119
            $grant = new RefreshToken();
×
120
            $token = $this->provider->getAccessToken($grant, ['refresh_token' => $identity->extra]);
×
121

122
            $identity->secret2 = $token->getToken();
×
123

124
            if ($token->getRefreshToken()) {
×
125
                $identity->extra = $token->getRefreshToken();
×
126
            }
127

128
            if ($token->getExpires()) {
×
129
                $identity->expires = Time::createFromTimestamp($token->getExpires());
×
130
            }
131

132
            $identityModel->save($identity);
×
133

134
            return $token;
×
135
        } catch (IdentityProviderException) {
×
136
            return null;
×
137
        }
138
    }
139

140
    /**
141
     * Redirect the user to the OAuth provider's authorization page.
142
     */
143
    public function redirect(): RedirectResponse
2✔
144
    {
145
        $authorizationUrl = $this->provider->getAuthorizationUrl();
2✔
146
        session()->set('oauth2state', $this->provider->getState());
2✔
147

148
        return redirect()->to($authorizationUrl);
2✔
149
    }
150

151
    /**
152
     * Handle the authorization callback from the OAuth provider.
153
     */
154
    public function handleCallback(string $code, string $state): User
3✔
155
    {
156
        $sessionState = session()->get('oauth2state');
3✔
157

158
        if ($state === '' || $state === '0' || ($state !== $sessionState)) {
3✔
159
            session()->remove('oauth2state');
1✔
160

161
            throw new AuthenticationException(lang('Auth.invalidOauthState'));
1✔
162
        }
163

164
        session()->remove('oauth2state');
2✔
165

166
        try {
167
            $token       = $this->provider->getAccessToken('authorization_code', ['code' => $code]);
2✔
168
            $userProfile = $this->provider->getResourceOwner($token);
2✔
169

170
            return $this->processUser($userProfile, $token);
2✔
171
        } catch (IdentityProviderException $e) {
×
172
            throw new AuthenticationException($e->getMessage());
×
173
        }
174
    }
175

176
    /**
177
     * Extract normalised email, name, and social ID from a resource owner.
178
     *
179
     * Each League provider exposes data differently:
180
     *   - Google / Facebook / GitHub : getEmail(), getName()
181
     *   - Azure                      : claim('email') / claim('unique_name')
182
     *   - GenericProvider & others   : toArray() fallback
183
     *
184
     * @return array{id: string, email: string|null, name: string|null}
185
     */
186
    protected function extractUserData(ResourceOwnerInterface $resourceOwner): array
2✔
187
    {
188
        $id    = (string) $resourceOwner->getId();
2✔
189
        $email = null;
2✔
190
        $name  = null;
2✔
191

192
        if ($this->providerName === 'azure') {
2✔
193
            // Azure resource-owner uses claim() for user attributes
194
            /** @var AzureResourceOwner $resourceOwner */
195
            $email = $resourceOwner->claim('email') ?: $resourceOwner->claim('unique_name');
×
196
            $name  = $resourceOwner->claim('name');
×
197
        } else {
198
            // Standard League providers implement getEmail() / getName()
199
            if (method_exists($resourceOwner, 'getEmail')) {
2✔
200
                $email = $resourceOwner->getEmail();
×
201
            }
202

203
            if (method_exists($resourceOwner, 'getName')) {
2✔
204
                $name = $resourceOwner->getName();
×
205
            }
206

207
            // toArray() fallback for GenericProvider or providers without typed methods
208
            if ($email === null || $name === null) {
2✔
209
                $data = $resourceOwner->toArray();
2✔
210
                $email ??= $data['email'] ?? null;
2✔
211
                $name ??= $data['name'] ?? $data['login'] ?? null;
2✔
212
            }
213
        }
214

215
        return ['id' => $id, 'email' => $email, 'name' => $name];
2✔
216
    }
217

218
    /**
219
     * Find or create a local user and OAuth identity for the resource owner.
220
     */
221
    protected function processUser(ResourceOwnerInterface $userProfile, AccessTokenInterface $token): User
2✔
222
    {
223
        /** @var UserIdentityModel $identityModel */
224
        $identityModel = model(UserIdentityModel::class);
2✔
225

226
        $data     = $this->extractUserData($userProfile);
2✔
227
        $socialId = $data['id'];
2✔
228
        $email    = $data['email'];
2✔
229
        $name     = $data['name'];
2✔
230

231
        if (empty($email)) {
2✔
232
            throw new AuthenticationException(lang('Auth.emailNotFoundInOauth'));
×
233
        }
234

235
        $type     = 'oauth_' . $this->providerName;
2✔
236
        $identity = $identityModel->where('type', $type)
2✔
237
            ->where('secret', $socialId)
2✔
238
            ->first();
2✔
239

240
        $user = null;
2✔
241

242
        if ($identity) {
2✔
243
            /** @var UserIdentity $identity */
244
            $user = $identity->user();
×
245
        } else {
246
            $provider = auth()->getProvider();
2✔
247
            $user     = $provider->findByCredentials(['email' => $email]);
2✔
248

249
            if (! $user instanceof User) {
2✔
250
                $username = explode('@', $email)[0] . '_' . bin2hex(random_bytes(3));
1✔
251
                $user     = new User([
1✔
252
                    'username' => $username,
1✔
253
                    'active'   => true,
1✔
254
                ]);
1✔
255
                $provider->save($user);
1✔
256
                $user = $provider->findById($provider->getInsertID());
1✔
257

258
                $provider->addToDefaultGroup($user);
1✔
259
            }
260

261
            $identityModel->insert([
2✔
262
                'user_id' => $user->id,
2✔
263
                'type'    => $type,
2✔
264
                'name'    => $name ?? $email,
2✔
265
                'secret'  => $socialId,
2✔
266
                'secret2' => $token->getToken(),
2✔
267
                'extra'   => $token->getRefreshToken(),
2✔
268
                'expires' => $token->getExpires() ? Time::createFromTimestamp($token->getExpires()) : null,
2✔
269
            ]);
2✔
270
        }
271

272
        auth()->login($user);
2✔
273

274
        return $user;
2✔
275
    }
276
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc