Skip to content

Commit

Permalink
Merge pull request #8 from itk-dev/develop
Browse files Browse the repository at this point in the history
OpenId Connect Symfony Bundle
  • Loading branch information
jekuaitk authored Sep 16, 2021
2 parents a39d9fa + b46912b commit 1288762
Show file tree
Hide file tree
Showing 27 changed files with 875 additions and 90 deletions.
8 changes: 7 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- This CHANGELOG file to hopefully serve as an evolving example of a
standardized open source project CHANGELOG.
- PHP-CS-Fixer
- Markdownlint
- Markdownlint
- Test Suite
- Psalm setup for static analysis
- Code formatting
- OpenID Connect Bundle: Upgraded from
`itk-dev/openid-connect` 1.0.0 to 2.1.0
- OpenId Connect Bundle: Added CLI login feature.
90 changes: 70 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,12 @@ the bundle authenticator, `OpenIdLoginAuthenticator`.

### Variable configuration

In `/config/packages/` you need the following `itk_dev_openid_connect.yaml`
In `/config/packages/` you need the following `itkdev_openid_connect.yaml`
file for configuring OpenId Connect variables

```yaml
itk_dev_open_id_connect:
open_id_provider_options:
itkdev_openid_connect:
openid_provider_options:
configuration_url: 'https://.../openid-configuration..' # url to OpenId Discovery document
client_id: 'client_id' # Client id assigned by authorizer
client_secret: 'client_secret' # Client password assigned by authorizer
Expand All @@ -41,18 +41,31 @@ itk_dev_open_id_connect:
```
In `/config/routes/` you need a similar
`itk_dev_openid_connect.yaml` file for configuring the routing
`itkdev_openid_connect.yaml` file for configuring the routing

```yaml
itk_dev_openid_connect:
resource: "@ItkDevOpenIdConnectBundle/config/routes.yaml"
itkdev_openid_connect:
resource: "@ItkDevOpenIdConnectBundle/src/Resources/config/routes.yaml"
prefix: "/openidconnect" # Prefix for bundle routes
```

It is not necessary to add a prefix to the bundle routes,
but in case you want i.e. another `/login` route,
it makes distinguishing between them easier.


### CLI login

In order to use the CLI login feature the following
environment variable must be set:

```shell
DEFAULT_URI=
```

