Passed
Push — master ( 46b679...20eab7 )
by meta
02:42
created

identifyAndValidateAccessToken()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 20
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 13
nc 3
nop 1
dl 0
loc 20
rs 9.4285
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 'app':
53
                $user = $this->validateOauthCreateOrUpdateAzureApp($accessToken);
54
                break;
55
            case 'user':
56
                $user = $this->validateOauthCreateOrUpdateUserAndGroups($accessToken);
57
                break;
58
            default:
59
                throw new \Exception('Could not identify type of access token: '.json_encode($token));
60
        }
61
62
        return $user;
63
    }
64
65
    // figure out wtf kind of token we are being given
66
    public function identifyToken($token)
67
    {
68
        // start with an unidentified token type
69
        $type = 'unknown';
70
71
        // If the token payload contains name or preferred_username then its a user
72
        if (isset($token['payload']['name']) && isset($token['payload']['upn'])) {
73
            $type = 'user';
74
        // ELSE If the token uses OUR app id as the AUDience then its an app... probablly...
75
        } elseif (isset($token['payload']['aud']) && $token['payload']['aud'] == config('enterpriseauth.credentials.client_id')) {
76
            $type = 'app';
77
        }
78
79
        return $type;
80
    }
81
82
    // This is called after an api auth gets intercepted and determined to be an app access token
83
    public function validateOauthCreateOrUpdateAzureApp($accessToken)
84
    {
85
        // Perform the validation and get the payload
86
        $appData = $this->validateRSAToken($accessToken);
87
        // Find or create for azure app user object
88
        $userData = [
89
                'id'                => $appData->azp,
90
                'displayName'       => $appData->azp,
91
                'mail'              => $appData->azp,
92
            ];
93
94
        // This is a laravel \App\User
95
        $user = $this->findOrCreateUser($userData);
96
97
        // Cache the users oauth accss token mapped to their user object for stuff and things
98
        $key = '/oauth/tokens/'.$accessToken;
99
        $remaining = $this->getTokenMinutesRemaining($accessToken);
100
        \Illuminate\Support\Facades\Log::debug('api auth token cached for '.$remaining.' minutes');
101
        // Cache the token until it expires
102
        \Cache::put($key, $user, $remaining);
103
104
        return $user;
105
    }
106
107
    // this checks the app token, validates it, returns decoded signed data
108
    public function validateRSAToken($accessToken)
109
    {
110
        // Unpack our jwt to verify it is correctly formed
111
        $token = $this->unpackJwt($accessToken);
112
        // app tokens must be signed in RSA
113
        if (! isset($token['header']['alg']) || $token['header']['alg'] != 'RS256') {
114
            throw new \Exception('Token is not using the correct signing algorithm RS256 '.$accessToken);
115
        }
116
        // app tokens are RSA signed with a key ID in the header of the token
117
        if (! isset($token['header']['kid'])) {
118
            throw new \Exception('Token with unknown RSA key id can not be validated '.$accessToken);
119
        }
120
        // Make sure the key id is known to our azure ad information
121
        $kid = $token['header']['kid'];
122
        if (! isset($this->azureActiveDirectory->signingKeys[$kid])) {
123
            throw new \Exception('Token signed with unknown KID '.$kid);
124
        }
125
        // get the x509 encoded cert body
126
        $x5c = $this->azureActiveDirectory->signingKeys[$kid]['x5c'];
127
        // if this is an array use the first entry
128
        if (is_array($x5c)) {
129
            $x5c = reset($x5c);
130
        }
131
        // Get the X509 certificate for the selected key id
132
        $certificate = '-----BEGIN CERTIFICATE-----'.PHP_EOL
133
                     .$x5c.PHP_EOL
134
                     .'-----END CERTIFICATE-----';
135
        // Perform the verification and get the verified payload results
136
        $payload = \Firebase\JWT\JWT::decode($accessToken, $certificate, ['RS256']);
137
138
        return $payload;
139
    }
140
141
    public function attemptCertAuth()
142
    {
143
        try {
144
            return $this->certAuth();
145
        } catch (\Exception $e) {
146
            \Illuminate\Support\Facades\Log::info('api auth cert exception: '.$e->getMessage());
147
        }
148
    }
149
150
    // Helper to find a token wherever it is hidden and attempt to auth it
151
    public function extractOauthAccessTokenFromRequest(\Illuminate\Http\Request $request)
152
    {
153
        $oauthAccessToken = '';
154
155
        // IF we get an explicit TOKEN=abc123 in the $request
156
        if ($request->query('token')) {
157
            $oauthAccessToken = $request->query('token');
158
        }
159
160
        // IF posted as access_token=abc123 in the $request
161
        if ($request->input('access_token')) {
162
            $oauthAccessToken = $request->input('access_token');
163
        }
164
165
        // IF the request has an Authorization: Bearer abc123 header
166
        $header = $request->headers->get('authorization');
167
        $regex = '/bearer\s+(\S+)/i';
168
        if ($header && preg_match($regex, $header, $matches)) {
169
            $oauthAccessToken = $matches[1];
170
        }
171
172
        return $oauthAccessToken;
173
    }
174
175
    // Route to dump out the authenticated API user
176
    public function getAuthorizedUserInfo(\Illuminate\Http\Request $request)
177
    {
178
        $user = auth()->user();
179
180
        return response()->json($user);
181
    }
182
183
    // Route to dump out the authenticated users groups/roles
184
    public function getAuthorizedUserRoles(\Illuminate\Http\Request $request)
185
    {
186
        $user = auth()->user();
187
        $roles = $user->roles()->get();
188
189
        return response()->json($roles);
190
    }
191
192
    // Route to dump out the authenticated users group/roles abilities/permissions
193
    public function getAuthorizedUserRolesAbilities(\Illuminate\Http\Request $request)
194
    {
195
        $user = auth()->user();
196
        $roles = $user->roles()->get()->all();
197
        foreach ($roles as $key => $role) {
198
            $role->permissions = $role->abilities()->get()->all();
199
            if (! count($role->permissions)) {
200
                unset($roles[$key]);
201
            }
202
        }
203
        $roles = array_values($roles);
204
205
        return response()->json($roles);
206
    }
207
}
208