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

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

12 Oct 2023 05:57PM UTC coverage: 45.913% (-0.2%) from 46.154%
6499210523

push

github

web-flow
Improve Entra ID Multi-tenant App Registration Support (#161)

* Make domain hint a config param

* Document multitenant config stuff

* Start the changelog

5 of 5 new or added lines in 1 file covered. (100.0%)

264 of 575 relevant lines covered (45.91%)

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

13

14
class NorthwesternAzureProvider extends AbstractProvider
15
{
16
    public const IDENTIFIER = 'NU_AZURE';
17
    public const NU_TENANT_ID = 'northwestern.edu';
18
    public const NU_DOMAIN_HINT = 'northwestern.edu';
19
    public const STATE_PART_SEPARATOR = '|';
20

21
    protected $encodingType = PHP_QUERY_RFC3986;
22

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

30
    /** @var string Default scopes to request */
31
    protected $scopes = ['openid'];
32
    protected $scopeSeparator = ' ';
33

34

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

52
        return Crypt::encryptString($state);
×
53
    }
54

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

65
        // Throws if the token isn't signed properly
66
        $idToken = AzureTokenVerifier::parseAndVerify($idTokenJwt);
×
67

68
        //Temporary fix to enable stateless
69
        $response = $this->getAccessTokenResponse($this->request->input('code'));
×
70

71
        $userToken = $this->getUserByToken(
×
72
            $token = Arr::get($response, 'access_token')
×
73
        );
×
74

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

81
            if ($this->hasInvalidState()) {
×
82
                throw new InvalidStateException();
×
83
            }
84

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

91
        $user = $this->mapUserToObject($userToken);
×
92

93
        if ($user instanceof User) {
×
94
            $user->setAccessTokenResponseBody($response);
×
95
        }
96

97
        return $user->setToken($token)
×
98
            ->setRefreshToken(Arr::get($response, 'refresh_token'))
×
99
            ->setExpiresIn(Arr::get($response, 'expires_in'));
×
100
    }
101

102
    /**
103
     * {@inheritdoc}
104
     */
105
    protected function getAuthUrl($state)
106
    {
107
        return $this->buildAuthUrlFromBase(
×
108
            'https://login.microsoftonline.com/'.($this->config['tenant'] ?: self::NU_TENANT_ID).'/oauth2/v2.0/authorize',
×
109
            $state
×
110
        );
×
111
    }
112

113
    protected function getTokenUrl()
114
    {
115
        return 'https://login.microsoftonline.com/'.($this->config['tenant'] ?: self::NU_TENANT_ID).'/oauth2/v2.0/token';
×
116
    }
117

118
    public function getLogoutUrl()
119
    {
120
        return 'https://login.microsoftonline.com/'.($this->config['tenant'] ?: self::NU_TENANT_ID).'/oauth2/v2.0/logout';
×
121
    }
122

123
    public function getAccessToken($code)
124
    {
125
        $response = $this->getHttpClient()->post($this->getTokenUrl(), [
×
126
            'form_params' => $this->getTokenFields($code),
×
127
        ]);
×
128

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

131
        return $this->parseAccessToken($response->getBody());
×
132
    }
133

134
    /**
135
     * {@inheritdoc}
136
     */
137
    protected function getUserByToken($token)
138
    {
139
        try {
140
            $response = $this->getHttpClient()->get($this->graphUrl, [
×
141
                'headers' => [
×
142
                    'Accept' => 'application/json',
×
143
                    'Authorization' => 'Bearer ' . $token,
×
144
                ],
×
145
            ]);
×
146
        } catch (ClientException $e) {
×
147
            throw new MicrosoftGraphError($e);
×
148
        }
149

150
        return json_decode($response->getBody()->getContents(), true);
×
151
    }
152

153
    /**
154
     * {@inheritdoc}
155
     */
156
    protected function mapUserToObject(array $user)
157
    {
158
        return (new User())->setRaw($user)->map([
×
159
            'id' => $user['id'],
×
160
            'nickname' => null,
×
161
            'name' => $user['displayName'],
×
162
            'email' => $user['mail'],
×
163
            'avatar' => null,
×
164
        ]);
×
165
    }
166

167
    /**
168
     * {@inheritdoc}
169
     */
170
    protected function getTokenFields($code)
171
    {
172
        return array_merge(parent::getTokenFields($code), [
×
173
            'grant_type' => 'authorization_code',
×
174
            'scope' => $this->formatScopes($this->getScopes(), $this->scopeSeparator),
×
175
        ]);
×
176
    }
177

178
    /**
179
     * {@inheritDoc}
180
     */
181
    protected function getCodeFields($state = null)
182
    {
183
        $fields = [
×
184
            'client_id'     => $this->clientId,
×
185
            'redirect_uri'  => $this->redirectUrl,
×
186
            'scope'         => $this->formatScopes($this->getScopes(), $this->scopeSeparator),
×
187
            'response_type' => 'id_token code',
×
188
            'response_mode' => 'form_post',
×
189
            'domain_hint'   => $this->domainHint(),
×
190
        ];
×
191

192
        if ($this->usesState()) {
×
193
            $fields['state'] = $state;
×
194
            $fields['nonce'] = sprintf('%s.%s', Str::uuid(), $state);
×
195
        }
196

197
        return array_merge($fields, $this->parameters);
×
198
    }
199

200
    /**
201
     * {@inheritDoc}
202
     */
203
    protected function hasInvalidState()
204
    {
205
        if ($this->isStateless()) {
×
206
            return false;
×
207
        }
208

209
        $state = $this->request->session()->pull('state');
×
210

211
        return ! (strlen($state) > 0 && $this->request->input('state') === $state);
×
212
    }
213

214
    /**
215
     * Get the access token response for the given code.
216
     *
217
     * @param  string  $code
218
     * @return array
219
     */
220
    public function getAccessTokenResponse($code)
221
    {
222
        $response = $this->getHttpClient()->post($this->getTokenUrl(), [
×
223
            'form_params' => $this->getTokenFields($code),
×
224
        ]);
×
225

226
        return json_decode($response->getBody(), true);
×
227
    }
228

229
    /**
230
     * @return string|null URL that the user should be redirected to, if any.
231
     */
232
    protected function unpackState(string $state)
233
    {
234
        $parts = explode(self::STATE_PART_SEPARATOR, Crypt::decryptString($state));
×
235

236
        return $parts[1] ?? null;
×
237
    }
238

239
    /**
240
     * Preserves backwards-compatability now that the domain hint is configurable.
241
     */
242
    protected function domainHint(): ?string
243
    {
244
        if ($this->config['tenant'] === null) {
×
245
            return $this->config['domain_hint'] ?: self::NU_DOMAIN_HINT;
×
246
        }
247

248
        return $this->config['domain_hint'];
×
249
    }
250

251
    /**
252
     * Add the additional configuration key 'tenant' to enable the branded sign-in experience.
253
     *
254
     * @return array
255
     */
256
    public static function additionalConfigKeys()
257
    {
258
        return ['tenant', 'domain_hint'];
×
259
    }
260
}
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