Passed
Push — master ( 71e000...2c8ddb )
by meta
03:26
created

ApiAuthController::unpackJwt()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 9
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 6
nc 1
nop 1
dl 0
loc 9
rs 9.6666
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
        // Check the cache to see if this is a previously authenticated oauth access token
25
        $key = '/oauth/tokens/'.$accessToken;
26
        if ($accessToken && \Cache::has($key)) {
27
            $user = \Cache::get($key);
28
        // Check to see if they have newly authenticated with an oauth access token
29
        } else {
30
            try {
31
                $user = $this->identifyAndValidateAccessToken($accessToken);
32
            } catch (\Exception $e) {
33
                \Illuminate\Support\Facades\Log::info('api auth token exception: '.$e->getMessage());
34
            }
35
        }
36
37
        return $user;
38
    }
39
40
    // This checks the kind of token and authenticates it appropriately
41
    public function identifyAndValidateAccessToken($accessToken)
42
    {
43
        // parse the token into readable info
44
        $token = $this->unpackJwt($accessToken);
45
        // identify the type of token
46
        $type = $this->identifyToken($token);
47
        // handle different types of tokens
48
        switch ($type) {
49
            case 'app':
50
                $auth = $this->validateOauthCreateOrUpdateAzureApp($accessToken);
51
                break;
52
            case 'user':
53
                $auth = $this->validateOauthCreateOrUpdateUserAndGroups($accessToken);
54
                break;
55
            default:
56
                throw new \Exception('Could not identify type of access token: '.json_encode($token));
57
        }
58
59
        return $auth;
60
    }
61
62
    // Try to unpack a jwt and get us the 3 chunks as assoc arrays so we can perform token identification
63
    public function unpackJwt($jwt)
64
    {
65
        list($headb64, $bodyb64, $cryptob64) = explode('.', $jwt);
66
        $token = [
67
            'header'    => json_decode(\Firebase\JWT\JWT::urlsafeB64Decode($headb64), true),
68
            'payload'   => json_decode(\Firebase\JWT\JWT::urlsafeB64Decode($bodyb64), true),
69
            'signature' => $cryptob64,
70
            ];
71
        return $token;
72
    }
73
74
    // figure out wtf kind of token we are being given
75
    public function identifyToken($token)
76
    {
77
        // start with an unidentified token type
78
        $type = 'unknown';
79
80
        // If the token payload contains name or preferred_username then its a user
81
        if (isset($token['payload']['name']) && isset($token['payload']['preferred_username'])) {
82
            $type = 'user';
83
        // ELSE If the token uses OUR app id as the AUDience then its an app... probablly...
84
        } else if (isset($token['payload']['aud']) && $token['payload']['aud'] == config('enterpriseauth.credentials.client_id')) {
85
            $type = 'app';
86
        }
87
88
        return $type;
89
    }
90
91
    // This is called after an api auth gets intercepted and determined to be an app access token
92
    public function validateOauthCreateOrUpdateAzureApp($accessToken)
93
    {
94
        // Perform the validation and get the payload
95
        $appData = $this->validateRSAToken($accessToken);
96
        // Upsert the Azure App object
97
        $app_id = $appData->azp;
98
        $app = \Metaclassing\EnterpriseAuth\Models\AzureApp::where('app_id', $app_id)->first();
99
        // If we dont have an existing app go create one
100
        if (! $app) {
101
            $app = \Metaclassing\EnterpriseAuth\Models\AzureApp::create();
102
            $app->name   = $app_id;
0 ignored issues
show
Bug Best Practice introduced by
The property name does not exist on Metaclassing\EnterpriseAuth\Models\AzureApp. Since you implemented __set, consider adding a @property annotation.
Loading history...
103
            $app->app_id = $app_id;
0 ignored issues
show
Bug Best Practice introduced by
The property app_id does not exist on Metaclassing\EnterpriseAuth\Models\AzureApp. Since you implemented __set, consider adding a @property annotation.
Loading history...
104
            $app->save();
105
        }
106
107
        return $app;
108
    }
109
110
    // this checks the app token, validates it, returns decoded signed data
111
    public function validateRSAToken($accessToken)
