Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,47 @@
# df-azure-ad
Azure Active Directory Support for DreamFactory


## Authenticating with Azure Active Directory OAuth 2.0 with the On-Behalf-Of flow
With the standard Azure AD OAuth 2.0 authentication flow, you can authenticate your Dream Factory API directly with your Azure AD service. The On-Behalf-Of (OBO) authentication flow allows your Dream Factory API to make requests to your Azure AD service on behalf of the currently authenticated user, instead of authorizing your Dream Factory API itself.

This is achieved by creating a new Azure AD service to represent your Dream Factory API, and authorizing this new service as a Client API for your existing Azure AD service. Your users can authenticate with this new service in the same way as usual, with their SSO credentials. Once this authentication is successfully completed, Dream Factory can request an access token for your target Azure AD service, on behalf of the authenticated user.

This process can be demonstrated in this diagram from the Microsoft OBO flow documentation. For the following setup guide, API A is your Dream Factory API, and API B is your existing Azure AD service.

![OBO Flow Diagram](./doc-images/protocols-oauth-on-behalf-of-flow.png)

There are a few extra steps required to configure and authorize this new Azure AD service.

First, log in to your dashboard at https://entra.microsoft.com/, go to `Identity -> Applications -> App Registrations` and create a new registration with the single tenant type. You will now have two App Registrations:

![App Registrations](./doc-images/AppRegistrations.jpg)


After you have created the App Registration for your Client API, go to the App Registration for your existing API (API B). From the Overview page, copy the value for `Application (client) ID` and navigate to `Expose an API`. In the `Application ID URI` field, set a value equal to `api://<your-client-id>`. Then, under `Scopes defined by this API` add a new scope, with the name of `user_impersonation`. Now, under `Authorized client applications`, add a client application, entering the client id of your newly created App Registration for your Client API, and mark your `user_impersonation` scope as authorized. Your configuration should now look like this:

![Scope Configuration](./doc-images/ScopeConfiguration.jpg)

Next, navigate back to the App Registrations, and select the new Client API, API A. Go to the API Permissions tab, and press `Add a Permission`. In the new permission dialog, go to the `My APIs` tab, click on your API B (your existing Azure AD resource). Then, choose `Delegated permissions` and check the box for your previously created `user_impersonation` scope, and press Add Permissions to finish adding the new permission.

![Add Permission](./doc-images/NewPermission.jpg)

The API Permissions tab for your API A should now look like this:

![API Permissions](./doc-images/API-Permissions.jpg)

Next, you will need to also configure a scope for API A. Under the `Expose an API` tab for API A, your Client API, configure the Application ID URI in the same `api://<your-client-id>` format as before, and add a new scope, also called `user_impersonation`, except this time you will use the Client ID for API A. You do not need to authorize any client applications for this scope.

![API A Scope](./doc-images/API-A-Scope.jpg)

Finally, navigate to the `Certificates and secrets` tab of your API A App Resigistration, go to the `Client secrets` tab, and press `New client secret`. When you have created the new client secret, make sure to keep the value somewhere safe, as you will not be able to view it again, and you will need it when configuring your API in DreamFactory.

![API A Client Secret](./doc-images/ClientSecret.jpg)

This completes the Azure AD configuration, and you are ready to create your new Authentication API in the DreamFactory dashboard.

After logging into the Dream Factory dashboard, go to Security -> Authentication, and create a new Authentication API with the Service Type of `Azure Active Directory OAuth 2.0 On-Behalf-Of`, and configure it as shown below. You will need to add the Client ID of API A, the Client Secret that you added for API A, as well as the scopes that you created for each API. Your Redirect URL will be specific to your own use case, but must match one of the redirect URLs configured in the Azure AD dashboard under Authentication for your App Registrations. The Tenant ID can be found on the Overview tab of either App Registration, and should be the same between both API A and API B.

![DreamFactory Config](./doc-images/DF-Config.jpg)

You should now be able to authenticate with your OAuth OBO service in the same way that you were previously able to with your standard Azure AD OAuth service.
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php

use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreateAzureAdOboConfigTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create(
'azure_ad_obo_config',
function (Blueprint $t){
$t->integer('service_id')->unsigned()->primary();
$t->foreign('service_id')->references('id')->on('service')->onDelete('cascade');
$t->integer('default_role')->unsigned()->nullable();
$t->foreign('default_role')->references('id')->on('role');
$t->string('client_id');
$t->longText('client_secret');
$t->string('client_resource_scope');
$t->string('api_resource_scope');
$t->string('redirect_url');
$t->string('icon_class')->nullable();
$t->string('tenant_id')->default('common');
$t->string('user_resource')->nullable()->default('https://graph.windows.net/');
}
);
}

/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('azure_ad_obo_config');
}
}
Binary file added doc-images/API-A-Scope.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added doc-images/API-Permissions.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added doc-images/AppRegistrations.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added doc-images/ClientSecret.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added doc-images/DF-Config.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added doc-images/NewPermission.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added doc-images/ScopeConfiguration.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added doc-images/protocols-oauth-on-behalf-of-flow.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
258 changes: 258 additions & 0 deletions src/Components/OAuthOboProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,258 @@
<?php
namespace DreamFactory\Core\AzureAD\Components;

use Illuminate\Http\Request;
use GuzzleHttp\ClientInterface;
use SocialiteProviders\Manager\OAuth2\User;
use Laravel\Socialite\Two\ProviderInterface;
use SocialiteProviders\Manager\OAuth2\AbstractProvider;
use DreamFactory\Core\OAuth\Components\DfOAuthTwoOboProvider;
use InvalidArgumentException;

