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

daycry / auth / 21003933517

14 Jan 2026 05:39PM UTC coverage: 66.786% (+0.2%) from 66.555%
21003933517

push

github

web-flow
Merge pull request #28 from daycry/development

Add OAuth 2.0 support and improve authentication extensibility

201 of 278 new or added lines in 24 files covered. (72.3%)

7 existing lines in 4 files now uncovered.

2246 of 3363 relevant lines covered (66.79%)

33.3 hits per line

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

65.17
/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\GenericProvider;
27
use League\OAuth2\Client\Token\AccessTokenInterface;
28
use TheNetworg\OAuth2\Client\Provider\Azure;
29

30
class OauthManager
31
{
32
    protected AuthConfig $config;
33
    protected AbstractProvider $provider;
34
    protected string $providerName;
35

36
    public function __construct(AuthConfig $config)
37
    {
38
        $this->config = $config;
5✔
39
    }
40

41
    /**
42
     * Set the provider instance directly (testing)
43
     */
44
    public function setProviderInstance(AbstractProvider $provider, string $name = 'test'): self
45
    {
46
        $this->provider     = $provider;
3✔
47
        $this->providerName = $name;
3✔
48

49
        return $this;
3✔
50
    }
51

52
    public function setProvider(string $providerName): self
53
    {
54
        if (! isset($this->config->providers[$providerName])) {
1✔
NEW
55
            throw new AuthenticationException(lang('Auth.unknownOauthProvider', [$providerName]));
×
56
        }
57

58
        $this->providerName = $providerName;
1✔
59
        $config             = $this->config->providers[$providerName];
1✔
60

61
        if ($providerName === 'azure') {
1✔
NEW
62
            if (! empty($config['tenant'])) {
×
NEW
63
                $config['urlAuthorize']   = 'https://login.microsoftonline.com/' . $config['tenant'] . '/oauth2/v2.0/authorize';
×
NEW
64
                $config['urlAccessToken'] = 'https://login.microsoftonline.com/' . $config['tenant'] . '/oauth2/v2.0/token';
×
65
            }
NEW
66
            $this->provider = new Azure($config);
×
67
        } else {
68
            // Default to GenericProvider or other specific ones
69
            $this->provider = new GenericProvider($config);
1✔
70
        }
71

72
        return $this;
1✔
73
    }
74

75
    public function refreshAccessToken(User $user): ?AccessTokenInterface
76
    {
NEW
77
        if (! isset($this->provider)) {
×
NEW
78
            throw new AuthenticationException(lang('Auth.unknownOauthProvider', [$this->providerName ?? 'null']));
×
79
        }
80

81
        /** @var UserIdentityModel $identityModel */
NEW
82
        $identityModel = model(UserIdentityModel::class);
×
83

NEW
84
        $type = 'oauth_' . $this->providerName;
×
85

86
        // Find identity for this user and provider
87
        /** @var UserIdentity|null $identity */
NEW
88
        $identity = $identityModel->where('user_id', $user->id)
×
NEW
89
            ->where('type', $type)
×
NEW
90
            ->first();
×
91

NEW
92
        if (! $identity || empty($identity->extra)) {
×
NEW
93
            return null;
×
94
        }
95

96
        try {
NEW
97
            $grant = new RefreshToken();
×
NEW
98
            $token = $this->provider->getAccessToken($grant, ['refresh_token' => $identity->extra]);
×
99

100
            // Update identity
NEW
101
            $identity->secret2 = $token->getToken();
×
NEW
102
            if ($token->getRefreshToken()) {
×
NEW
103
                $identity->extra = $token->getRefreshToken();
×
104
            }
105

NEW
106
            if ($token->getExpires()) {
×
NEW
107
                $identity->expires = Time::createFromTimestamp($token->getExpires());
×
108
            }
109

NEW
110
            $identityModel->save($identity);
×
111

NEW
112
            return $token;
×
NEW
113
        } catch (IdentityProviderException $e) {
×
114
            // Handle error (e.g. refresh token expired or revoked)
NEW
115
            return null;
×
116
        }
117
    }
118

119
    public function redirect(): RedirectResponse
120
    {
121
        $authorizationUrl = $this->provider->getAuthorizationUrl();
2✔
122
        session()->set('oauth2state', $this->provider->getState());
2✔
123

124
        return redirect()->to($authorizationUrl);
2✔
125
    }
126

127
    public function handleCallback(string $code, string $state): User
128
    {
129
        $sessionState = session()->get('oauth2state');
3✔
130

131
        if ($state === '' || $state === '0' || ($state !== $sessionState)) {
3✔
132
            session()->remove('oauth2state');
1✔
133

134
            throw new AuthenticationException(lang('Auth.invalidOauthState'));
1✔
135
        }
136

137
        session()->remove('oauth2state');
2✔
138

139
        try {
140
            // Try to get an access token (using the authorization code grant)
141
            $token = $this->provider->getAccessToken('authorization_code', [
2✔
142
                'code' => $code,
2✔
143
            ]);
2✔
144

145
            // Optional: Now you have a token you can look up a users profile data
146
            $userProfile = $this->provider->getResourceOwner($token);
2✔
147

148
            return $this->processUser($userProfile, $token);
2✔
NEW
149
        } catch (IdentityProviderException $e) {
×
NEW
150
            throw new AuthenticationException($e->getMessage());
×
151
        }
152
    }
153

154
    protected function processUser($userProfile, $token): User
155
    {
156
        /** @var UserIdentityModel $identityModel */
157
        $identityModel = model(UserIdentityModel::class);
2✔
158

159
        // Normalize data based on provider
160
        $email    = null;
2✔
161
        $name     = null;
2✔
162
        $socialId = $userProfile->getId();
2✔
163

164
        if ($this->providerName === 'azure') {
2✔
NEW
165
            $email = $userProfile->claim('email') ?: $userProfile->claim('unique_name');
×
NEW
166
            $name  = $userProfile->claim('name');
×
167
        } else {
168
            // Standard OIDC/OAuth claims
169
            $data  = $userProfile->toArray();
2✔
170
            $email = $data['email'] ?? null;
2✔
171
            $name  = $data['name'] ?? null;
2✔
172
        }
173

174
        if (empty($email)) {
2✔
NEW
175
            throw new AuthenticationException(lang('Auth.emailNotFoundInOauth'));
×
176
        }
177

178
        // Check if identity exists
179
        // We use 'oauth_{provider}' as type
180
        $type     = 'oauth_' . $this->providerName;
2✔
181
        $identity = $identityModel->where('type', $type)
2✔
182
            ->where('secret', $socialId)
2✔
183
            ->first();
2✔
184

185
        $user = null;
2✔
186

187
        if ($identity) {
2✔
188
            /** @var UserIdentity $identity */
NEW
189
            $user = $identity->user();
×
190
        } else {
191
            // Check if user with email exists to link
192
            $provider = auth()->getProvider();
2✔
193
            $user     = $provider->findByCredentials(['email' => $email]);
2✔
194

195
            if (! $user instanceof User) {
2✔
196
                // Create new user
197
                $user = new User([
1✔
198
                    'username' => explode('@', $email)[0] . '_' . bin2hex(random_bytes(3)), // Generate a username
1✔
199
                    'active'   => true,
1✔
200
                ]);
1✔
201
                $provider->save($user);
1✔
202
                $user = $provider->findById($provider->getInsertID());
1✔
203

204
                // Add to default group
205
                $provider->addToDefaultGroup($user);
1✔
206
            }
207

208
            // Create identity
209
            $identityModel->insert([
2✔
210
                'user_id' => $user->id,
2✔
211
                'type'    => $type,
2✔
212
                'secret'  => $socialId, // Social ID
2✔
213
                'secret2' => $token->getToken(), // Access Token (optional to store)
2✔
214
                'extra'   => $token->getRefreshToken(), // Refresh Token (optional)
2✔
215
                'expires' => $token->getExpires() ? Time::createFromTimestamp($token->getExpires()) : null,
2✔
216
            ]);
2✔
217
        }
218

219
        // Login the user
220
        auth()->login($user);
2✔
221

222
        return $user;
2✔
223
    }
224
}
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