Skip to content

Commit

Permalink
Add PingIdentity provider (#2038)
Browse files Browse the repository at this point in the history
Add a local package for the Socialite Package for the PingIdentity
provider.

---------

Co-authored-by: Zack Galbreath <[email protected]>
Co-authored-by: William Allen <[email protected]>
  • Loading branch information
3 people authored Apr 24, 2024
1 parent 6e8d60a commit 2a48ce0
Show file tree
Hide file tree
Showing 12 changed files with 270 additions and 3 deletions.
1 change: 1 addition & 0 deletions app/Providers/EventServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ class EventServiceProvider extends ServiceProvider
\SocialiteProviders\GitLab\GitLabExtendSocialite::class.'@handle',
\SocialiteProviders\GitHub\GitHubExtendSocialite::class.'@handle',
\SocialiteProviders\Google\GoogleExtendSocialite::class.'@handle',
\SocialiteProviders\PingIdentity\PingIdentityExtendSocialite::class.'@handle',
],
];

Expand Down
11 changes: 9 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@
"shiftonelabs/laravel-sqs-fifo-queue": "3.0.1",
"socialiteproviders/github": "4.1.0",
"socialiteproviders/gitlab": "4.1.0",
"socialiteproviders/google": "4.1.0"
"socialiteproviders/google": "4.1.0",
"socialiteproviders/pingidentity": "1.0.0"
},
"require-dev": {
"ext-dom": "*",
Expand Down Expand Up @@ -109,5 +110,11 @@
"post-create-project-cmd": [
"@php artisan key:generate --ansi"
]
}
},
"repositories": [
{
"type": "path",
"url": "./resources/providers/PingIdentity"
}
]
}
39 changes: 38 additions & 1 deletion composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 11 additions & 0 deletions config/services.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,17 @@
'display_name' => "Google",
],

'pingidentity' => [
'client_id' => env('PINGIDENTITY_CLIENT_ID'),
'client_secret' => env('PINGIDENTITY_CLIENT_SECRET'),
'redirect' => env('PINGIDENTITY_REDIRECT_URI'),
'instance_uri' => env('PINGIDENTITY_DOMAIN'),
'enable' => env('PINGIDENTITY_ENABLE', false),
'autoregister' => env('PINGIDENTITY_AUTO_REGISTER_NEW_USERS', false),
'oauth' => true,
'display_name' => "PingIdentity",
],