See [Symfony documentation](https://symfony.com/doc/current/routing.html#generating-urls-in-commands)
for more information.

### Creating the Authenticator

The bundle handles the extraction of credentials received from the authorizer -
Expand Down Expand Up @@ -99,7 +112,7 @@ security:
main:
guard:
authenticators:
- App\Security\TestAuthenticator
- App\Security\ExampleAuthenticator
```

#### Example authenticator functions
Expand All @@ -125,7 +138,7 @@ use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\User\UserProviderInterface;
class TestAuthenticator extends OpenIdLoginAuthenticator
class ExampleAuthenticator extends OpenIdLoginAuthenticator
{
/**
* @var UrlGeneratorInterface
Expand All @@ -136,32 +149,31 @@ class TestAuthenticator extends OpenIdLoginAuthenticator
*/
private $entityManager;
public function __construct(EntityManagerInterface $entityManager, SessionInterface $session, UrlGeneratorInterface $router)
public function __construct(EntityManagerInterface $entityManager, int $leeway, SessionInterface $session, UrlGeneratorInterface $router, OpenIdConfigurationProvider $provider)
{
$this->router = $router;
$this->entityManager = $entityManager;
parent::__construct($session);
parent::__construct($provider, $session, $leeway);
}
public function getUser($credentials, UserProviderInterface $userProvider)
{
// Extract properties from credentials
$name = $credentials['name'];
$email = $credentials['upn'];
//Check if user exists already - if not create a user
// Check if user exists already - if not create a user
$user = $this->entityManager->getRepository(User::class)
->findOneBy(['email'=> $email]);
if (null === $user) {
// Create the new user
// Create the new user and persist it
$user = new User();
$this->entityManager->persist($user);
}
// Update/set names here
// Update/set user properties
$user->setName($name);
$user->setEmail($email);
// persist and flush user to database
// If no change persist will recognize this
$this->entityManager->persist($user);
$this->entityManager->flush();
return $user;
Expand All @@ -174,11 +186,49 @@ class TestAuthenticator extends OpenIdLoginAuthenticator
public function start(Request $request, AuthenticationException $authException = null)
{
return new RedirectResponse($this->router->generate('itk_dev_openid_connect_login'));
return new RedirectResponse($this->router->generate('itkdev_openid_connect_login'));
}
}
```

For this example we have bound `$leeway` via `.env`
and `services.yaml`:

```text
###> itk-dev/openid-connect-bundle ###
ITK_DEV_LEEWAY=10
###< itk-dev/openid-connect-bundle ###
```

```yaml
services:
_defaults:
bind:
$leeway: '%env(ITK_DEV_LEEWAY)%'
```

## Sign in from command line

Rather than signing in via OpenId Connect, you can get
a sign in url from the command line by providing a username.
Make sure to configure `DEFAULT_URI`. Run

```shell
bin/console itk-dev:openid-connect:login <username>
```

Or

```shell
bin/console itk-dev:openid-connect:login --help
```

for details.

Be aware that a login token only can be used once
before it is removed, and if you used email as your user provider property
the email goes into the `username` argument.

## Changes for Symfony 6.0

In Symfony 6.0 a new security system is
Expand Down Expand Up @@ -207,7 +257,7 @@ docker compose exec phpfpm ./vendor/bin/phpunit

### Psalm static analysis

Where using [Psalm](https://psalm.dev/) for static analysis. To run
Where using [Psalm](https://psalm.dev/) for static analysis. To run
psalm do

```shell
Expand All @@ -230,7 +280,7 @@ the coding standard for the project.

```shell
docker run -v ${PWD}:/app itkdev/yarn:latest install
docker run -v ${PWD}:/app itkdev/yarn:latest check-coding-standards
docker run -v ${PWD}:/app itkdev/yarn:latest coding-standards-check
```

### Apply Coding Standards
Expand All @@ -247,7 +297,7 @@ To attempt to automatically fix coding style

```shell
docker run -v ${PWD}:/app itkdev/yarn:latest install
docker run -v ${PWD}:/app itkdev/yarn:latest check-coding-standards
docker run -v ${PWD}:/app itkdev/yarn:latest coding-standards-apply
```

## CI
Expand Down
6 changes: 4 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,10 @@
"symfony/framework-bundle": "^5.2",
"doctrine/orm": "^2.8",
"symfony/security-bundle": "^5.2",
"itk-dev/openid-connect": "^1.0",
"symfony/yaml": "^5.2"
"itk-dev/openid-connect": "^2.0",
"symfony/yaml": "^5.2",
"symfony/uid": "^5.2",
"symfony/cache": "^5.2"
},
"require-dev": {
"phpunit/phpunit": "^9.5",
Expand Down
7 changes: 7 additions & 0 deletions psalm.xml
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,11 @@
<directory name="vendor" />
</ignoreFiles>
</projectFiles>
<issueHandlers>
<InvalidCatch>
<errorLevel type="suppress">
<referencedClass name="Psr\Cache\InvalidArgumentException" />
</errorLevel>
</InvalidCatch>
</issueHandlers>
</psalm>
104 changes: 104 additions & 0 deletions src/Command/UserLoginCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
<?php

namespace ItkDev\OpenIdConnectBundle\Command;

use ItkDev\OpenIdConnectBundle\Exception\CacheException;
use ItkDev\OpenIdConnectBundle\Util\CliLoginHelper;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
use Symfony\Component\Security\Core\User\UserProviderInterface;

class UserLoginCommand extends Command
{
protected static $defaultName = 'itk-dev:openid-connect:login';
protected static $defaultDescription = 'Get login url for user';

/**
* @var UrlGeneratorInterface
*/
private $urlGenerator;

/**
* @var CliLoginHelper
*/
private $cliLoginHelper;

/**
* @var UserProviderInterface
*/
private $userProvider;

/**
* @var string
*/
private $cliLoginRedirectRoute;

/**
* UserLoginCommand constructor.
*
* @param CliLoginHelper $cliLoginHelper
* @param string $cliLoginRedirectRoute
* @param UrlGeneratorInterface $urlGenerator
* @param UserProviderInterface $userProvider
*/
public function __construct(CliLoginHelper $cliLoginHelper, string $cliLoginRedirectRoute, UrlGeneratorInterface $urlGenerator, UserProviderInterface $userProvider)
{
$this->cliLoginHelper = $cliLoginHelper;
$this->cliLoginRedirectRoute = $cliLoginRedirectRoute;
$this->urlGenerator = $urlGenerator;
$this->userProvider = $userProvider;

parent::__construct();
}

protected function configure(): void
{
$this
->setDescription(self::$defaultDescription)
->addArgument('username', InputArgument::REQUIRED, 'Username');
}

/**
* Executes the CLI login url generation.
*
* @param InputInterface $input
* @param OutputInterface $output
*
* @return int
* @throws CacheException
*/
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$username = $input->getArgument('username');

if (!is_string($username)) {
$io->error('Username is not type string');
return Command::FAILURE;
}
// Check if username is registered in User database
try {
$this->userProvider->loadUserByUsername($username);
} catch (UsernameNotFoundException $e) {
$io->error('User does not exist');
return Command::FAILURE;
}

// Create token via CliLoginHelper
$token = $this->cliLoginHelper->createToken($username);

//Generate absolute url for login
$loginPage = $this->urlGenerator->generate($this->cliLoginRedirectRoute, [
'loginToken' => $token,
], UrlGeneratorInterface::ABSOLUTE_URL);

$io->writeln($loginPage);

return Command::SUCCESS;
}
}
31 changes: 19 additions & 12 deletions src/Controller/LoginController.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,37 +2,44 @@

namespace ItkDev\OpenIdConnectBundle\Controller;

use ItkDev\OpenIdConnect\Exception\ItkOpenIdConnectException;
use ItkDev\OpenIdConnect\Security\OpenIdConfigurationProvider;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Session\SessionInterface;

/**
* Login Controller class.
*/
class LoginController extends AbstractController
{

/**
* @var array
* @var OpenIdConfigurationProvider
*/
private $openIdProviderOptions;
private $provider;

public function __construct(array $openIdProviderOptions)
public function __construct(OpenIdConfigurationProvider $provider)
{
$this->openIdProviderOptions = $openIdProviderOptions;
$this->provider = $provider;
}

/**
* Login method redirecting to authorizer.
*
* @param SessionInterface $session
* @return Response
* @return RedirectResponse
* @throws ItkOpenIdConnectException
*/
public function login(SessionInterface $session): Response
public function login(SessionInterface $session): RedirectResponse
{
$provider = new OpenIdConfigurationProvider($this->openIdProviderOptions);
$nonce = $this->provider->generateNonce();
$state = $this->provider->generateState();

$authUrl = $provider->getAuthorizationUrl();
// Save to session
$session->set('oauth2state', $state);
$session->set('oauth2nonce', $nonce);

// Set a oauth2state to avoid CSRF check it in authenticator
$session->set('oauth2state', $provider->getState());
$authUrl = $this->provider->getAuthorizationUrl(['state' => $state, 'nonce' => $nonce]);

return new RedirectResponse($authUrl);
}
Expand Down
Loading

0 comments on commit 1288762

Please sign in to comment.