Passed
Push — master ( 5c6f59...1ba333 )
by meta
04:04
created

ApiAuthController::identifyToken()   D

Complexity

Conditions 9
Paths 5

Size

Total Lines 25
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

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