Using Swagger to Explore the LTI Advantage API

LTI Advantage platforms support Assignment and Grade Services and Names and Role Provisioning Services. These services include several RESTful endpoints:

  • Assignment and Grade Services
    • LineItems (GET, POST, PUT, DELETE)
    • Results (GET)
    • Scores (POST)
  • Names and Role Provisioning Services
    • Membership (GET)

Together, this API can be used to interact with the class gradebook:

Student 1Student 2Student 3
LineItem 1Score 1
Score 2
Result
LineItem 2
LineItem 3Score 1
Result

You can explore this API in my sample platform using Swagger: https://advantageplatform.azurewebsites.net/swagger. The rest of this post describes how I integrated Swagger into my sample platform.

All of the source code can be found in GitHub:

  • LtiAdvantagePlatform – Sample LTI Advantage Platform using ASP.NET Core. The Swagger tools are installed and configured here.
  • LtiAdvantage – ASP.NET Core library for both platforms and tools. The endpoints are defined and documented here.

Install and Configure Swashbuckle

To make it easier to develop and test the endpoints in my sample platform, I added Swagger tools by adding Swashbuckle.AspNetCore:

Install-Package Swashbuckle.AspNetCore 

To make sure the authorizations are working correctly, I added the OAuth2Scheme to AddSwaggerGen in the ConfigureServices method of Startup.cs:

                // All the controllers in this sample platform require authorization. This 
                // SecurityDefinition will use validate the user is registered, then request
                // an access token from the TokenUrl.
                options.AddSecurityDefinition("oauth2", new OAuth2Scheme
                {
                    TokenUrl = "/connect/token",
                    Type = "oauth2",
                    Flow = "password",
                    Scopes = Config.LtiScopes.ToDictionary(s => s, s => "")
                });

All of the API endpoints look for an Access Token in the request header to authenticate the request. The Swagger UI knows how to request a token from TokenUrl which is handled by Identity Server 4, but Identity Server needs to know about the Swagger UI. This is done in three steps.

First define a “swagger” client in Config.cs:

        /// <summary>
        /// Built-in clients.
        /// </summary>
        /// <returns></returns>
        public static IEnumerable<Client> GetClients()
        {
            return new List<Client>
            {
                // Client for Swagger UI
                new Client
                {
                    ClientId = "swagger",
                    ClientSecrets = new List<Secret>
                    {
                        new Secret("secret".Sha256())
                    },
                    AllowedGrantTypes = GrantTypes.ResourceOwnerPassword,
                    AllowedScopes = LtiScopes,
                    RedirectUris = new [] { "/swagger/oauth2-redirect.html" }
                }
            };
        }

Then load the client in the InitializeDatabase method of Startup.cs:

                    foreach (var client in Config.GetClients())
                    {
                        context.Clients.Add(client.ToEntity());
                    }

And finally configure the Swagger UI to use the client when requesting an Access Token in the Configure method of Startup.cs:

            // Fire up Swagger and Swagger UI
            app.UseSwagger();
            app.UseSwaggerUI(options =>
            {
                options.SwaggerEndpoint("/swagger/v1/swagger.json", "LTI Advantage 1.3");
                options.DocumentTitle = "Advantage Platform - Swagger UI";
                options.OAuthClientId("swagger");
                options.OAuthClientSecret("secret");
            });

Add API Documentation

ASP.NET Core includes OpenAPI documentation to any API by simply decorating the Controller with the [ApiController] attribute. And ASP.NET Core includes the [ProducesResponseType] attribute to add document specific types of responses. For example, since all of the LTI APIs require authorization and specific scopes, they all may return 401 Unauthorized (not signed in) or 403 Forbidden (wrong scope). By decorating the Controller with the [ProducesResponseType] attribute, all of the endpoints inside the controller inherit those two possible responses.

    [ApiController]
    [ProducesResponseType(StatusCodes.Status401Unauthorized)]
    [ProducesResponseType(StatusCodes.Status403Forbidden)]
    public abstract class MembershipControllerBase : ControllerBase, IMembershipController

