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

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

06 Feb 2024 07:28PM UTC coverage: 45.675% (-0.2%) from 45.913%
7805057844

push

github

web-flow
Housekeeping (#162)

11 of 35 new or added lines in 14 files covered. (31.43%)

9 existing lines in 7 files now uncovered.

264 of 578 relevant lines covered (45.67%)

14.07 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 SocialiteProviders\Manager\OAuth2\AbstractProvider;
12
use SocialiteProviders\Manager\OAuth2\User;
13

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

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

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

22
    public const STATE_PART_SEPARATOR = '|';
23

24
    protected $encodingType = PHP_QUERY_RFC3986;
25

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

33
    /** @var string[] Default scopes to request */
34
    protected $scopes = ['openid'];
35

36
    protected $scopeSeparator = ' ';
37

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

55
        return Crypt::encryptString($state);
×
56
    }
57

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

68
        // Throws if the token isn't signed properly
69
        $idToken = AzureTokenVerifier::parseAndVerify($idTokenJwt);
×
70

71
        //Temporary fix to enable stateless
72
        $response = $this->getAccessTokenResponse($this->request->input('code'));
×
73

74
        $userToken = $this->getUserByToken(
×
75
            $token = Arr::get($response, 'access_token')
×
76
        );
×
77

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

84
            if ($this->hasInvalidState()) {
×
85
                throw new InvalidStateException();
×
86
            }
87

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

94
        $user = $this->mapUserToObject($userToken);
×
95

96
        if ($user instanceof User) {
×
97
            $user->setAccessTokenResponseBody($response);
×
98
        }
99

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

110
    /**
111
     * {@inheritdoc}
112
     */
113
    protected function getAuthUrl($state)
114
    {
115
        return $this->buildAuthUrlFromBase(
×
116
            'https://login.microsoftonline.com/'.($this->config['tenant'] ?: self::NU_TENANT_ID).'/oauth2/v2.0/authorize',
×
117
            $state
×
118
        );
×
119
    }
120

121
    protected function getTokenUrl()
122
    {
123
        return 'https://login.microsoftonline.com/'.($this->config['tenant'] ?: self::NU_TENANT_ID).'/oauth2/v2.0/token';
×
124
    }
125

126
    public function getLogoutUrl()
127
    {
128
        return 'https://login.microsoftonline.com/'.($this->config['tenant'] ?: self::NU_TENANT_ID).'/oauth2/v2.0/logout';
×
129
    }
130

131
    public function getAccessToken($code)
132
    {
133
        $response = $this->getHttpClient()->post($this->getTokenUrl(), [
×
134
            'form_params' => $this->getTokenFields($code),
×
135
        ]);
×
136

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

NEW
139
        return $this->parseAccessToken($this->credentialsResponseBody);
×
140
    }
141

142
    /**
143
     * {@inheritdoc}
144
     */
145
    protected function getUserByToken($token)
146
    {
147
        try {
148
            $response = $this->getHttpClient()->get($this->graphUrl, [
×
149
                'headers' => [
×
150
                    'Accept' => 'application/json',
×
NEW
151
                    'Authorization' => 'Bearer '.$token,
×
152
                ],
×
153
            ]);
×
154
        } catch (ClientException $e) {
×
155
            throw new MicrosoftGraphError($e);
×
156
        }
157

158
        return json_decode($response->getBody()->getContents(), true);
×
159
    }
160

161
    /**
162
     * {@inheritdoc}
163
     */
164
    protected function mapUserToObject(array $user)
165
    {
166
        return (new User())->setRaw($user)->map([
×
167
            'id' => $user['id'],
×
168
            'nickname' => null,
×
169
            'name' => $user['displayName'],
×
170
            'email' => $user['mail'],
×
171
            'avatar' => null,
×
172
        ]);
×
173
    }
174

175
    /**
176
     * {@inheritdoc}
177
     */
178
    protected function getTokenFields($code)
179
    {
180
        return array_merge(parent::getTokenFields($code), [
×
181
            'grant_type' => 'authorization_code',
×
182
            'scope' => $this->formatScopes($this->getScopes(), $this->scopeSeparator),
×
183
        ]);
×
184
    }
185

186
    /**
187
     * {@inheritDoc}
188
     */
189
    protected function getCodeFields($state = null)
190
    {
191
        $fields = [
×
NEW
192
            'client_id' => $this->clientId,
×
NEW
193
            'redirect_uri' => $this->redirectUrl,
×
NEW
194
            'scope' => $this->formatScopes($this->getScopes(), $this->scopeSeparator),
×
195
            'response_type' => 'id_token code',
×
196
            'response_mode' => 'form_post',
×
NEW
197
            'domain_hint' => $this->domainHint(),
×
198
        ];
×
199

200
        if ($this->usesState()) {
×
201
            $fields['state'] = $state;
×
202
            $fields['nonce'] = sprintf('%s.%s', Str::uuid(), $state);
×
203
        }
204

205
        return array_merge($fields, $this->parameters);
×
206
    }
207

208
    /**
209
     * {@inheritDoc}
210
     */
211
    protected function hasInvalidState()
212
    {
213
        if ($this->isStateless()) {
×
214
            return false;
×
215
        }
216

217
        $state = $this->request->session()->pull('state');
×
218

219
        return ! (strlen($state) > 0 && $this->request->input('state') === $state);
×
220
    }
221

222
    /**
223
     * Get the access token response for the given code.
224
     *
225
     * @param  string  $code
226
     * @return array
227
     */
228
    public function getAccessTokenResponse($code)
229
    {
230
        $response = $this->getHttpClient()->post($this->getTokenUrl(), [
×
231
            'form_params' => $this->getTokenFields($code),
×
232
        ]);
×
233

234
        return json_decode($response->getBody(), true);
×
235
    }
236

237
    /**
238
     * @return string|null URL that the user should be redirected to, if any.
239
     */
240
    protected function unpackState(string $state)
241
    {
242
        $parts = explode(self::STATE_PART_SEPARATOR, Crypt::decryptString($state));
×
243

244
        return $parts[1] ?? null;
×
245
    }
246

247
    /**
248
     * Preserves backwards-compatability now that the domain hint is configurable.
249
     */
250
    protected function domainHint(): ?string
251
    {
252
        if ($this->config['tenant'] === null) {
×
253
            return $this->config['domain_hint'] ?: self::NU_DOMAIN_HINT;
×
254
        }
255

256
        return $this->config['domain_hint'];
×
257
    }
258

259
    /**
260
     * Add the additional configuration key 'tenant' to enable the branded sign-in experience.
261
     *
262
     * @return array
263
     */
264
    public static function additionalConfigKeys()
265
    {
266
        return ['tenant', 'domain_hint'];
×
267
    }
268
}
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