'mailgun' => [
'domain' => env('MAILGUN_DOMAIN'),
'secret' => env('MAILGUN_SECRET'),
Expand Down
13 changes: 13 additions & 0 deletions docs/authentication.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,19 @@ Begin by [creating OAuth2 credentials for your Google project](https://developer
| GOOGLE_CLIENT_SECRET | The client secret from your Google OAuth2 credentials. | '' |
| GOOGLE_AUTO_REGISTER_NEW_USERS | Whether to automatically register a new user or provide them the Registration form | false

###### PingIdentity

Begin by [creating OAuth2 client in your PingIdentity console](https://docs.pingidentity.com/r/en-us/solution-guides/mzt1663945300370). Then fill out the following `.env` variables:

| Variable | Description | Default |
| -------- |------------ | ------- |
| PINGIDENTITY_ENABLE | Whether or not to use Google as an OAuth2 provider. | false |
| PINGIDENTITY_CLIENT_ID | The client ID from your Google OAuth2 credentials. | '' |
| PINGIDENTITY_CLIENT_SECRET | The client secret from your Google OAuth2 credentials. | '' |
| PINGIDENTITY_DOMAIN | The GitLab server to authenticate against. | https://auth.pingone.com/ |
| PINGIDENTITY_REDIRECT_URI | The callback URL for the CDash instance: <CDashURL>/auth/pingidentity/callback. | '' |
| PINGIDENTITY_AUTO_REGISTER_NEW_USERS | Whether to automatically register a new user or provide them the Registration form | false

## SAML2

To configure CDash to authenticate against a SAML2 identity provider, you need to call `php artisan saml2:create-tenant` from the root of your CDash clone. For more details about the arguments that this Artisan command accepts, please run `php artisan saml2:create-tenant --help` or view the [upstream documentation](https://github.com/24Slides/laravel-saml2/#step-2-create-a-tenant).
Expand Down
20 changes: 20 additions & 0 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -29442,6 +29442,16 @@ parameters:
count: 1
path: database/migrations/2023_04_16_204249_project_name_regex.php

-
message: "#^Method SocialiteProviders\\\\PingIdentity\\\\Provider\\:\\:mapUserToObject\\(\\) has parameter \\$user with no value type specified in iterable type array\\.$#"
count: 1
path: resources/providers/PingIdentity/Provider.php

-
message: "#^Property SocialiteProviders\\\\PingIdentity\\\\Provider\\:\\:\\$scopes type has no value type specified in iterable type array\\.$#"
count: 1
path: resources/providers/PingIdentity/Provider.php

-
message: "#^Undefined variable\\: \\$this$#"
count: 1
Expand Down Expand Up @@ -29558,6 +29568,16 @@ parameters:
count: 1
path: tests/Feature/LdapAuthWithRulesTest.php

-
message: "#^Call to an undefined method Mockery\\\\Expectation\\:\\:shouldReceive\\(\\)\\.$#"
count: 1
path: tests/Feature/LoginAndRegistration.php

-
message: "#^Call to method shouldReceive\\(\\) on an unknown class Laravel\\\\Socialite\\\\PingIdentity\\\\Provider\\.$#"
count: 1
path: tests/Feature/LoginAndRegistration.php

-
message: "#^Cannot call method delete\\(\\) on App\\\\Models\\\\User\\|null\\.$#"
count: 1
Expand Down
Binary file added public/img/pingidentity_signin.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
13 changes: 13 additions & 0 deletions resources/providers/PingIdentity/PingIdentityExtendSocialite.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

namespace SocialiteProviders\PingIdentity;

use SocialiteProviders\Manager\SocialiteWasCalled;

class PingIdentityExtendSocialite
{
public function handle(SocialiteWasCalled $socialiteWasCalled): void
{
$socialiteWasCalled->extendSocialite('pingidentity', Provider::class);
}
}
91 changes: 91 additions & 0 deletions resources/providers/PingIdentity/Provider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
<?php

namespace SocialiteProviders\PingIdentity;

use GuzzleHttp\RequestOptions;
use SocialiteProviders\Manager\OAuth2\AbstractProvider;
use SocialiteProviders\Manager\OAuth2\User;

class Provider extends AbstractProvider
{
public const IDENTIFIER = 'PINGIDENTITY';

/**
* The scopes being requested.
*
* @var array
*/
protected $scopes = [
'openid',
'profile',
'email',
];

protected $scopeSeparator = ' ';

/**
* Get the authentication URL for the provider.
*
* @param string $state
*/
protected function getAuthUrl($state): string
{
$auth_url = $this->buildAuthUrlFromBase($this->getInstanceUri().'/as/authorization.oauth2', $state);
$auth_url .= "&acr_values=Single_Factor&prompt=login";
return $auth_url;
}

/**
* Get the token URL for the provider.
*/
protected function getTokenUrl(): string
{
return $this->getInstanceUri() . '/as/token.oauth2?acr_values=Single_Factor&prompt=login';
}

/**
* Map the raw user array to a Socialite User instance.
*/
protected function mapUserToObject(array $user): \Laravel\Socialite\Two\User
{
return (new User())->setRaw($user)->map([
'nickname' => $user['name'],
'name' => $user['given_name'] . " " . $user['family_name'],
'email' => $user['email'],
]);
}

/**
* Get the Instance URL for the provider.
*/
protected function getInstanceUri(): string
{
return $this->getConfig('instance_uri', 'https://auth.pingone.com/');
}

/**
* Get the raw user for the given access token.
*
* @param string $token
* @return array<string>
*/
protected function getUserByToken($token): array
{
$response = $this->getHttpClient()->get($this->getInstanceUri() . '/idp/userinfo.openid', [
RequestOptions::HEADERS => [
'Authorization' => "Bearer $token",
],
]);

return json_decode((string) $response->getBody(), true);
}

/**
* Additional configuration key values that may be set
* @return array<string>
*/
public static function additionalConfigKeys(): array
{
return ['instance_uri'];
}
}
27 changes: 27 additions & 0 deletions resources/providers/PingIdentity/composer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"name": "socialiteproviders/pingidentity",
"description": "PingIdentity OAuth2 Provider for Laravel Socialite",
"version": "1.0.0",
"license": "MIT",
"keywords": [
"pingidentity",
"laravel",
"oauth",
"provider",
"socialite"
],
"authors": [
{
}
],
"require": {
"php": "^8.0",
"ext-json": "*",
"socialiteproviders/manager": "^4.4"
},
"autoload": {
"psr-4": {
"SocialiteProviders\\PingIdentity\\": ""
}
}
}
7 changes: 7 additions & 0 deletions resources/providers/PingIdentity/packages.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"packages": {
"socialiteproviders/pingidentity": {
"1.0.0": { @composer.json }
}
}
}
40 changes: 40 additions & 0 deletions tests/Feature/LoginAndRegistration.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
use Slides\Saml2\Events\SignedIn as Saml2SignedInEvent;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Tests\TestCase;
use Laravel\Socialite\Facades\Socialite;

class LoginAndRegistration extends TestCase
{
Expand Down Expand Up @@ -250,6 +251,45 @@ public function testSaml2LoginListener() : void
Mockery::close();
}

public function testPingIdentity() : void
{
// Verify that the PingIdentity button doesn't appear by default.
$response = $this->get('/login');
$response->assertDontSeeText('PingIdentity');

// Enable PingIdentity, verify the button appears.
config(['services.pingidentity.enable' => true]);
$response = $this->get('/login');
$response->assertSeeText('PingIdentity');
}

/**
* Test PingIdentity authentication
*/
public function testPingIdentityProvider() : void
{
// Stolen from: https://laracasts.com/discuss/channels/testing/testing-laravel-socialite-callback
$abstractUser = Mockery::mock('Laravel\Socialite\Two\User');
$abstractUser->shouldReceive('getId')
->andReturn(1234567890)
->shouldReceive('getEmail')
->andReturn('[email protected]')
->shouldReceive('getNickname')
->andReturn('Pseudo')
->shouldReceive('getName')
->andReturn('Arlette Laguiller')
->shouldReceive('getAvatar')
->andReturn('https://en.gravatar.com/userimage');

$provider = Mockery::mock('Laravel\Socialite\PingIdentity\Provider');
$provider->shouldReceive('user')->andReturn($abstractUser);

Socialite::shouldReceive('driver')->with('pingidentity')->andReturn($provider);

$response = $this->get("auth/pingidentity/callback");
$response->assertRedirect("/register?fname=Arlette&lname=Laguiller&email=cdash%40test.com");
}

public function testRegisterUserWhenDisabled() : void
{
// Create a user by sending proper data
Expand Down

0 comments on commit 2a48ce0

Please sign in to comment.