Each endpoint can have its own, specific documentation too. For example GET Membership will produce a response with a MembershipContainer with a 200 OK status code.

[ProducesResponseType(typeof(MembershipContainer), StatusCodes.Status200OK)]

And Swashbuckle will merge xml-doc comments into the Swagger UI.

        /// <summary>
        /// Returns the membership of a context.
        /// </summary>
        /// <param name="contextId">The context id.</param>
        /// <param name="limit">Optional limit to the number of members to return.</param>
        /// <param name="rlid">Optional resource link filter for members with access to resource link.</param>
        /// <param name="role">Optional role filter for members that have the specified role.</param>
        /// <returns>The members.</returns>

The complete definition of the Membership endpoint looks like this:

    /// <inheritdoc cref="ControllerBase" />
    /// <summary>
    /// Implements the Names and Role Provisioning Service membership endpoint.
    /// </summary>
    [ApiController]
    [ProducesResponseType(StatusCodes.Status401Unauthorized)]
    [ProducesResponseType(StatusCodes.Status403Forbidden)]
    public abstract class MembershipControllerBase : ControllerBase, IMembershipController
    {
        private readonly IHostingEnvironment _env;
        private readonly ILogger<MembershipControllerBase> _logger;

        /// <summary>
        /// </summary>
        protected MembershipControllerBase(IHostingEnvironment env, ILogger<MembershipControllerBase> logger)
        {
            _env = env;
            _logger = logger;
        }

        /// <summary>
        /// Returns the membership.
        /// </summary>
        protected abstract Task<ActionResult<MembershipContainer>> OnGetMembershipAsync(GetMembershipRequest request);

        /// <summary>
        /// Returns the membership of a context.
        /// </summary>
        /// <param name="contextId">The context id.</param>
        /// <param name="limit">Optional limit to the number of members to return.</param>
        /// <param name="rlid">Optional resource link filter for members with access to resource link.</param>
        /// <param name="role">Optional role filter for members that have the specified role.</param>
        /// <returns>The members.</returns>
        [HttpGet]
        [Produces(Constants.MediaTypes.MembershipContainer)]
        [ProducesResponseType(typeof(MembershipContainer), StatusCodes.Status200OK)]
        [ProducesResponseType(StatusCodes.Status400BadRequest)]
        [ProducesResponseType(StatusCodes.Status404NotFound)]
        [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)]
        [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme, 
            Policy = Constants.LtiScopes.NrpsMembershipReadonly)]
        [Route("context/{contextId}/membership", Name = Constants.ServiceEndpoints.NrpsMembershipService)]
        [Route("context/{contextId}/membership.{format}")]
        public virtual async Task<ActionResult<MembershipContainer>> GetMembershipAsync([Required] string contextId, 
            int? limit = null, string rlid = null, Role? role = null)
        {
            try
            {
                _logger.LogDebug($"Entering {nameof(GetMembershipAsync)}.");

                try
                {
                    var request = new GetMembershipRequest(contextId, limit, rlid, role);
                    return await OnGetMembershipAsync(request).ConfigureAwait(false);
                }
                catch (Exception ex)
                {
                    _logger.LogError(ex, $"An unexpected error occurred in {nameof(GetMembershipAsync)}.");
                    return StatusCode(StatusCodes.Status500InternalServerError, new ProblemDetails
                    {
                        Title = "An unexpected error occurred",
                        Status = StatusCodes.Status500InternalServerError,
                        Detail = _env.IsDevelopment()
                            ? ex.Message + ex.StackTrace
                            : ex.Message
                    });
                }
            }
            finally
            {
                _logger.LogDebug($"Exiting {nameof(GetMembershipAsync)}.");
            }
        }
    }

And the Swagger UI looks like this:

Membership Endpoint

You can explore the explore the entire LTI Advantage API in my sample platform using Swagger here: https://advantageplatform.azurewebsites.net/swagger.

This entry was posted in LTI and tagged , , , , , , , , . Bookmark the permalink.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.