LTI Service - Platform
How to use the bundle to make your application act as a platform in the context of LTI services.
Providing platform service access token endpoint
The OAuth2AccessTokenCreationAction is automatically added to your application via the related flex recipe, in file config/routes/lti1p3.yaml
.
Default route: [POST] '/lti1p3/auth/{keyChainIdentifier}/token'
This endpoint:
- allow tools to get granted to call your platform services endpoints, by following the client_credentials grant type with assertion.
- is working for a defined
keyChainIdentifier
as explained here, so you can expose several of them if your application is acting as several deployed platforms - is able to grant (give access tokens) for a defined list of allowed scopes
You must first configure the list of allowed scopes to grant access tokens:
# config/packages/lti1p3.yaml
lti1p3:
scopes:
- 'https://purl.imsglobal.org/spec/lti-ags/scope/lineitem'
- 'https://purl.imsglobal.org/spec/lti-ags/scope/result/read'
Then, if you configure a key chain as following:
# config/packages/lti1p3.yaml
lti1p3:
key_chains:
platformKey:
key_set_name: "platformSet"
public_key: "file://path/to/public.key"
private_key: "file://path/to/private.key"
private_key_passphrase: 'someSecretPassPhrase'
You can then configure a platform as following (using the key chain identifier platformKey
):
# config/packages/lti1p3.yaml
lti1p3:
platforms:
myPlatform:
name: "My Platform"
audience: "http://platform.com"
oidc_authentication_url: "http://platform.com/lti1p3/oidc/authentication"
oauth2_access_token_url: "http://platform.com/lti1p3/auth/platformKey/token"
Once set up, tools can request access tokens by following the client_credentials grant type with assertion:
grant_type
:client_credentials
client_assertion_type
:urn:ietf:params:oauth:client-assertion-type:jwt-bearer
client_assertion
: the tool's generated JWT assertionscope
:https://purl.imsglobal.org/spec/lti-ags/scope/lineitem https://purl.imsglobal.org/spec/lti-ags/scope/result/read
Request example:
POST /lti1p3/auth/platformKey/token HTTP/1.1
Host: example.com
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials
&client_assertion_type=urn%3Aietf%3Aparams%3Aoauth%3Aclient-assertion-type%3Ajwt-bearer
&client_assertion=eyJ0eXAiOi....
&scope=http%3A%2F%2Fimsglobal.org%2Fspec%2Flti-ags%2Fscope%2Flineitem%20http%3A%2F%2Fimsglobal.org%2Fspec%2Flti-ags%2Fscope%2Fresult%2Fread
As a response, the OAuth2AccessTokenCreationAction will offer an access token (following OAuth2 standards), valid for 3600 seconds
:
HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
Cache-Control: no-store
Pragma: no-cache
{
"access_token" : "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1N.....",
"token_type" : "bearer",
"expires_in" : 3600,
"scope" : "https://purl.imsglobal.org/spec/lti-ags/scope/lineitem https://purl.imsglobal.org/spec/lti-ags/scope/result/read"
}
Notes:
- a
HTTP 400
response is returned if the requested scopes are not configured, or invalid - a
HTTP 401
response is returned if the client assertion cannot match a registered tool - to automate (and cache) authentication grants from the tools side, a LtiServiceClient is ready to use for your LTI service calls as explained here
Protecting platform service endpoints
For example, considering you have the following platform service endpoints:
#config/routes.yaml
platform_service_ags_lineitem:
path: /platform/service/ags/lineitem
controller: App\Action\Platform\Service\Ags\LineItemAction
platform_service_ags_result:
path: /platform/service/ags/result
controller: App\Action\Platform\Service\Ags\ResultAction
To protect your endpoint, this bundle provides the lti1p3_service
security firewall to put in front of your routes:
# config/packages/security.yaml
security:
firewalls:
secured_service_ags_lineitem_area:
pattern: ^/platform/service/ags/lineitem
stateless: true
lti1p3_service: { scopes: ['https://purl.imsglobal.org/spec/lti-ags/scope/lineitem'] }
secured_service_ags_result_area:
pattern: ^/platform/service/ags/result
stateless: true
lti1p3_service: { scopes: ['https://purl.imsglobal.org/spec/lti-ags/scope/result/read'] }
Note: you can define per firewall the list of allowed scopes, to have better granularity for your endpoints protection.
It will:
- handle the provided access token validation (signature validity, expiry, matching configured firewall scopes, etc ...)
- add on success a LtiServiceSecurityToken in the security token storage, that you can use to retrieve your authentication context
For example (in one of the endpoints):
<?php
declare(strict_types=1);
namespace App\Action\Platform\Service\Ags;
use OAT\Bundle\Lti1p3Bundle\Security\Authentication\Token\Service\LtiServiceSecurityToken;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Security;
class LineItemAction
{
/** @var Security */
private $security;
public function __construct(Security $security)
{
$this->security = $security;
}
public function __invoke(Request $request): Response
{
/** @var LtiServiceSecurityToken $token */
$token = $this->security->getToken();
// Related registration (to spare queries)
$registration = $token->getRegistration();
// Related access token
$token = $token->getAccessToken();
// Related scopes (if you want to implement some ACL)
$scopes = $token->getScopes(); // ['https://purl.imsglobal.org/spec/lti-ags/scope/lineitem']
// You can even access validation results
$validationResults = $token->getValidationResult();
// Your service endpoint logic ...
return new Response(...);
}
}
Providing platform service endpoints using the LTI libraries
We provide a collection of LTI libraries to offer LTI capabilities (NRPS, AGS, basic outcomes, etc) to your application.
The bundle provides a way to easily integrate them when it comes to expose LTI services endpoints:
- the core LtiServiceServerRequestHandlerInterface is implemented by those libraries to provide their service endpoints logic
- the bundle LtiServiceServerHttpFoundationRequestHandlerInterface symfony service automates any
LtiServiceServerRequestHandlerInterface
implementation execution - the bundle LtiServiceServerHttpFoundationRequestHandlerFactoryInterface symfony service can be used to ease the
LtiServiceServerHttpFoundationRequestHandlerInterface
creation (symfony service factory)
For example, let's implement step by step the NRPS library membership service endpoint into your application:
- install the library
$ composer require oat-sa/lib-lti1p3-nrps
- allow NRPS scope
# config/packages/lti1p3.yaml
lti1p3:
scopes:
- 'https://purl.imsglobal.org/spec/lti-nrps/scope/contextmembership.readonly'
- provide a required MembershipServiceServerBuilderInterface implementation
<?php
declare(strict_types=1);
namespace App\Nrps;
use OAT\Library\Lti1p3Core\Registration\RegistrationInterface;
use OAT\Library\Lti1p3Nrps\Model\Membership\MembershipInterface;
use OAT\Library\Lti1p3Nrps\Service\Server\Builder\MembershipServiceServerBuilderInterface;
class MembershipServiceServerBuilder implements MembershipServiceServerBuilderInterface
{
public function buildContextMembership(
RegistrationInterface $registration,
?string $role = null,
?int $limit = null,
?int $offset = null
): MembershipInterface {
// Logic for building context membership for a given registration
}
public function buildResourceLinkMembership(
RegistrationInterface $registration,
string $resourceLinkIdentifier,
?string $role = null,
?int $limit = null,
?int $offset = null
): MembershipInterface {
// Logic for building resource link membership for a given registration and resource link identifier
}
};
- register the NRPS library request handler using your builder in your application services
# config/services.yaml
services:
OAT\Library\Lti1p3Nrps\Service\Server\Handler\MembershipServiceServerRequestHandler:
arguments:
- '@App\Nrps\MembershipServiceServerBuilder'
- use the LtiServiceServerHttpFoundationRequestHandlerFactoryInterface service factory to create a controller service
# config/services.yaml
services:
app.nrps_membership_controller:
class: OAT\Bundle\Lti1p3Bundle\Service\Server\Handler\LtiServiceServerHttpFoundationRequestHandler
factory: ['@OAT\Bundle\Lti1p3Bundle\Service\Server\Factory\LtiServiceServerHttpFoundationRequestHandlerFactoryInterface', 'create']
arguments:
- '@OAT\Library\Lti1p3Nrps\Service\Server\Handler\MembershipServiceServerRequestHandler'
tags: ['controller.service_arguments']
- bind this controller service to a route in your application
# config/routes.yaml
nrps_membership:
path: /platform/service/nrps
controller: app.nrps_membership_controller
- protect this route using the bundle service firewall
# config/packages/security.yaml
security:
firewalls:
secured_service_nrps_area:
pattern: ^/platform/service/nrps
stateless: true
lti1p3_service: { scopes: ['https://purl.imsglobal.org/spec/lti-nrps/scope/contextmembership.readonly'] }
At this point, your application now offers a new endpoint [GET] /platform/service/nrps
, that automates:
- HTTP method validation
- NRPS content type validation
- access token validation
- access token NRPS scope validation
- the response of NRPS memberships representations, relying on the provided membership builder implementation
Note: exposing a controller as service is convenient but not mandatory, you can still inject the LtiServiceServerHttpFoundationRequestHandlerFactoryInterface in a controller constructor to have more control on this process, as done in the bundle TestServiceAction for example.