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

NIT-Administrative-Systems / SysDev-laravel-soa / 13508657684

24 Feb 2025 09:57PM UTC coverage: 45.13% (-0.1%) from 45.277%
13508657684

Pull #184

github

web-flow
Merge 265b9b845 into a782f5d90
Pull Request #184: Laravel 12 Support

0 of 4 new or added lines in 2 files covered. (0.0%)

1 existing line in 1 file now uncovered.

278 of 616 relevant lines covered (45.13%)

14.48 hits per line

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

0.0
/src/Auth/OAuth2/NorthwesternAzureProvider.php
1
<?php
2

3
namespace Northwestern\SysDev\SOA\Auth\OAuth2;
4

5
use GuzzleHttp\Exception\ClientException;
6
use Illuminate\Support\Arr;
7
use Illuminate\Support\Facades\Crypt;
8
use Illuminate\Support\Str;
9
use Laravel\Socialite\Two\InvalidStateException;
10
use Laravel\Socialite\Two\User as TwoUser;
11
use Northwestern\SysDev\SOA\Auth\OAuth2\TokenVerifier\Contract\TokenVerifierInterface;
12
use SocialiteProviders\Manager\OAuth2\AbstractProvider;
13
use SocialiteProviders\Manager\OAuth2\User;
14

15
class NorthwesternAzureProvider extends AbstractProvider
16
{
17
    public const IDENTIFIER = 'NU_AZURE';
18

19
    public const NU_TENANT_ID = 'northwestern.edu';
20

21
    public const NU_DOMAIN_HINT = 'northwestern.edu';
22

23
    public const STATE_PART_SEPARATOR = '|';
24

25
    public const ISSUER_ATTRIBUTE = 'idIssuer';
26

27
    protected $encodingType = PHP_QUERY_RFC3986;
28

29
    /**
30
     * The Microsoft Graph API endpoint for profile information.
31
     *
32
     * @var string
33
     */
34
    protected $graphUrl = 'https://graph.microsoft.com/v1.0/me/';
35

36
    /** @var string[] Default scopes to request */
37
    protected $scopes = ['openid'];
38

39
    protected $scopeSeparator = ' ';
40

41
    /**
42
     * {@inheritDoc}
43
     *
44
     * Includes the intended URL in the state, since this will be lost when Azure POSTs the user back.
45
     *
46
     * The POST does not originate from the app, so the app's session cookie will not be part of the
47
     * request, losing all previously-saved session variables.
48
     *
49
     * This is a recommended use for the state per Microsoft's OpenID Connect docs.
50
     */
51
    protected function getState()
52
    {
53
        $state = implode(self::STATE_PART_SEPARATOR, [
×
54
            Str::random(40),
×
55
            $this->request->session()->pull('url.intended', null),
×
56
        ]);
×
57

58
        return Crypt::encryptString($state);
×
59
    }
60

61
    /**
62
     * {@inheritdoc}
63
     */
64
    public function user(): TwoUser
65
    {
66
        $idTokenJwt = $this->request->input('id_token');
×
67
        if (! $idTokenJwt) {
×
68
            throw new InvalidStateException('id_token value was not found in response');
×
69
        }
70

71
        // Throws if the token isn't signed properly
72
        $idToken = $this->verifierService()->parseAndVerify($idTokenJwt);
×
73

74
        // Fix to enable stateless
75
        $response = $this->getAccessTokenResponse($this->request->input('code'));
×
76

77
        $userToken = $this->getUserByToken(
×
78
            $token = Arr::get($response, 'access_token')
×
79
        );
×
80

81
        if ($this->usesState()) {
×
82
            $state = explode('.', $idToken->claims()->get('nonce'))[1];
×
83
            if ($state === $this->request->input('state')) {
×
84
                $this->request->session()->put('state', $state);
×
85
            }
86

87
            if ($this->hasInvalidState()) {
×
88
                throw new InvalidStateException();
×
89
            }
90

91
            $intendedUrl = $this->unpackState($state);
×
92
            if ($intendedUrl) {
×
93
                $this->request->session()->put('url.intended', $intendedUrl);
×
94
            }
95
        }
96

97
        $user = $this->mapUserToObject($userToken);
×
98
        $user->attributes[self::ISSUER_ATTRIBUTE] = $idToken->claims()->get('iss');
×
99

100
        if ($user instanceof User) {
×
101
            $user->setAccessTokenResponseBody($response);
×
102
        }
103

104
        /**
105
         * Return type error is because the socialite docblock's return type is wrong.
106
         *
107
         * @phpstan-ignore-next-line
108
         */
109
        return $user->setToken($token)
×
110
            ->setRefreshToken(Arr::get($response, 'refresh_token'))
×
111
            ->setExpiresIn(Arr::get($response, 'expires_in'));
×
112
    }
113

114
    protected function verifierService(): TokenVerifierInterface
115
    {
116
        return resolve(TokenVerifierInterface::class);
×
117
    }
118

119
    /**
120
     * {@inheritdoc}
121
     */
122
    protected function getAuthUrl($state)
123
    {
124
        return $this->buildAuthUrlFromBase(
×
125
            'https://login.microsoftonline.com/'.($this->config['tenant'] ?: self::NU_TENANT_ID).'/oauth2/v2.0/authorize',
×
126
            $state
×
127
        );
×
128
    }
129

130
    protected function getTokenUrl()
131
    {
132
        return 'https://login.microsoftonline.com/'.($this->config['tenant'] ?: self::NU_TENANT_ID).'/oauth2/v2.0/token';
×
133
    }
134

135
    public function getLogoutUrl()
136
    {
137
        return 'https://login.microsoftonline.com/'.($this->config['tenant'] ?: self::NU_TENANT_ID).'/oauth2/v2.0/logout';
×
138
    }
139

140
    public function getAccessToken($code)
141
    {
142
        $response = $this->getHttpClient()->post($this->getTokenUrl(), [
×
143
            'form_params' => $this->getTokenFields($code),
×
144
        ]);
×
145

146
        $this->credentialsResponseBody = json_decode($response->getBody()->getContents(), true);
×
147

148
        return $this->parseAccessToken($this->credentialsResponseBody);
×
149
    }
150

151
    /**
152
     * {@inheritdoc}
153
     */
154
    protected function getUserByToken($token)
155
    {
156
        try {
157
            $response = $this->getHttpClient()->get($this->graphUrl, [
×
158
                'headers' => [
×
159
                    'Accept' => 'application/json',
×
160
                    'Authorization' => 'Bearer '.$token,
×
161
                ],
×
162
            ]);
×
163
        } catch (ClientException $e) {
×
164
            throw new MicrosoftGraphError($e);
×
165
        }
166

167
        return json_decode($response->getBody()->getContents(), true);
×
168
    }
169

170
    /**
171
     * {@inheritdoc}
172
     */
173
    protected function mapUserToObject(array $user)
174
    {
175
        return (new User())->setRaw($user)->map([
×
176
            'id' => $user['id'],
×
177
            'nickname' => null,
×
178
            'name' => $user['displayName'],
×
179
            'email' => $user['mail'],
×
180
            'avatar' => null,
×
181
        ]);
×
182
    }
183

184
    /**
185
     * {@inheritdoc}
186
     */
187
    protected function getTokenFields($code)
188
    {
189
        return array_merge(parent::getTokenFields($code), [
×
190
            'grant_type' => 'authorization_code',
×
191
            'scope' => $this->formatScopes($this->getScopes(), $this->scopeSeparator),
×
192
        ]);
×
193
    }
194

195
    /**
196
     * {@inheritDoc}
197
     */
198
    protected function getCodeFields($state = null)
199
    {
200
        $fields = [
×
201
            'client_id' => $this->clientId,
×
202
            'redirect_uri' => $this->redirectUrl,
×
203
            'scope' => $this->formatScopes($this->getScopes(), $this->scopeSeparator),
×
204
            'response_type' => 'id_token code',
×
205
            'response_mode' => 'form_post',
×
206
            'domain_hint' => $this->domainHint(),
×
207
        ];
×
208

209
        if ($this->usesState()) {
×
210
            $fields['state'] = $state;
×
211
            $fields['nonce'] = sprintf('%s.%s', Str::uuid(), $state);
×
212
        }
213

214
        return array_merge($fields, $this->parameters);
×
215
    }
216

217
    /**
218
     * {@inheritDoc}
219
     */
220
    protected function hasInvalidState()
221
    {
222
        if ($this->isStateless()) {
×
223
            return false;
×
224
        }
225

226
        $state = $this->request->session()->pull('state');
×
227

228
        return ! (strlen($state) > 0 && $this->request->input('state') === $state);
×
229
    }
230

231
    /**
232
     * Get the access token response for the given code.
233
     *
234
     * @param  string  $code
235
     * @return array
236
     */
237
    public function getAccessTokenResponse($code)
238
    {
239
        $response = $this->getHttpClient()->post($this->getTokenUrl(), [
×
240
            'form_params' => $this->getTokenFields($code),
×
241
        ]);
×
242

243
        return json_decode($response->getBody(), true);
×
244
    }
245

246
    /**
247
     * @return string|null URL that the user should be redirected to, if any.
248
     */
249
    protected function unpackState(string $state)
250
    {
251
        $parts = explode(self::STATE_PART_SEPARATOR, Crypt::decryptString($state));
×
252

253
        return $parts[1] ?? null;
×
254
    }
255

256
    /**
257
     * Preserves backwards-compatability now that the domain hint is configurable.
258
     */
259
    protected function domainHint(): ?string
260
    {
261
        if ($this->config['tenant'] === null) {
×
262
            return $this->config['domain_hint'] ?: self::NU_DOMAIN_HINT;
×
263
        }
264

265
        return $this->config['domain_hint'];
×
266
    }
267

268
    /**
269
     * Add the additional configuration key 'tenant' to enable the branded sign-in experience.
270
     *
271
     * @return array
272
     */
273
    public static function additionalConfigKeys()
274
    {
275
        return ['tenant', 'domain_hint'];
×
276
    }
277
}
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