112
    {
113
        // Unpack our jwt to verify it is correctly formed
114
        $token = $this->unpackJwt($accessToken);
115
        // app tokens must be signed in RSA
116
        if (! isset($token['header']['alg']) || $token['header']['alg'] != 'RS256') {
117
            throw new \Exception('Token is not using the correct signing algorithm RS256 '.$accessToken);
118
        }
119
        // app tokens are RSA signed with a key ID in the header of the token
120
        if (! isset($token['header']['kid'])) {
121
            throw new \Exception('Token with unknown RSA key id can not be validated '.$accessToken);
122
        }
123
        // Make sure the key id is known to our azure ad information
124
        $kid = $token['header']['kid'];
125
        if (! isset($this->azureActiveDirectory->signingKeys[$kid])) {
126
            throw new \Exception('Token signed with unknown KID '.$kid);
127
        }
128
        // get the x509 encoded cert body
129
        $x5c = $this->azureActiveDirectory->signingKeys[$kid]['x5c'];
130
        // if this is an array use the first entry
131
        if (is_array($x5c)) {
132
            $x5c = reset($x5c);
133
        }
134
        // Get the X509 certificate for the selected key id
135
        $certificate = '-----BEGIN CERTIFICATE-----'.PHP_EOL
136
                     . $x5c . PHP_EOL
137
                     . '-----END CERTIFICATE-----';
138
        // Perform the verification and get the verified payload results
139
        $payload = \Firebase\JWT\JWT::decode($accessToken, $certificate, array('RS256'));
140
141
        return $payload;
142
    }
143
144
    public function attemptCertAuth()
145
    {
146
        try {
147
            return $apiAuthController->certAuth();
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $apiAuthController seems to be never defined.
Loading history...
148
        } catch (\Exception $e) {
149
            \Illuminate\Support\Facades\Log::info('api auth cert exception: '.$e->getMessage());
150
        }
151
    }
152
153
    // Helper to find a token wherever it is hidden and attempt to auth it
154
    public function extractOauthAccessTokenFromRequest(\Illuminate\Http\Request $request)
155
    {
156
        $oauthAccessToken = '';
157
158
        // IF we get an explicit TOKEN=abc123 in the $request
159
        if ($request->query('token')) {
160
            $oauthAccessToken = $request->query('token');
161
        }
162
163
        // IF posted as access_token=abc123 in the $request
164
        if ($request->input('access_token')) {
165
            $oauthAccessToken = $request->input('access_token');
166
        }
167
168
        // IF the request has an Authorization: Bearer abc123 header
169
        $header = $request->headers->get('authorization');
170
        $regex = '/bearer\s+(\S+)/i';
171
        if ($header && preg_match($regex, $header, $matches)) {
172
            $oauthAccessToken = $matches[1];
173
        }
174
175
        return $oauthAccessToken;
176
    }
177
178
    // Route to dump out the authenticated API user
179
    public function getAuthorizedUserInfo(\Illuminate\Http\Request $request)
1 ignored issue
show
Unused Code introduced by
The parameter $request is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

179
    public function getAuthorizedUserInfo(/** @scrutinizer ignore-unused */ \Illuminate\Http\Request $request)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
180
    {
181
        $user = auth()->user();
182
183
        return response()->json($user);
184
    }
185
186
    // Route to dump out the authenticated users groups/roles
187
    public function getAuthorizedUserRoles(\Illuminate\Http\Request $request)
1 ignored issue
show
Unused Code introduced by
The parameter $request is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

187
    public function getAuthorizedUserRoles(/** @scrutinizer ignore-unused */ \Illuminate\Http\Request $request)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
188
    {
189
        $user = auth()->user();
190
        $roles = $user->roles()->get();
191
192
        return response()->json($roles);
193
    }
194
195
    // Route to dump out the authenticated users group/roles abilities/permissions
196
    public function getAuthorizedUserRolesAbilities(\Illuminate\Http\Request $request)
1 ignored issue
show
Unused Code introduced by
The parameter $request is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

196
    public function getAuthorizedUserRolesAbilities(/** @scrutinizer ignore-unused */ \Illuminate\Http\Request $request)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
197
    {
198
        $user = auth()->user();
199
        $roles = $user->roles()->get()->all();
200
        foreach ($roles as $key => $role) {
201
            $role->permissions = $role->abilities()->get()->all();
202
            if (! count($role->permissions)) {
203
                unset($roles[$key]);
204
            }
205
        }
206
        $roles = array_values($roles);
207
208
        return response()->json($roles);
209
    }
210
}
211