metaclassing /
PHP7-Laravel5-EnterpriseAuth
| 1 | <?php |
||||
| 2 | |||||
| 3 | namespace Metaclassing\EnterpriseAuth\Controllers; |
||||
| 4 | |||||
| 5 | use Illuminate\Routing\Controller; |
||||
| 6 | |||||
| 7 | class AuthController extends Controller |
||||
| 8 | { |
||||
| 9 | protected $azureActiveDirectory; |
||||
| 10 | |||||
| 11 | public function __construct() |
||||
| 12 | { |
||||
| 13 | $tenant = config('enterpriseauth.credentials.tenant'); |
||||
| 14 | $this->azureActiveDirectory = new \Metaclassing\EnterpriseAuth\AzureActiveDirectory($tenant); |
||||
| 15 | } |
||||
| 16 | |||||
| 17 | // This is called after a web auth gets an access token, or api auth sends an access token |
||||
| 18 | public function validateOauthCreateOrUpdateUserAndGroups($accessToken) |
||||
| 19 | { |
||||
| 20 | $userData = $this->getMicrosoftGraphSelf($accessToken); |
||||
| 21 | $userData = $this->scrubMicrosoftGraphUserData($userData); |
||||
| 22 | |||||
| 23 | // This is a laravel \App\User |
||||
| 24 | $user = $this->findOrCreateUser($userData); |
||||
| 25 | |||||
| 26 | // Try to update the group/role membership for this user |
||||
| 27 | $this->updateGroups($user); |
||||
| 28 | |||||
| 29 | // Cache the users oauth accss token mapped to their user object for stuff and things |
||||
| 30 | $key = '/oauth/tokens/'.$accessToken; |
||||
| 31 | $remaining = $this->getTokenMinutesRemaining($accessToken); |
||||
| 32 | \Illuminate\Support\Facades\Log::debug('oauth token cached for '.$remaining.' minutes'); |
||||
| 33 | // Cache the token until it expires |
||||
| 34 | \Cache::put($key, $user, $remaining); |
||||
| 35 | |||||
| 36 | return $user; |
||||
| 37 | } |
||||
| 38 | |||||
| 39 | public function getMicrosoftGraphSelf($accessToken) |
||||
| 40 | { |
||||
| 41 | $graph = new \Microsoft\Graph\Graph(); |
||||
| 42 | $graph->setAccessToken($accessToken); |
||||
| 43 | $user = $graph->createRequest('GET', '/me') |
||||
| 44 | ->setReturnType(\Microsoft\Graph\Model\User::class) |
||||
| 45 | ->execute(); |
||||
| 46 | |||||
| 47 | return $user->jsonSerialize(); |
||||
|
0 ignored issues
–
show
The method
jsonSerialize() does not exist on Psr\Http\Message\StreamInterface. It seems like you code against a sub-type of said class. However, the method does not exist in GuzzleHttp\Psr7\BufferStream or GuzzleHttp\Psr7\PumpStream or GuzzleHttp\Psr7\AppendStream or GuzzleHttp\Psr7\Stream or GuzzleHttp\Psr7\FnStream. Are you sure you never get one of those?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
Loading history...
|
|||||
| 48 | } |
||||
| 49 | |||||
| 50 | public function scrubMicrosoftGraphUserData($userData) |
||||
| 51 | { |
||||
| 52 | // Fix any stupid crap with missing or null fields |
||||
| 53 | if (! isset($userData['mail']) || ! $userData['mail']) { |
||||
| 54 | \Illuminate\Support\Facades\Log::debug('graph api did not contain mail field, using userPrincipalName instead '.json_encode($userData)); |
||||
| 55 | $userData['mail'] = $userData['userPrincipalName']; |
||||
| 56 | } |
||||
| 57 | |||||
| 58 | return $userData; |
||||
| 59 | } |
||||
| 60 | |||||
| 61 | protected function findOrCreateUser($userData) |
||||
| 62 | { |
||||
| 63 | // Configurable \App\User type and ID field name |
||||
| 64 | $userType = config('enterpriseauth.user_class'); |
||||
| 65 | $userIdField = config('enterpriseauth.user_id_field'); |
||||
| 66 | // Try to find an existing user |
||||
| 67 | $user = $userType::where($userIdField, $userData['id'])->first(); |
||||
| 68 | // If we dont have an existing user |
||||
| 69 | if (! $user) { |
||||
| 70 | // Go create a new one with this data |
||||
| 71 | $user = $this->createUserFromAzureData($userData); |
||||
| 72 | } |
||||
| 73 | |||||
| 74 | return $user; |
||||
| 75 | } |
||||
| 76 | |||||
| 77 | // This takes the azure userdata and makes a new user out of it |
||||
| 78 | public function createUserFromAzureData($userData) |
||||
| 79 | { |
||||
| 80 | // Config options for user type/id/field map |
||||
| 81 | $userType = config('enterpriseauth.user_class'); |
||||
| 82 | $userFieldMap = config('enterpriseauth.user_map'); |
||||
| 83 | $idField = config('enterpriseauth.user_id_field'); |
||||
| 84 | |||||
| 85 | // Should build new \App\User |
||||
| 86 | $user = new $userType(); |
||||
| 87 | $user->$idField = $userData['id']; |
||||
| 88 | // Go through any other fields the config wants us to map |
||||
| 89 | foreach ($userFieldMap as $azureField => $userField) { |
||||
| 90 | if (isset($userData[$azureField])) { |
||||
| 91 | $user->$userField = $userData[$azureField]; |
||||
| 92 | } else { |
||||
| 93 | \Illuminate\Support\Facades\Log::info('createUserFromAzureData did not contain configured field '.$azureField.' in '.json_encode($userData)); |
||||
| 94 | } |
||||
| 95 | } |
||||
| 96 | // Save our newly minted user |
||||
| 97 | $user->save(); |
||||
| 98 | |||||
| 99 | return $user; |
||||
| 100 | } |
||||
| 101 | |||||
| 102 | public function certAuth() |
||||
| 103 | { |
||||
| 104 | // get the cert from the webserver and load it into an x509 phpseclib object |
||||
| 105 | $cert = $this->loadClientCertFromWebserver(); |
||||
| 106 | // extract the UPN from the client cert |
||||
| 107 | $upn = $this->getUserPrincipalNameFromClientCert($cert); |
||||
| 108 | // get the user if it exists |
||||
| 109 | $user_class = config('enterpriseauth.user_class'); |
||||
| 110 | |||||
| 111 | // TODO: rewrite this so that if the user doesnt exist we create them and get their groups from AAD |
||||
| 112 | $user = $user_class::where('userPrincipalName', $upn)->first(); |
||||
| 113 | if (! $user) { |
||||
| 114 | throw new \Exception('No user found with user principal name '.$upn); |
||||
| 115 | } |
||||
| 116 | |||||
| 117 | return $user; |
||||
| 118 | } |
||||
| 119 | |||||
| 120 | public function loadClientCertFromWebserver() |
||||
| 121 | { |
||||
| 122 | // Make sure we got a client certificate from the web server |
||||
| 123 | if (! isset($_SERVER['SSL_CLIENT_CERT']) || ! $_SERVER['SSL_CLIENT_CERT']) { |
||||
| 124 | throw new \Exception('TLS client certificate missing'); |
||||
| 125 | } |
||||
| 126 | // try to parse the certificate we got |
||||
| 127 | $x509 = new \phpseclib\File\X509(); |
||||
| 128 | // NGINX screws up the cert by putting a bunch of tab characters into it so we need to clean those out |
||||
| 129 | $asciicert = str_replace("\t", '', $_SERVER['SSL_CLIENT_CERT']); |
||||
| 130 | $x509->loadX509($asciicert); |
||||
| 131 | |||||
| 132 | return $x509; |
||||
| 133 | } |
||||
| 134 | |||||
| 135 | public function getUserPrincipalNameFromClientCert($x509) |
||||
| 136 | { |
||||
| 137 | $names = $x509->getExtension('id-ce-subjectAltName'); |
||||
| 138 | if (! $names) { |
||||
| 139 | throw new \Exception('TLS client cert missing subject alternative names'); |
||||
| 140 | } |
||||
| 141 | // Search subject alt names for user principal name |
||||
| 142 | $upn = ''; |
||||
| 143 | foreach ($names as $name) { |
||||
| 144 | foreach ($name as $key => $value) { |
||||
| 145 | if ($key == 'otherName') { |
||||
| 146 | if (isset($value['type-id']) && $value['type-id'] == '1.3.6.1.4.1.311.20.2.3') { |
||||
| 147 | $upn = $value['value']['utf8String']; |
||||
| 148 | } |
||||
| 149 | } |
||||
| 150 | } |
||||
| 151 | } |
||||
| 152 | if (! $upn) { |
||||
| 153 | throw new \Exception('Could not find user principal name in TLS client cert'); |
||||
| 154 | } |
||||
| 155 | |||||
| 156 | return $upn; |
||||
| 157 | } |
||||
| 158 | |||||
| 159 | public function updateGroups($user) |
||||
| 160 | { |
||||
| 161 | // See if we can get the users group membership data |
||||
| 162 | $groupData = $this->getMicrosoftGraphGroupMembership($user); |
||||
| 163 | |||||
| 164 | // Process group data into a list of displayNames we use as roles |
||||
| 165 | $groups = []; |
||||
| 166 | foreach ($groupData as $info) { |
||||
| 167 | // Now there are NEW kinds of awful groups where groupTypes => [Unified] is BAD. |
||||
| 168 | // We only want to process groupTypes = [] (empty set evaluates to false) |
||||
| 169 | if (isset($info['groupTypes']) && $info['groupTypes'] == false) { |
||||
| 170 | $groups[] = $info['displayName']; |
||||
| 171 | } else { |
||||
| 172 | \Illuminate\Support\Facades\Log::debug('skipping grouptype named '.$info['displayName']); |
||||
| 173 | } |
||||
| 174 | } |
||||
| 175 | // make sure the array of groups is UNIQUE because stupid azuread names are not! |
||||
| 176 | $groups = array_unique($groups); |
||||
| 177 | |||||
| 178 | // If we have user group information from this oauth attempt |
||||
| 179 | \Illuminate\Support\Facades\Log::debug('assigning user to '.count($groups).' groups as roles'); |
||||
| 180 | if (count($groups)) { |
||||
| 181 | // remove the users existing database roles before assigning new ones |
||||
| 182 | \DB::table('assigned_roles') |
||||
| 183 | ->where('entity_id', $user->id) |
||||
| 184 | ->where('entity_type', get_class($user)) |
||||
| 185 | ->delete(); |
||||
| 186 | // TRY to add the user to each group they are assigned |
||||
| 187 | try { |
||||
| 188 | $user->assign($groups); |
||||
| 189 | } catch (\Exception $e) { |
||||
| 190 | \Illuminate\Support\Facades\Log::debug('unable to add user to groups '.implode(',', $groups).' because'.$e->getMessage()); |
||||
| 191 | } |
||||
| 192 | } |
||||
| 193 | } |
||||
| 194 | |||||
| 195 | public function getMicrosoftGraphGroupMembership($user) |
||||
| 196 | { |
||||
| 197 | // Get an access token for our application (not the users token) |
||||
| 198 | $accessToken = $this->azureActiveDirectory->getApplicationAccessToken(config('enterpriseauth.credentials.client_id'), config('enterpriseauth.credentials.client_secret')); |
||||
| 199 | |||||
| 200 | // Use the app access token to get a given users group membership |
||||
| 201 | $graph = new \Microsoft\Graph\Graph(); |
||||
| 202 | $graph->setAccessToken($accessToken); |
||||
| 203 | $path = '/users/'.$user->userPrincipalName.'/memberOf'; |
||||
| 204 | // $groups = $graph->createRequest('GET', $path) |
||||
| 205 | // $graph->createCollectionRequest('GET', $path)->setReturnType(\Microsoft\Graph\Model\Group::class)->setPageSize(200)->execute(); |
||||
| 206 | $groups = $graph->createCollectionRequest('GET', $path) |
||||
| 207 | ->setReturnType(\Microsoft\Graph\Model\Group::class) |
||||
| 208 | ->setPageSize(900) |
||||
| 209 | ->execute(); |
||||
| 210 | \Illuminate\Support\Facades\Log::debug('azure ad returned '.count($groups).' groups for user'); |
||||
|
0 ignored issues
–
show
It seems like
$groups can also be of type Microsoft\Graph\Http\GraphResponse and Psr\Http\Message\StreamInterface; however, parameter $var of count() does only seem to accept Countable|array, maybe add an additional type check?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
Loading history...
|
|||||
| 211 | |||||
| 212 | // Convert the microsoft graph group objects into data that is useful |
||||
| 213 | $groupData = []; |
||||
| 214 | foreach ($groups as $group) { |
||||
| 215 | $groupData[] = $group->jsonSerialize(); |
||||
| 216 | } |
||||
| 217 | |||||
| 218 | return $groupData; |
||||
| 219 | } |
||||
| 220 | |||||
| 221 | // Try to unpack a jwt and get us the 3 chunks as assoc arrays so we can perform token identification |
||||
| 222 | public function unpackJwt($jwt) |
||||
| 223 | { |
||||
| 224 | list($headb64, $bodyb64, $cryptob64) = explode('.', $jwt); |
||||
| 225 | $token = [ |
||||
| 226 | 'header' => json_decode(\Firebase\JWT\JWT::urlsafeB64Decode($headb64), true), |
||||
| 227 | 'payload' => json_decode(\Firebase\JWT\JWT::urlsafeB64Decode($bodyb64), true), |
||||
| 228 | 'signature' => $cryptob64, |
||||
| 229 | ]; |
||||
| 230 | |||||
| 231 | return $token; |
||||
| 232 | } |
||||
| 233 | |||||
| 234 | // calculate the delta between $jwt['exp'] and time() / 60 for minutes remaining |
||||
| 235 | protected function getTokenMinutesRemaining($accessToken) |
||||
| 236 | { |
||||
| 237 | $tokenData = $this->unpackJwt($accessToken); |
||||
| 238 | $now = time(); |
||||
| 239 | $expires = $tokenData['payload']['exp']; |
||||
| 240 | $remainingSecs = $expires - $now; |
||||
| 241 | // round up to the nearest minute |
||||
| 242 | $remainingMins = ceil($remainingSecs / 60); |
||||
| 243 | |||||
| 244 | return $remainingMins; |
||||
| 245 | } |
||||
| 246 | } |
||||
| 247 |
This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.
This is most likely a typographical error or the method has been renamed.