Passed
Push — master ( 5ce155...c96346 )
by meta
02:37
created

AuthController::loadClientCertFromWebserver()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 12
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 6
nc 2
nop 0
dl 0
loc 12
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 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
        // TODO: Replace static value 1440 with actual life of the oauth access token we got
33
        \Cache::put($key, $user, 1440);
34
35
        return $user;
36
    }
37
38
    public function getMicrosoftGraphSelf($accessToken)
39
    {
40
        $graph = new \Microsoft\Graph\Graph();
41
        $graph->setAccessToken($accessToken);
42
        $user = $graph->createRequest('GET', '/me')
43
                      ->setReturnType(\Microsoft\Graph\Model\User::class)
44
                      ->execute();
45
46
        return $user->jsonSerialize();
47
    }
48
49
    public function scrubMicrosoftGraphUserData($userData)
50
    {
51
        // Fix any stupid crap with missing or null fields
52
        if (! isset($userData['mail']) || ! $userData['mail']) {
53
            $userData['mail'] = $userData['userPrincipalName'];
54
        }
55
56
        return $userData;
57
    }
58
59
    protected function findOrCreateUser($userData)
60
    {
61
        // Configurable \App\User type and ID field name
62
        $userType = config('enterpriseauth.user_class');
63
        $userIdField = config('enterpriseauth.user_id_field');
64
        // Try to find an existing user
65
        $user = $userType::where($userIdField, $userData['id'])->first();
66
        // If we dont have an existing user
67
        if (! $user) {
68
            // Go create a new one with this data
69
            $user = $this->createUserFromAzureData($userData);
70
        }
71
72
        return $user;
73
    }
74
75
    // This takes the azure userdata and makes a new user out of it
76
    public function createUserFromAzureData($userData)
77
    {
78
        // Config options for user type/id/field map
79
        $userType = config('enterpriseauth.user_class');
80
        $userFieldMap = config('enterpriseauth.user_map');
81
        $idField = config('enterpriseauth.user_id_field');
82
83
        // Should build new \App\User
84
        $user = new $userType();
85
        $user->$idField = $userData['id'];
86
        // Go through any other fields the config wants us to map
87
        foreach ($userFieldMap as $azureField => $userField) {
88
            $user->$userField = $userData[$azureField];
89
        }
90
        // Save our newly minted user
91
        $user->save();
92
93
        return $user;
94
    }
95
96
    public function certAuth()
97
    {
98
        // get the cert from the webserver and load it into an x509 phpseclib object
99
        $cert = $this->loadClientCertFromWebserver();
100
         // extract the UPN from the client cert
101
        $upn = $this->getUserPrincipalNameFromClientCert($cert);
102
        // get the user if it exists
103
        $user_class = config('enterpriseauth.user_class');
104
105
        // TODO: rewrite this so that if the user doesnt exist we create them and get their groups from AAD
106
        $user = $user_class::where('userPrincipalName', $upn)->first();
107
        if (! $user) {
108
            throw new \Exception('No user found with user principal name '.$upn);
109
        }
110
111
        return $user;
112
    }
113
114
    public function loadClientCertFromWebserver()
115
    {
116
        // Make sure we got a client certificate from the web server
117
        if (! $_SERVER['SSL_CLIENT_CERT']) {
118
            throw new \Exception('TLS client certificate missing');
119
        }
120
        // try to parse the certificate we got
121
        $x509 = new \phpseclib\File\X509();
122
        // NGINX screws up the cert by putting a bunch of tab characters into it so we need to clean those out
123
        $asciicert = str_replace("\t", '', $_SERVER['SSL_CLIENT_CERT']);
124
        $x509->loadX509($asciicert);
125
        return $x509;
126
    }
127
128
    public function getUserPrincipalNameFromClientCert($x509)
129
    {
130
        $names = $x509->getExtension('id-ce-subjectAltName');
131
        if (! $names) {
132
            throw new \Exception('TLS client cert missing subject alternative names');
133
        }
134
        // Search subject alt names for user principal name
135
        $upn = '';
136
        foreach ($names as $name) {
137
            foreach ($name as $key => $value) {
138
                if ($key == 'otherName') {
139
                    if (isset($value['type-id']) && $value['type-id'] == '1.3.6.1.4.1.311.20.2.3') {
140
                        $upn = $value['value']['utf8String'];
141
                    }
142
                }
143
            }
144
        }
145
        if (! $upn) {
146
            throw new \Exception('Could not find user principal name in TLS client cert');
147
        }
148
        return $upn;
149
    }
150
151
    public function updateGroups($user)
152
    {
153
        // See if we can get the users group membership data
154
        $groupData = $this->getMicrosoftGraphGroupMembership($user);
155
156
        // Process group data into a list of displayNames we use as roles
157
        $groups = [];
158
        foreach ($groupData as $info) {
159
            $groups[] = $info['displayName'];
160
        }
161
162
        // If we have user group information from this oauth attempt
163
        if (count($groups)) {
164
            // remove the users existing database roles before assigning new ones
165
            \DB::table('assigned_roles')
166
               ->where('entity_id', $user->id)
167
               ->where('entity_type', get_class($user))
168
               ->delete();
169
            // add the user to each group they are assigned
170
            $user->assign($groups);
171
        }
172
    }
173
174
    public function getMicrosoftGraphGroupMembership($user)
175
    {
176
        // Get an access token for our application (not the users token)
177
        $accessToken = $this->azureActiveDirectory->getApplicationAccessToken(config('enterpriseauth.credentials.client_id'), config('enterpriseauth.credentials.client_secret'));
178
179
        // Use the app access token to get a given users group membership
180
        $graph = new \Microsoft\Graph\Graph();
181
        $graph->setAccessToken($accessToken);
182
        $path = '/users/'.$user->userPrincipalName.'/memberOf';
183
        $groups = $graph->createRequest('GET', $path)
184
                        ->setReturnType(\Microsoft\Graph\Model\Group::class)
185
                        ->execute();
186
187
        // Convert the microsoft graph group objects into data that is useful
188
        $groupData = [];
189
        foreach ($groups as $group) {
190
            $groupData[] = $group->jsonSerialize();
191
        }
192
193
        return $groupData;
194
    }
195
}
196