Passed
Push — master ( c96346...8ec09b )
by meta
05:18 queued 02:31
created

AuthController::getMicrosoftGraphSelf()   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
        // 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
126
        return $x509;
127
    }
128
129
    public function getUserPrincipalNameFromClientCert($x509)
130
    {
131
        $names = $x509->getExtension('id-ce-subjectAltName');
132
        if (! $names) {
133
            throw new \Exception('TLS client cert missing subject alternative names');
134
        }
135
        // Search subject alt names for user principal name
136
        $upn = '';
137
        foreach ($names as $name) {
138
            foreach ($name as $key => $value) {
139
                if ($key == 'otherName') {
140
                    if (isset($value['type-id']) && $value['type-id'] == '1.3.6.1.4.1.311.20.2.3') {
141
                        $upn = $value['value']['utf8String'];
142
                    }
143
                }
144
            }
145
        }
146
        if (! $upn) {
147
            throw new \Exception('Could not find user principal name in TLS client cert');
148
        }
149
150
        return $upn;
151
    }
152
153
    public function updateGroups($user)
154
    {
155
        // See if we can get the users group membership data
156
        $groupData = $this->getMicrosoftGraphGroupMembership($user);
157
158
        // Process group data into a list of displayNames we use as roles
159
        $groups = [];
160
        foreach ($groupData as $info) {
161
            $groups[] = $info['displayName'];
162
        }
163
164
        // If we have user group information from this oauth attempt
165
        if (count($groups)) {
166
            // remove the users existing database roles before assigning new ones
167
            \DB::table('assigned_roles')
168
               ->where('entity_id', $user->id)
169
               ->where('entity_type', get_class($user))
170
               ->delete();
171
            // add the user to each group they are assigned
172
            $user->assign($groups);
173
        }
174
    }
175
176
    public function getMicrosoftGraphGroupMembership($user)
177
    {
178
        // Get an access token for our application (not the users token)
179
        $accessToken = $this->azureActiveDirectory->getApplicationAccessToken(config('enterpriseauth.credentials.client_id'), config('enterpriseauth.credentials.client_secret'));
180
181
        // Use the app access token to get a given users group membership
182
        $graph = new \Microsoft\Graph\Graph();
183
        $graph->setAccessToken($accessToken);
184
        $path = '/users/'.$user->userPrincipalName.'/memberOf';
185
        $groups = $graph->createRequest('GET', $path)
186
                        ->setReturnType(\Microsoft\Graph\Model\Group::class)
187
                        ->execute();
188
189
        // Convert the microsoft graph group objects into data that is useful
190
        $groupData = [];
191
        foreach ($groups as $group) {
192
            $groupData[] = $group->jsonSerialize();
193
        }
194
195
        return $groupData;
196
    }
197
}
198