validateOauthCreateOrUpdateAzureUser()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 26
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 13
nc 1
nop 1
dl 0
loc 26
rs 9.8333
c 0
b 0
f 0
1
<?php
2
3
namespace Metaclassing\EnterpriseAuth\Controllers;
4
5
use Illuminate\Routing\Controller;
6
7
class ApiAuthController extends AuthController
8
{
9
    public function authenticateRequest(\Illuminate\Http\Request $request)
10
    {
11
        $accessToken = $this->extractOauthAccessTokenFromRequest($request);
12
13
        // IF we got a token, prefer using that over cert auth
14
        if ($accessToken) {
15
            return $this->attemptTokenAuth($accessToken);
16
        } else {
17
            return $this->attemptCertAuth();
18
        }
19
    }
20
21
    public function attemptTokenAuth($accessToken)
22
    {
23
        $user = null;
24
25
        // Check the cache to see if this is a previously authenticated oauth access token
26
        $key = '/oauth/tokens/'.$accessToken;
27
        if ($accessToken && \Cache::has($key)) {
28
            $user = \Cache::get($key);
29
        // Check to see if they have newly authenticated with an oauth access token
30
        } else {
31
            try {
32
                $user = $this->identifyAndValidateAccessToken($accessToken);
33
            } catch (\Exception $e) {
34
                \Illuminate\Support\Facades\Log::info('api auth token exception: '.$e->getMessage());
35
            }
36
        }
37
38
        return $user;
39
    }
40
41
    // This checks the kind of token and authenticates it appropriately
42
    public function identifyAndValidateAccessToken($accessToken)
43
    {
44
        // parse the token into readable info
45
        $token = $this->unpackJwt($accessToken);
46
        // identify the type of token
47
        $type = $this->identifyToken($token);
48
        // handle different types of tokens
49
        \Illuminate\Support\Facades\Log::debug('api auth identified token type '.$type);
50
        switch ($type) {
51
            case 'azureapp':
52
                $user = $this->validateOauthCreateOrUpdateAzureApp($accessToken);
53
                break;
54
            case 'azureuser':
55
                $user = $this->validateOauthCreateOrUpdateAzureUser($accessToken);
56
                break;
57
            case 'graphuser':
58
                $user = $this->validateOauthCreateOrUpdateUserAndGroups($accessToken);
59
                break;
60
            default:
61
                throw new \Exception('Could not identify type of access token: '.json_encode($token));
62
        }
63
64
        return $user;
65
    }
66
67
    // figure out wtf kind of token we are being given
68
    public function identifyToken($token)
69
    {
70
        // start with an unidentified token type
71
        $type = 'unknown';
72
73
        // If the token is for the graph API
74
        if (isset($token['payload']['aud']) && $token['payload']['aud'] == 'https://graph.microsoft.com') {
75
            // and its a user
76
            if (isset($token['payload']['name']) && isset($token['payload']['upn'])) {
77
                $type = 'graphuser';
78
            } else {
79
                // This is entirely possible but I have no idea how to decode or validate it...
80
            }
81
            // If the token uses OUR app id as the AUDience then we dont need the graph api
82
        } elseif (isset($token['payload']['aud']) && $token['payload']['aud'] == config('enterpriseauth.credentials.client_id')) {
83
            // users have names
84
            if (isset($token['payload']['name']) && isset($token['payload']['preferred_username'])) {
85
                $type = 'azureuser';
86
            // apps do not?
87
            } else {
88
                $type = 'azureapp';
89
            }
90
        }
91
92
        return $type;
93
    }
94
95
    // This is called after an api auth gets intercepted and determined to be an app access token
96
    public function validateOauthCreateOrUpdateAzureApp($accessToken)
97
    {
98
        // Perform the validation and get the payload
99
        $appData = $this->validateRSAToken($accessToken);
100
        // Determine what property to use or explode
101
        $prop = '';
102
        if (property_exists($appData, 'oid')) {
103
            $prop = 'oid';
104
        } elseif (property_exists($appData, 'azp')) {
105
            $prop = 'azp';
106
        } else {
107
            throw new \Exception('Token data was valid but did not contain a known property to use for subject/caller');
108
        }
109
        // Find or create for azure app user object
110
        $userData = [
111
            'id'                => $appData->$prop,
112
            'displayName'       => $appData->$prop,
113
            'mail'              => $appData->$prop,
114
        ];
115
116
        // This is a laravel \App\User
117
        $user = $this->findOrCreateUser($userData);
118
119
        // Cache the users oauth accss token mapped to their user object for stuff and things
120
        $key = '/oauth/tokens/'.$accessToken;
121
        $remaining = $this->getTokenMinutesRemaining($accessToken);
122
        \Illuminate\Support\Facades\Log::debug('api auth token cached for '.$remaining.' minutes');
123
        // Cache the token until it expires
124
        \Cache::put($key, $user, $remaining);
125
126
        return $user;
127
    }
128
129
    // This is called after an api auth gets intercepted and determined to be an app access token
130
    public function validateOauthCreateOrUpdateAzureUser($accessToken)
131
    {
132
        // Perform the validation and get the payload
133
        $appData = $this->validateRSAToken($accessToken);
134
        // Find or create for azure app user object
135
        $userData = [
136
            'id'                => $appData->oid,
137
            'displayName'       => $appData->name,
138
            'mail'              => $appData->preferred_username,
139
            'userPrincipalName' => $appData->preferred_username,
140
        ];
141
142
        // This is a laravel \App\User
143
        $user = $this->findOrCreateUser($userData);
144
145
        // Try to update the group/role membership for this user
146
        $this->updateGroups($user);
147
148
        // Cache the users oauth accss token mapped to their user object for stuff and things
149
        $key = '/oauth/tokens/'.$accessToken;
150
        $remaining = $this->getTokenMinutesRemaining($accessToken);
151
        \Illuminate\Support\Facades\Log::debug('api auth token cached for '.$remaining.' minutes');
152
        // Cache the token until it expires
153
        \Cache::put($key, $user, $remaining);
154
155
        return $user;
156
    }
157
158
    // this checks the app token, validates it, returns decoded signed data
159
    public function validateRSAToken($accessToken)
160
    {
161
        // Unpack our jwt to verify it is correctly formed
162
        $token = $this->unpackJwt($accessToken);
163
        // app tokens must be signed in RSA
164
        if (! isset($token['header']['alg']) || $token['header']['alg'] != 'RS256') {
165
            throw new \Exception('Token is not using the correct signing algorithm RS256 '.$accessToken);
166
        }
167
        // app tokens are RSA signed with a key ID in the header of the token
168
        if (! isset($token['header']['kid'])) {
169
            throw new \Exception('Token with unknown RSA key id can not be validated '.$accessToken);
170
        }
171
        // Make sure the key id is known to our azure ad information
172
        $kid = $token['header']['kid'];
173
        if (! isset($this->azureActiveDirectory->signingKeys[$kid])) {
174
            throw new \Exception('Token signed with unknown KID '.$kid);
175
        }
176
        // get the x509 encoded cert body
177
        $x5c = $this->azureActiveDirectory->signingKeys[$kid]['x5c'];
178
        // if this is an array use the first entry
179
        if (is_array($x5c)) {
180
            $x5c = reset($x5c);
181
        }
182
        // Get the X509 certificate for the selected key id
183
        $certificate = '-----BEGIN CERTIFICATE-----'.PHP_EOL
184
                     .$x5c.PHP_EOL
185
                     .'-----END CERTIFICATE-----';
186
        // Perform the verification and get the verified payload results
187
        $payload = \Firebase\JWT\JWT::decode($accessToken, $certificate, ['RS256']);
188
189
        return $payload;
190
    }
191
192
    public function attemptCertAuth()
193
    {
194
        try {
195
            return $this->certAuth();
196
        } catch (\Exception $e) {
197
            \Illuminate\Support\Facades\Log::info('api auth cert exception: '.$e->getMessage());
198
        }
199
    }
200
201
    // Helper to find a token wherever it is hidden and attempt to auth it
202
    public function extractOauthAccessTokenFromRequest(\Illuminate\Http\Request $request)
203
    {
204
        $oauthAccessToken = '';
205
206
        // IF we get an explicit TOKEN=abc123 in the $request
207
        if ($request->query('token')) {
208
            $oauthAccessToken = $request->query('token');
209
        }
210
211
        // IF posted as access_token=abc123 in the $request
212
        if ($request->input('access_token')) {
213
            $oauthAccessToken = $request->input('access_token');
214
        }
215
216
        // IF the request has an Authorization: Bearer abc123 header
217
        $header = $request->headers->get('authorization');
218
        $regex = '/bearer\s+(\S+)/i';
219
        if ($header && preg_match($regex, $header, $matches)) {
220
            $oauthAccessToken = $matches[1];
221
        }
222
223
        return $oauthAccessToken;
224
    }
225
226
    // Route to dump out the authenticated API user
227
    public function getAuthorizedUserInfo(\Illuminate\Http\Request $request)
228
    {
229
        $user = auth()->user();
230
231
        return response()->json($user);
232
    }
233
234
    // Route to dump out the authenticated users groups/roles
235
    public function getAuthorizedUserRoles(\Illuminate\Http\Request $request)
236
    {
237
        $user = auth()->user();
238
        $roles = $user->roles()->get();
239
240
        return response()->json($roles);
241
    }
242
243
    // Route to dump out the authenticated users group/roles abilities/permissions
244
    public function getAuthorizedUserRolesAbilities(\Illuminate\Http\Request $request)
245
    {
246
        $user = auth()->user();
247
        $roles = $user->roles()->get()->all();
248
        foreach ($roles as $key => $role) {
249
            $role->permissions = $role->abilities()->get()->all();
250
            if (! count($role->permissions)) {
251
                unset($roles[$key]);
252
            }
253
        }
254
        $roles = array_values($roles);
255
256
        return response()->json($roles);
257
    }
258
}
259