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

AuthController::getTokenMinutesRemaining()   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 AuthController extends Controller
9
{
10
    protected $azureActiveDirectory;
11
12
    public function __construct()
13
    {
14
        $tenant = config('enterpriseauth.credentials.tenant');
15
        $this->azureActiveDirectory = new \Metaclassing\EnterpriseAuth\AzureActiveDirectory($tenant);
16
    }
17
18
    // This is called after a web auth gets an access token, or api auth sends an access token
19
    public function validateOauthCreateOrUpdateUserAndGroups($accessToken)
20
    {
21
        $userData = $this->getMicrosoftGraphSelf($accessToken);
22
        $userData = $this->scrubMicrosoftGraphUserData($userData);
23
24
        // This is a laravel \App\User
25
        $user = $this->findOrCreateUser($userData);
26
27
        // Try to update the group/role membership for this user
28
        $this->updateGroups($user);
29
30
        // Cache the users oauth accss token mapped to their user object for stuff and things
31
        $key = '/oauth/tokens/'.$accessToken;
32
        $remaining = $this->getTokenMinutesRemaining($accessToken);
33
        \Illuminate\Support\Facades\Log::debug('oauth token cached for '.$remaining.' minutes');
34
        // Cache the token until it expires
35
        \Cache::put($key, $user, $remaining);
36
37
        return $user;
38
    }
39
40
    public function getMicrosoftGraphSelf($accessToken)
41
    {
42
        $graph = new \Microsoft\Graph\Graph();
43
        $graph->setAccessToken($accessToken);
44
        $user = $graph->createRequest('GET', '/me')
45
                      ->setReturnType(\Microsoft\Graph\Model\User::class)
46
                      ->execute();
47
48
        return $user->jsonSerialize();
49
    }
50
51
    public function scrubMicrosoftGraphUserData($userData)
52
    {
53
        // Fix any stupid crap with missing or null fields
54
        if (! isset($userData['mail']) || ! $userData['mail']) {
55
            \Illuminate\Support\Facades\Log::debug('graph api did not contain mail field, using userPrincipalName instead '.json_encode($userData));
56
            $userData['mail'] = $userData['userPrincipalName'];
57
        }
58
59
        return $userData;
60
    }
61
62
    protected function findOrCreateUser($userData)
63
    {
64
        // Configurable \App\User type and ID field name
65
        $userType = config('enterpriseauth.user_class');
66
        $userIdField = config('enterpriseauth.user_id_field');
67
        // Try to find an existing user
68
        $user = $userType::where($userIdField, $userData['id'])->first();
69
        // If we dont have an existing user
70
        if (! $user) {
71
            // Go create a new one with this data
72
            $user = $this->createUserFromAzureData($userData);
73
        }
74
75
        return $user;
76
    }
77
78
    // This takes the azure userdata and makes a new user out of it
79
    public function createUserFromAzureData($userData)
80
    {
81
        // Config options for user type/id/field map
82
        $userType = config('enterpriseauth.user_class');
83
        $userFieldMap = config('enterpriseauth.user_map');
84
        $idField = config('enterpriseauth.user_id_field');
85
86
        // Should build new \App\User
87
        $user = new $userType();
88
        $user->$idField = $userData['id'];
89
        // Go through any other fields the config wants us to map
90
        foreach ($userFieldMap as $azureField => $userField) {
91
            if (isset($userData[$azureField])) {
92
                $user->$userField = $userData[$azureField];
93
            } else {
94
                \Illuminate\Support\Facades\Log::info('createUserFromAzureData did not contain configured field '.$azureField.' in '.json_encode($userData));
95
            }
96
        }
97
        // Save our newly minted user
98
        $user->save();
99
100
        return $user;
101
    }
102
103
    public function certAuth()
104
    {
105
        // get the cert from the webserver and load it into an x509 phpseclib object
106
        $cert = $this->loadClientCertFromWebserver();
107
        // extract the UPN from the client cert
108
        $upn = $this->getUserPrincipalNameFromClientCert($cert);
109
        // get the user if it exists
110
        $user_class = config('enterpriseauth.user_class');
111
112
        // TODO: rewrite this so that if the user doesnt exist we create them and get their groups from AAD
113
        $user = $user_class::where('userPrincipalName', $upn)->first();
114
        if (! $user) {
115
            throw new \Exception('No user found with user principal name '.$upn);
116
        }
117
118
        return $user;
119
    }
120
121
    public function loadClientCertFromWebserver()
122
    {
123
        // Make sure we got a client certificate from the web server
124
        if (! isset($_SERVER['SSL_CLIENT_CERT']) || ! $_SERVER['SSL_CLIENT_CERT']) {
125
            throw new \Exception('TLS client certificate missing');
126
        }
127
        // try to parse the certificate we got
128
        $x509 = new \phpseclib\File\X509();
129
        // NGINX screws up the cert by putting a bunch of tab characters into it so we need to clean those out
130
        $asciicert = str_replace("\t", '', $_SERVER['SSL_CLIENT_CERT']);
131
        $x509->loadX509($asciicert);
132
133
        return $x509;
134
    }
135
136
    public function getUserPrincipalNameFromClientCert($x509)
137
    {
138
        $names = $x509->getExtension('id-ce-subjectAltName');
139
        if (! $names) {
140
            throw new \Exception('TLS client cert missing subject alternative names');
141
        }
142
        // Search subject alt names for user principal name
143
        $upn = '';
144
        foreach ($names as $name) {
145
            foreach ($name as $key => $value) {
146
                if ($key == 'otherName') {
147
                    if (isset($value['type-id']) && $value['type-id'] == '1.3.6.1.4.1.311.20.2.3') {
148
                        $upn = $value['value']['utf8String'];
149
                    }
150
                }
151
            }
152
        }
153
        if (! $upn) {
154
            throw new \Exception('Could not find user principal name in TLS client cert');
155
        }
156
157
        return $upn;
158
    }
159
160
    public function updateGroups($user)
161
    {
162
        // See if we can get the users group membership data
163
        $groupData = $this->getMicrosoftGraphGroupMembership($user);
164
165
        // Process group data into a list of displayNames we use as roles
166
        $groups = [];
167
        foreach ($groupData as $info) {
168
            $groups[] = $info['displayName'];
169
        }
170
171
        // If we have user group information from this oauth attempt
172
        if (count($groups)) {
173
            // remove the users existing database roles before assigning new ones
174
            \DB::table('assigned_roles')
175
               ->where('entity_id', $user->id)
176
               ->where('entity_type', get_class($user))
177
               ->delete();
178
            // add the user to each group they are assigned
179
            $user->assign($groups);
180
        }
181
    }
182
183
    public function getMicrosoftGraphGroupMembership($user)
184
    {
185
        // Get an access token for our application (not the users token)
186
        $accessToken = $this->azureActiveDirectory->getApplicationAccessToken(config('enterpriseauth.credentials.client_id'), config('enterpriseauth.credentials.client_secret'));
187
188
        // Use the app access token to get a given users group membership
189
        $graph = new \Microsoft\Graph\Graph();
190
        $graph->setAccessToken($accessToken);
191
        $path = '/users/'.$user->userPrincipalName.'/memberOf';
192
        $groups = $graph->createRequest('GET', $path)
193
                        ->setReturnType(\Microsoft\Graph\Model\Group::class)
194
                        ->execute();
195
196
        // Convert the microsoft graph group objects into data that is useful
197
        $groupData = [];
198
        foreach ($groups as $group) {
199
            $groupData[] = $group->jsonSerialize();
200
        }
201
202
        return $groupData;
203
    }
204
205
    // Try to unpack a jwt and get us the 3 chunks as assoc arrays so we can perform token identification
206
    public function unpackJwt($jwt)
207
    {
208
        list($headb64, $bodyb64, $cryptob64) = explode('.', $jwt);
209
        $token = [
210
            'header'    => json_decode(\Firebase\JWT\JWT::urlsafeB64Decode($headb64), true),
211
            'payload'   => json_decode(\Firebase\JWT\JWT::urlsafeB64Decode($bodyb64), true),
212
            'signature' => $cryptob64,
213
            ];
214
215
        return $token;
216
    }
217
218
    // calculate the delta between $jwt['exp'] and time() / 60 for minutes remaining
219
    protected function getTokenMinutesRemaining($accessToken)
220
    {
221
        $tokenData = $this->unpackJwt($accessToken);
222
        $now = time();
223
        $expires = $tokenData['payload']['exp'];
224
        $remainingSecs = $expires - $now;
225
        // round up to the nearest minute
226
        $remainingMins = ceil($remainingSecs / 60);
227
        return $remainingMins;
228
    }
229
}
230