Single Sign On Identity Provider
, (*1)
Disclaimer
I am by no means a security expert. I'm not bad at it either, but I cannot vouch for the security of this bundle.
You can use this in production if you want, but please do so at your own risk.
That said, if you'd like to contribute to make this bundle better/safer, you can always create an issue or send a pull request., (*2)
Description
This bundle provides an easy way to integrate a single-sign-on in your website. It uses an existing ('main') firewall for the actual authentication,
and redirects all configured SSO-routes to authenticate via a one-time-password., (*3)
Installation
Installation is a 10 steps process:, (*4)
- Download SingleSignOnIdentityProviderBundle using composer
- Enable the bundle
- Create service provider(s)
- Configure SingleSignOnIdentityProviderBundle
- Enable the route to validate OTP
- Modify security settings
- Add / Modify login and logout success handlers
- Create OTP route
- Add redirect path to login form
- Update database schema
Step 1: Download SingleSignOnIdentityProviderBundle using composer
Tell composer to require the package:, (*5)
``` bash
composer require korotovsky/sso-idp-bundle, (*6)
Composer will install the bundle to your project's `vendor/korotovsky` directory.
### Step 2: Enable the bundle
``` php
Step 3: Create service provider(s)
You have to create a ServiceProvider for each application that uses the SSO SP bundle., (*7)
Each ServiceProvider must implement Krtv\Bundle\SingleSignOnIdentityProviderBundle\Manager\ServiceProviderInterface
., (*8)
``` php
, (*9)
``` php
And define them as services., (*10)
``` yaml, (*11)
app/config/services.yml
services:
acme_bundle.sso.consumer1:
class: AcmeBundle\ServiceProviders\Consumer1
tags:
- { name: sso.service_provider, service: consumer1 }, (*12)
acme_bundle.sso.consumer2:
class: AcmeBundle\ServiceProviders\Consumer2
tags:
- { name: sso.service_provider, service: consumer2 }
### Step 4: Configure SingleSignOnIdentityProviderBundle
The bundle relies on an existing firewall to provide the actual authentication.
To do this, you have to configure the single-sign-on login path to be behind that firewall,
and make sure you need to be authenticated to access that route.
Add the following settings to your **config.yml**.
``` yaml
# app/config/config.yml:
krtv_single_sign_on_identity_provider:
host: idp.example.com
host_scheme: http
login_path: /sso/login/
logout_path: /sso/logout
services:
- consumer1
- consumer2
otp_parameter: _otp
secret_parameter: secret
Step 5: Enable route to validate OTP
``` yaml, (*13)
app/config/routing.yml
sso:
resource: .
type: sso, (*14)
### Step 6: Modify security settings
``` yaml
# app/config/security.yml
security:
access_control:
# We need to allow users to access the /sso/login route
# without being logged in
- { path: ^/sso/login, role: IS_AUTHENTICATED_ANONYMOUSLY }
Step 7: Add / Modify login and logout success handlers
Modify your existing constructor for login and logout success handlers to include the following service:, (*15)
sso_identity_provider.otp_manager
In your method used as for the succes handler, add near the end a call to the method clear()
of that service., (*16)
In case you don't have a success handler for either login or logout, here's a sample implementation for it:, (*17)
``` php
serviceManager = $serviceManager;
$this->uriSigner = $uriSigner;
$this->session = $session;
$this->router = $router;
}
/**
* @param Request $request
* @param TokenInterface $token
*
* @return RedirectResponse
*/
public function onAuthenticationSuccess(Request $request, TokenInterface $token)
{
$redirectUrl = $this->session->get('_security.main.target_path', '/');
if ($request->query->has('_target_path')) {
if ($this->uriSigner->check($request->query->get('_target_path'))) {
$redirectUrl = $request->query->get('_target_path');
}
}
if (strpos($redirectUrl, '/sso/login') === false) {
$targetService = $this->serviceManager->getSessionService();
if ($targetService != null) {
$redirectUrl = $this->getSsoWrappedUrl($token, $targetService, $redirectUrl);
} else {
$redirectUrl = $this->router->generate('_passport_dashboard_index');
}
}
$this->serviceManager->clear();
if ($request->isXmlHttpRequest()) {
return new JsonResponse([
'status' => true,
'location' => $redirectUrl,
]);
}
return new RedirectResponse($redirectUrl);
}
/**
* @param TokenInterface $token
* @param string $targetService
* @param string $redirectUrl
*
* @return string
*/
protected function getSsoWrappedUrl(TokenInterface $token, $targetService, $redirectUrl)
{
/** @var $serviceManager ServiceProviderInterface */
$serviceManager = $this->serviceManager->getServiceManager($targetService);
$owner = $token->getUser();
$wrappedSsoUrl = $this->router->generate('sso_login_path', [
'_target_path' => $serviceManager->getOTPValidationUrl([
'_target_path' => $redirectUrl,
]),
'service' => $targetService,
], Router::ABSOLUTE_URL);
return $this->uriSigner->sign($wrappedSsoUrl);
}
}
?>, (*18)
``` php
serviceManager = $serviceManager;
$this->uriSigner = $uriSigner;
$this->session = $session;
$this->router = $router;
}
/**
* Logout success handler
*
* @param Request $request
*
* @return RedirectResponse|JsonResponse
*/
public function onLogoutSuccess(Request $request)
{
$redirectUrl = $this->session->get('_security.main.target_path', '/');
if ($request->query->has('_target_path')) {
if ($this->uriSigner->check($request->query->get('_target_path'))) {
$redirectUrl = $request->query->get('_target_path');
}
}
$this->serviceManager->clear();
if ($request->isXmlHttpRequest()) {
return new JsonResponse([
'status' => true,
'location' => $redirectUrl,
]);
}
return new RedirectResponse($redirectUrl);
}
}
?>
Define them as services, (*19)
``` yaml, (*20)
app/config/services.yml
services:
acme_bundle.security.login_success_handler:
class: AcmeBundle\Handler\LoginSuccessHandler
arguments:
- "@sso_identity_provider.service_manager"
- "@sso_identity_provider.uri_signer"
- "@session"
- "@router", (*21)
acme_bundle.security.logout_success_handler:
class: AcmeBundle\Handler\LogoutSuccessHandler
arguments:
- "@sso_identity_provider.service_manager"
- "@sso_identity_provider.uri_signer"
- "@session"
- "@router"
And then finally, set the services as handlers in your firewall definition
``` yaml
# app/config/security.yml
security:
firewall:
main:
# ...
form_login:
# ...
success_handler: acme_bundle.security.login_success_handler
logout:
# ...
success_handler: acme_bundle.security.logout_success_handler
Step 8: Create OTP route
In order to validate the OTP and authenticate the user, you must create a route that can retrieve the OTP details
from the database and that can verify if it is valid., (*22)
The route path doesn't really matter, but take note of it. It will be used in the SP bundle.
In our example, the route is /internal/v1/sso
., (*23)
``` php
get('sso_identity_provider.otp_manager');
$pass = str_replace(' ', '+', $request->query->get('_otp'));
/** @var \Krtv\SingleSignOn\Model\OneTimePasswordInterface */
$otp = $otpManager->get($pass);
if (!($otp instanceof OneTimePassword) || $otp->getUsed() === true) {
throw new BadRequestHttpException('Invalid OTP password');
}
$response = [
'data' => [
'created_at' => $otp->getCreated()->format('r'),
'hash' => $otp->getHash(),
'password' => $otp->getPassword(),
'is_used' => $otp->getUsed(),
],
];
$otpManager->invalidate($otp);
return new JsonResponse($response);
}
}
?>, (*24)
### Step 9: Add redirect path to login form
In your login form, add a hidden input with the name `_target_path` and the value
`{{ app.session.get('_security.main.target_path') }}` like so:
``` twig
<input type="hidden" name="_target_path" value="{{ app.session.get('_security.main.target_path') }}" />
This will be used to redirect the user after login to the OTP validation route., (*25)
Step 10: Update database schema
To be able to store the OTPs, you must run the command:, (*26)
bash
php bin/console doctrine:schema:update --force
, (*27)
Public API of this bundle
This bundle registers several services into service container. These services will help you customize SSO flow in the your application:, (*28)
That's it for Identity Provider.
Now you can continue configure ServiceProvider part, (*29)