About Me
Мy name is Dzianis Kotau. I'm Solutions Architect and Zend Certified PHP Engineer. I'm PHP evangelist and loved in it.
IBM App ID Provider for OAuth 2.0 Client
Dzianis Kotau • December 30, 2019
phpLet me introduce you my own IBM App ID Provider for the PHP League's OAuth 2.0 Client. It's not about IBM App ID service, nor PHP League's OAuth 2.0 Client. You can read about those here and here. In brief, IBM App ID is an IBM Cloud service that hides implementation of different OAuth providers (Facebook, Google, SAML, etc.) and allows you to use them in one way. This article is about a provider that you can use to communicate with IBM App ID service using standalone PHP or Symfony.
Table of Contents
Standalone provider
Let's start from standalone PHP. To install the package run
composer require jampire/oauth2-appid
Usage is the same as The League's OAuth client, using \Jampire\OAuth2\Client\Provider\AppIdProvider
as the provider.
Use baseAuthUri
to specify the IBM App ID base server URL. You can lookup the correct value from the Application
settings of your IBM App ID service under oAuthServerUrl
without tenantId
section, eg.
https://us-south.appid.cloud.ibm.com/oauth/v4
.
Use tenantId
to specify the IBM App ID tenant ID. You can lookup the correct value from the Application settings of
your IBM App ID service under tenantId
, eg. abcd-efgh-1234-5678-mnop
.
All other values you can find in Application settings of your IBM App ID service.
Register your redirect URL in your IBM App ID whitelist. Please, read IBM App ID documentation. Otherwise, you can get the error like this:
Authorization Code Flow
<?php
require_once __DIR__ . '/vendor/autoload.php';
use Jampire\OAuth2\Client\Provider\AppIdProvider;
use Jampire\OAuth2\Client\Provider\AppIdException;
session_start();
try {
$provider = new AppIdProvider([
'baseAuthUri' => '{baseAuthUri}',
'tenantId' => '{tenantId}',
'clientId' => '{clientId}',
'clientSecret' => '{clientSecret}',
'redirectUri' => '{redirectUri}', // use http://localhost for testing
]);
} catch (AppIdException $e) {
exit('Failed to create provider: ' . $e->getMessage());
}
if (!isset($_GET['code'])) {
// If we don't have an authorization code then get one
// Fetch the authorization URL from the provider; this returns the
// urlAuthorize option and generates and applies any necessary parameters
// (e.g. state).
$authorizationUrl = $provider->getAuthorizationUrl();
// Get the state generated for you and store it to the session.
$_SESSION['oauth2state'] = $provider->getState();
// Redirect the user to the authorization URL.
header('Location: ' . $authorizationUrl);
exit;
}
if (empty($_GET['state']) || (isset($_SESSION['oauth2state']) && $_GET['state'] !== $_SESSION['oauth2state'])) {
// Check given state against previously stored one to mitigate CSRF attack
if (isset($_SESSION['oauth2state'])) {
unset($_SESSION['oauth2state']);
}
exit('Invalid state');
}
try {
// Try to get an access token using the authorization code grant.
$accessToken = $provider->getAccessToken('authorization_code', [
'code' => $_GET['code']
]);
// We have an access token, which we may use in authenticated
// requests against the service provider's API.
echo '<b>Access Token:</b> ', $accessToken->getToken(), '<br>';
echo '<b>Refresh Token:</b> ', $accessToken->getRefreshToken(), '<br>';
echo '<b>Expired in:</b> ', $accessToken->getExpires(), '<br>';
echo '<b>Already expired?</b> ', ($accessToken->hasExpired() ? 'expired' : 'not expired'), '<br>';
// Using the access token, we may look up details about the
// resource owner.
$resourceOwner = $provider->getResourceOwner($accessToken);
} catch (Exception $e) {
// Failed to get the access token or user details.
exit($e->getMessage());
}
Then run in your console:
php -S localhost:80
If you did everything right, you’ll be redirect to App ID for authentication. Depending on your configuration you’ll be shown with a login widget (either App ID’s or redirected directly to the single configured IdP, but that’s far less important for your use case now).
My login page is IBM SSO one
In case, you are using Facebook or Google, you will see their own login pages.
Final result
After successful authorization, you will be redirected to your registered URL (http://localhost in our test case). And you will see something like this (note, that my IDP is SAML):
You are now authorized in the system and can perform different tasks as authorized user.
Symfony integration
Note: I'm using knpuniversity/oauth2-client-bundle for Symfony integration. My provider was included to the bundle since version 1.33.0.
Note: Full documentation for adding providers is available at KnpUOAuth2ClientBundle.
This example is based on Symfony v4.3.
I assume that you already have Symfony installation, but without OAuth2 functionality. So, to start using it install
knpuniversity/oauth2-client-bundle
and jampire/oauth2-appid
:
composer require knpuniversity/oauth2-client-bundle jampire/oauth2-appid
Step 1 - Configuring Security
Configure your oauth security.
# config/packages/security.yaml
security:
providers:
oauth:
id: App\Security\UserProvider
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
guard:
authenticators:
- App\Security\AppIdAuthenticator
# config/packages/knpu_oauth2_client.yaml
knpu_oauth2_client:
# can be set to the service id of a service that implements Guzzle\ClientInterface
# http_client: null
# options to configure the default http client
# http_client_options:
# timeout: 0
# proxy: null
# Use only with proxy option set
# verify: false
clients:
# will create service: "knpu.oauth2.client.appid"
# an instance of: KnpU\OAuth2ClientBundle\Client\Provider\AppIdClient
# composer require jampire/oauth2-appid
appid:
# must be "appid" - it activates that type!
type: appid
# add and configure client_id and client_secret in .env* file
client_id: '%env(OAUTH_APPID_CLIENT_ID)%'
client_secret: '%env(OAUTH_APPID_CLIENT_SECRET)%'
# a route name you'll create
redirect_route: '%env(OAUTH_APPID_REDIRECT_ROUTE)%'
redirect_params: {}
# IBM App ID base URL. For example, "https://us-south.appid.cloud.ibm.com/oauth/v4". More details at https://cloud.ibm.com/docs/services/appid?topic=appid-getting-started
base_auth_uri: '%env(OAUTH_APPID_BASE_AUTH_URI)%'
# IBM App ID service tenant ID. For example, "1234-5678-abcd-efgh". More details at https://cloud.ibm.com/docs/services/appid?topic=appid-getting-started
tenant_id: '%env(OAUTH_APPID_TENANT_ID)%'
# Identity Provider code. Defaults to "saml". More details at https://cloud.ibm.com/docs/services/appid?topic=appid-getting-started
# idp: '%env(OAUTH_APPID_IDP)%'
# whether to check OAuth2 "state": defaults to true
# use_state: true
Add your credentials in env. My IDP is SAML here.
OAUTH_APPID_BASE_AUTH_URI=https://us-south.appid.cloud.ibm.com/oauth/v4
OAUTH_APPID_REDIRECT_ROUTE=connect_appid_check
OAUTH_APPID_TENANT_ID=xxxxxxxxxxxxxxxxxxxxxxxxxx
OAUTH_APPID_CLIENT_ID=xxxxxxxxxxxxxxxxxxxxxxxxxx
OAUTH_APPID_CLIENT_SECRET=xxxxxxxxxxxxxxxxxxxxxx
Step 2 - Add the client controller
Create IBM App ID authenticator controller
<?php
// src/Controller/AppIdController.php
namespace App\Controller;
use KnpU\OAuth2ClientBundle\Client\ClientRegistry;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
/**
* Class AppIdController
*
* @author Dzianis Kotau <jampire.blr@gmail.com>
* @package App\Controller
*/
class AppIdController extends AbstractController
{
/**
* @Route("/connect", name="connect_appid")
*
* Authorization route
*
* @param ClientRegistry $clientRegistry
*
* @return RedirectResponse
* @author Dzianis Kotau <jampire.blr@gmail.com>
*/
public function connect(ClientRegistry $clientRegistry): RedirectResponse
{
return $clientRegistry->getClient('appid')->redirect();
}
/**
* @Route("/connect/check", name="connect_appid_check")
*
* Callback route
*
* @return Response
* @author Dzianis Kotau <jampire.blr@gmail.com>
*/
public function check(): Response
{
if (!$this->getUser()) {
return new JsonResponse([
'status' => false,
'message' => 'User not found!',
]);
}
return $this->redirectToRoute('home');
}
}
Create HomeController
<?php
// src/Controller/HomeController.php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Routing\Annotation\Route;
/**
* Class HomeController
*
* @author Dzianis Kotau <jampire.blr@gmail.com>
* @package App\Controller
*/
class HomeController extends AbstractController
{
/**
* @Route("/", name="home")
* @return JsonResponse
* @author Dzianis Kotau <jampire.blr@gmail.com>
*/
public function home(): JsonResponse
{
$this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY');
return new JsonResponse([
'name' => $this->getUser()->getFullName(),
'email' => $this->getUser()->getEmail(),
]);
}
}
Step 3 - Add the guard authenticator
Create IBM App ID authenticator guard. Below code block is published under MIT license. Please see License File for more information.
<?php
// src/Security/AppIdAuthenticator.php
namespace App\Security;
use KnpU\OAuth2ClientBundle\Client\OAuth2ClientInterface;
use KnpU\OAuth2ClientBundle\Security\Authenticator\SocialAuthenticator;
use KnpU\OAuth2ClientBundle\Client\ClientRegistry;
use League\OAuth2\Client\Token\AccessToken;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
/**
* Class AppIdAuthenticator
*
* @author Dzianis Kotau <jampire.blr@gmail.com>
* @package App\Security
*/
class AppIdAuthenticator extends SocialAuthenticator
{
/** @var ClientRegistry */
private $clientRegistry;
/** @var RouterInterface */
private $router;
public function __construct(ClientRegistry $clientRegistry, RouterInterface $router)
{
$this->clientRegistry = $clientRegistry;
$this->router = $router;
}
/**
* @param Request $request
*
* @return bool
* @author Dzianis Kotau <jampire.blr@gmail.com>
*/
public function supports(Request $request): bool
{
$provider = $this->getClient()->getOAuth2Provider();
return ($request->isMethod('GET') &&
$request->getPathInfo() === $this->router->generate($provider->getRedirectRoute()));
}
/**
* @param Request $request
*
* @return AccessToken|mixed
* @author Dzianis Kotau <jampire.blr@gmail.com>
*/
public function getCredentials(Request $request): AccessToken
{
return $this->fetchAccessToken($this->getClient());
}
/**
* @param mixed $credentials
* @param UserProviderInterface|UserProvider $userProvider
*
* @return UserInterface
* @author Dzianis Kotau <jampire.blr@gmail.com>
*/
public function getUser($credentials, UserProviderInterface $userProvider): UserInterface
{
return $userProvider->loadUserByUsername($this->getClient()->fetchUserFromToken($credentials));
}
/**
* @param mixed $credentials
* @param UserInterface $user
*
* @return bool
* @author Dzianis Kotau <jampire.blr@gmail.com>
*/
public function checkCredentials($credentials, UserInterface $user): bool
{
$provider = $this->getClient()->getOAuth2Provider();
return $provider->validateAccessToken($credentials);
}
/**
* @return OAuth2ClientInterface|\KnpU\OAuth2ClientBundle\Client\Provider\AppIdClient
* @author Dzianis Kotau <jampire.blr@gmail.com>
*/
private function getClient(): OAuth2ClientInterface
{
return $this->clientRegistry->getClient('appid');
}
/**
* @param Request $request
* @param TokenInterface $token
* @param string $providerKey
*
* @return Response|null
* @author Dzianis Kotau <jampire.blr@gmail.com>
*/
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey): ?Response
{
// let the request continue to be handled by the controller
return null;
}
/**
* @param Request $request
* @param AuthenticationException $exception
*
* @return Response
* @author Dzianis Kotau <jampire.blr@gmail.com>
*/
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): Response
{
$message = strtr($exception->getMessageKey(), $exception->getMessageData());
return new Response($message, Response::HTTP_FORBIDDEN);
}
/**
* Called when authentication is needed, but it's not sent.
* This redirects to the App ID authorization.
* @param Request $request
* @param AuthenticationException|null $authException
*
* @return RedirectResponse
* @author Dzianis Kotau <jampire.blr@gmail.com>
*/
public function start(Request $request, AuthenticationException $authException = null): RedirectResponse
{
return $this->getClient()->redirect();
}
}
Step 4 - Add User security
Full documentation for adding providers is available at Symfony Security Documentation.
Create User provider class. Below code block is published under MIT license. Please see License File for more information.
<?php
// src/Security/UserProvider.php
namespace App\Security;
use KnpU\OAuth2ClientBundle\Security\User\OAuthUserProvider;
use League\OAuth2\Client\Provider\ResourceOwnerInterface;
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
use Symfony\Component\Security\Core\User\UserInterface;
use function get_class;
/**
* Class UserProvider
*
* @author Dzianis Kotau <jampire.blr@gmail.com>
* @package App\Security
*/
class UserProvider extends OAuthUserProvider
{
private $roles;
public function __construct(array $roles = ['ROLE_USER', 'ROLE_OAUTH_USER'])
{
$this->roles = $roles;
parent::__construct($roles);
}
/**
* @param ResourceOwnerInterface $resourceOwner
*
* @return UserInterface
* @author Dzianis Kotau <jampire.blr@gmail.com>
*/
public function loadUserByUsername($resourceOwner): UserInterface
{
return new User($resourceOwner, $this->roles);
}
public function supportsClass($class): bool
{
return User::class === $class;
}
/**
* @param UserInterface $user
*
* @return UserInterface
* @author Dzianis Kotau <jampire.blr@gmail.com>
*/
public function refreshUser(UserInterface $user): UserInterface
{
if (!$user instanceof User) {
throw new UnsupportedUserException(sprintf('Instances of "%s" are not supported.', get_class($user)));
}
return $this->loadUserByUsername($user->getResourceOwner());
}
}
Create User class. Below code block is published under MIT license. Please see License File for more information.
<?php
// src/Security/User.php
namespace App\Security;
use KnpU\OAuth2ClientBundle\Security\User\OAuthUser;
use League\OAuth2\Client\Provider\ResourceOwnerInterface;
/**
* Class User
*
* @author Dzianis Kotau <jampire.blr@gmail.com>
* @package App\Security
*/
class User extends OAuthUser
{
/** @var string */
private $fullName;
/** @var string */
private $cNum;
/** @var string */
private $lotusNotesId;
/** @var array */
private $ibmInfo = [];
/** @var string */
private $location;
/** @var ResourceOwnerInterface */
private $resourceOwner;
/**
* User constructor.
*
* @param array $roles
* @param ResourceOwnerInterface $resourceOwner
* @author Dzianis Kotau <jampire.blr@gmail.com>
*/
public function __construct(ResourceOwnerInterface $resourceOwner, array $roles)
{
$this->resourceOwner = $resourceOwner;
$this->setFullName($this->resourceOwner->getFullName());
$this->setCnum($this->resourceOwner->getCnum());
$this->setLotusNotesId($this->resourceOwner->getLotusNotesId());
$this->setIbmInfo($this->resourceOwner->getIbmInfo());
$this->setLocation($this->resourceOwner->getLocation());
parent::__construct($resourceOwner->getEmail(), $roles);
}
/**
* @return string|null
* @author Dzianis Kotau <jampire.blr@gmail.com>
*/
public function getEmail(): ?string
{
return $this->getUsername();
}
/**
* @author Dzianis Kotau <jampire.blr@gmail.com>
* @return string
*/
public function getFullName(): ?string
{
return $this->fullName;
}
/**
* @author Dzianis Kotau <jampire.blr@gmail.com>
* @param string $fullName
*
* @return self
*/
public function setFullName(string $fullName): self
{
$this->fullName = $fullName;
return $this;
}
/**
* @author Dzianis Kotau <jampire.blr@gmail.com>
* @return string
*/
public function getCnum(): ?string
{
return $this->cNum;
}
/**
* @param string $cNum
*
* @return self
* @author Dzianis Kotau <jampire.blr@gmail.com>
*/
public function setCnum(string $cNum): self
{
$this->cNum = $cNum;
return $this;
}
/**
* @author Dzianis Kotau <jampire.blr@gmail.com>
* @return string
*/
public function getLotusNotesId(): ?string
{
return $this->lotusNotesId;
}
/**
* @author Dzianis Kotau <jampire.blr@gmail.com>
* @param string $lotusNotesId
*
* @return self
*/
public function setLotusNotesId(string $lotusNotesId): self
{
$this->lotusNotesId = $lotusNotesId;
return $this;
}
/**
* @author Dzianis Kotau <jampire.blr@gmail.com>
* @return array
*/
public function getIbmInfo(): array
{
return $this->ibmInfo;
}
/**
* @author Dzianis Kotau <jampire.blr@gmail.com>
* @param array $ibmInfo
*
* @return self
*/
public function setIbmInfo(array $ibmInfo = []): self
{
$this->ibmInfo = $ibmInfo;
return $this;
}
/**
* @author Dzianis Kotau <jampire.blr@gmail.com>
* @return string
*/
public function getLocation(): ?string
{
return $this->location;
}
/**
* @author Dzianis Kotau <jampire.blr@gmail.com>
* @param string $location
*
* @return self
*/
public function setLocation(string $location): self
{
$this->location = $location;
return $this;
}
/**
* @author Dzianis Kotau <jampire.blr@gmail.com>
* @return ResourceOwnerInterface
*/
public function getResourceOwner(): ResourceOwnerInterface
{
return $this->resourceOwner;
}
}
Then run your application
symfony server:start --no-tls
and try to login into it in the browser.