/**
* Class OAuthOboProvider
*
* @package DreamFactory\Core\AzureAD\Components
*
* Implementation of Microsoft OAuth 2.0 On-Behalf-Of (OBO) flow
* https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-on-behalf-of-flow
*/

class OAuthOboProvider extends AbstractProvider implements ProviderInterface
{
use DfOAuthTwoOboProvider;

/** @var null|string */
protected $tokenUrl = null;

/** @var null|string */
protected $authUrl = null;

/** @var array */
protected $scopes = ['User.Read'];

/** @var string */
protected $resource = 'https://graph.microsoft.com/';

/** @var string */
protected $graphUrl = 'https://graph.microsoft.com/v1.0/me';

/** @var null|string */
protected $clientResourceScope = null;

/** @var null|string */
protected $apiResourceScope = null;

/**
* @param string $clientId
* @param string $clientSecret
* @param string $redirectUrl
*/
public function __construct($clientId, $clientSecret, $redirectUrl)
{
/** @var Request $request */
$request = \Request::instance();
parent::__construct($request, $clientId, $clientSecret, $redirectUrl);
}

/**
* {@inheritdoc}
*/
protected function getAuthUrl($state)
{
return $this->buildAuthUrlFromBase(
$this->authUrl, $state
);
}

/**
* {@inheritdoc}
*/
protected function getTokenUrl()
{
return $this->tokenUrl;
}

/**
* {@inheritdoc}
*/
protected function getUserByToken($token)
{
$response = $this->getHttpClient()->get($this->graphUrl, [
'headers' => [
'Authorization' => 'Bearer ' . $token
]
]);

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

/**
* {@inheritdoc}
*/
protected function mapUserToObject(array $user)
{
return (new User())->setRaw($user)->map([
'id' => $user['id'],
'nickname' => $user['displayName'],
'name' => $user['givenName'] . ' ' . $user['surname'],
'email' => $user['userPrincipalName'],
'avatar' => null,
]);
}

/**
* {@inheritdoc}
*/
public function getAccessTokenResponse($code)
{
$postValue = $this->getClientTokenFields($code);

$response = $this->getHttpClient()->post($this->getTokenUrl(), [
'headers' => ['Content-Type' => 'application/x-www-form-urlencoded'],
'form_params' => $postValue,
]);

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

/**
* {@inheritdoc}
*/
public function getDatabaseTokenResponse($token)
{
$postValue = $this->getDatabaseTokenFields($token);

$response = $this->getHttpClient()->post($this->getTokenUrl(), [
'headers' => ['Content-Type' => 'application/x-www-form-urlencoded'],
'form_params' => $postValue,
]);

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

/**
* {@inheritdoc}
*/
public function getGraphTokenResponse($token)
{
$postValue = $this->getGraphTokenFields($token);

$response = $this->getHttpClient()->post($this->getTokenUrl(), [
'headers' => ['Content-Type' => 'application/x-www-form-urlencoded'],
'form_params' => $postValue,
]);

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

/**
* {@inheritdoc}
*/
protected function getCodeFields($state = null)
{
$fields = [
'client_id' => $this->clientId,
'scope' => $this->clientResourceScope,
'response_type' => 'code',
'redirect_uri' => $this->redirectUrl,
];

if ($this->usesState()) {
$fields['state'] = $state;
}

return array_merge($fields, $this->parameters);
}

/**
* {@inheritdoc}
*/
protected function getClientTokenFields($code)
{
$fields = [
'grant_type' => 'authorization_code',
'client_id' => $this->clientId,
'client_secret' => $this->clientSecret,
'code' => $code,
'redirect_uri' => $this->redirectUrl,
];

return $fields;
}

/**
* {@inheritdoc}
*/
protected function getDatabaseTokenFields($token)
{
if (empty($this->clientSecret)) {
throw new InvalidArgumentException('The client secret is required for on-behalf-of token request.');
}else{
$fields = [
'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer',
'client_id' => $this->clientId,
'client_secret' => $this->clientSecret,
'assertion' => $token,
'scope' => $this->apiResourceScope,
'requested_token_use' => 'on_behalf_of',
];

return $fields;
}
}

/**
* {@inheritdoc}
*/
protected function getGraphTokenFields($token)
{
if (empty($this->clientSecret)) {
throw new InvalidArgumentException('The client secret is required for on-behalf-of token request.');
}else{
$fields = [
'grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer',
'client_id' => $this->clientId,
'client_secret' => $this->clientSecret,
'assertion' => $token,
'scope' => $this->formatScopes($this->getScopes(), $this->scopeSeparator),
'requested_token_use' => 'on_behalf_of',
];

return $fields;
}
}

/**
* Sets the OAuth2 endpoints based on tenant ID
*
* @param $tenantId
*/
public function setEndpoints($tenantId)
{
$this->tokenUrl = 'https://login.microsoftonline.com/' . $tenantId . '/oauth2/v2.0/token';
$this->authUrl = 'https://login.microsoftonline.com/' . $tenantId . '/oauth2/v2.0/authorize';
}

/**
* Sets the OAuth 2 resource
*
* @param $resource
*/
public function setResource($resource)
{
$this->resource = $resource;
}

/**
* Sets the OAuth 2 OBO resource scopes
*
* @param $resource
*/
public function setResourceScopes($clientResourceScope, $apiResourceScope)
{
$this->clientResourceScope = $clientResourceScope;
$this->apiResourceScope = $apiResourceScope;
}

}
Loading