Request access token to call a web service with OAuth2

Authorization code flow sample

This flow is recommended for applications that can run their logic on a dedicated backend server outside of the browser.

For the purpose of this demo a third party MVC app that lists Sitefinity CMS` news items, that specific user has access to, is created.

The recommended OAuth flow for such scenarios is the Authorization code. For more information about OAuth authorization code flow see IETF's The OAuth 2.0 Authorization Framework.

Configuration in Sitefinity CMS

First you must setup a third party app in the Sitefinity CMS backend. 

  1. Navigate to Administration » Settings » Advanced » Authentication » OAuthServer » AuthorizedClients.
  2. Click Create new.
    The AuthorizedClients page appears
  3. Fill out ClientId field.
    For instance, type NewsApp.
  4. Fill out Secret field.
    For instance, type testsecret.
  5. Expand the newly created NewsApp client.
  6. Click RedirectUrls.
  7. Click Create new.
  8. Add the redirect URL that is going to be used by the third party app. The URL must match exactly the URL used by the app to listen for the authorization code.
    Use the following URL structure: https://<yourdomaim>/callback
  9. Click Save changes.
  10. Restart Sitefinity CMS.

Then you must configure the server endpoints:

  1. Navigate to Administration » Settings » Advanced » Authentication » OAuthServer.
  2. Fill out AuthorizeEndpointPath.
    This is a OAuth 2 endpoint for authorizing third party requests. For instance, /sitefinity/oauth/authorize.
  3. Fill out TokenEndpointPath.
    This is Oauth 2 endpoint for tokens. For instance, /sitefinity/oauth/token.
  4. Fill out AccessTokenTime.
    Indicates for how long the access token is valid in seconds.
  5. Fill out RefreshTokenTime.
    Indicate for how long the refresh token is valid in seconds.

Third party app sample

Next lets create the third party app that will access the news items in Sitefinity.

Here`s the code for the controller. Keep in mind that this is for demo only. You must always use secure network protocols such as TLS. Also this is a bare bones authroization example that does not take advantage of the built in ASP.NET Core helper methods for OAuth2 authentication. Hence you could use it in any kind of application with server side backend. For SPA applications use the implicit flow in a similar bare bones fashion or with JS library of your choice for OAuth2 authorization.

The controller checks for the authorization cookie`s presence and if not triggers authorization request to Sitefinity CMS. There the user is asked to authenticate and then returned to the callback path with the authroization code. There the third party app makes a request using the code to receive access_token for the user which is persisted in the authroization cookie. Finally the user is redirected back to list the news item using the access token stored in the cookie.

 

using System;
using System.Collections.Generic;
using System.Dynamic;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using System.Web;
using Microsoft.AspNetCore.Mvc;
using testApp1.Models;
namespace testApp1.Controllers
{
// This is bare bones use of OAuth2 authorization for the sake of demonstration that is not coupled to any framework
// Ideally each framework most probably has a library or internal implementation to ease the OAuth2 authorization
// for asp.net core helpers regarding OAuth2 authorization this is a good example: https://www.red-gate.com/simple-talk/development/dotnet-development/oauth-2-0-with-github-in-asp-net-core/
public class NewsController : Controller
{
private const string CookieName = "user_session";
private const string SitefinityAddress = "http://sf0:54507";
private const string TestAppAddress = "http://localhost:40982";
private const string ClientId = "testApp";
private const string Secret = "testsecret"; // do not hardcode secrets - use secure storage here, ex, get from environment variables
private readonly string CallbackUrl = $"{TestAppAddress}/callback";
public IActionResult Index()
{
string accessToken = null;
if (this.Request.Cookies.ContainsKey(CookieName))
{
accessToken = this.Unprotect(this.Request.Cookies[CookieName]);
}
else
{
// trigger authorization request to Sitefinity as described in https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.1
return this.Redirect($"{SitefinityAddress}/sitefinity/oauth/authorize?response_type=code&client_id={ClientId}&redirect_uri={HttpUtility.UrlEncode(CallbackUrl)}");
}
if (accessToken != null)
{
using (var client = GetClient())
{
client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("bearer", accessToken);
var response = client.SendAsync(new HttpRequestMessage(HttpMethod.Get, $"{SitefinityAddress}/api/default/newsitems")).Result;
response.EnsureSuccessStatusCode();
var responseBody = response.Content.ReadAsStringAsync().Result;
var model = JsonSerializer.Deserialize<JsonElement>(responseBody).GetProperty("value");
return View(model);
}
}
else
{
throw new Exception("Access token cannot be null or empy.");
}
}
[Route("callback")]
public IActionResult Callback()
{
var code = this.Request.Query["code"].First();
using (var client = GetClient())
{
client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Basic", Convert.ToBase64String(Encoding.GetEncoding("UTF-8").GetBytes($"{ClientId}:{Secret}")));
var requestMessage = new HttpRequestMessage(HttpMethod.Post, $"{SitefinityAddress}/sitefinity/oauth/token")
{
Content = new StringContent($"grant_type=authorization_code&code={code}&client_id={ClientId}&redirect_uri={HttpUtility.UrlEncode(CallbackUrl)}", Encoding.UTF8, "application/x-www-form-urlencoded"),
};
var response = client.SendAsync(requestMessage).Result;
response.EnsureSuccessStatusCode();
var responseBody = response.Content.ReadAsStringAsync().Result;
var responseJson = JsonSerializer.Deserialize<JsonElement>(responseBody).TryGetProperty("access_token", out var accessTokenElement);
var accessToken = accessTokenElement.GetString();
// also retrieve and persist refresh token server side to get new access token when it expires as to not make the user authenticate on Sitefinity again
// persist access_token for the current browser session, in this case cookies are used
this.Response.Cookies.Append(CookieName, this.Protect(accessToken), new Microsoft.AspNetCore.Http.CookieOptions()
{
HttpOnly = true,
SameSite = Microsoft.AspNetCore.Http.SameSiteMode.Strict,
// Always use secure channels, for sake of simplicity in the example insecure channels are used
// Secure = true
});
}
// get token and save to cookie
return this.Redirect(TestAppAddress + "/news");
}
private string Protect(string accessToken)
{
// TODO: Cookies should use some form of protection - usually a symmetrical encryption.
// It is recommended to use the built in data protection mechanisms of the framework
return accessToken;
}
private HttpClient GetClient()
{
var client = new HttpClient()
{
BaseAddress = new Uri(SitefinityAddress),
};
return client;
}
private string Unprotect(string value)
{
// TODO: Cookies should use some form of protection - usually a symmetrical encryption.
return value;
}
}
}

 

@model System.Text.Json.JsonElement
@foreach (var item in Model.EnumerateArray())
{
<div class="text-center">
<h1 class="display-4">@item.GetProperty("Title").GetString()</h1>
</div>
}
view raw callback.cshtml hosted with ❤ by GitHub

Implicit flow sample

This flow is recommended for applications that can run their logic only inside the user`s browser - Single Page Application (SPA)

First, register the SPA in Sitefinity CMS:

  1. Navigate to Administration » Settings » Advanced » Authentication » OAuthServer » AuthorizedClients
  2. Click Create new.
  3. Fill out the ClientID.
    For instance, SPAclient.

    NOTE: For implicit flow configuration, you don't need a secret.

  4. Click Save changes.
  5. Expand the newly created client.
  6. Click RedirectUrls.
  7. Click Create new.
  8. Enter the URL of the callback.html file.
    Use the following URL structure: https://<yourdomaim>/callback.html.
  9. Click Save changes.
  10. Navigate to Administration » Settings » Advanced » Security
  11. Fill out the AccessControlAllowOrigin field.
    This is to enable CORS for JS apps to be able to call Sitefinity CMS services. For this demo, type in * to enable all domains. This is not recommended for production environments as this could be a security concern.
  12.  Click Save changes.

Sample JavaScript app

The next sample shows how to  create a simple JS app that lists all the created news items in Sitefinity that a specific user has access to.

The app checks if there is an access token and if not, it redirects to Sitefinity CMS where the user is authenticated and an access token is returned to the callback.html page. It saves it in the local storage and returns the browser back to the original page where news items allowed for the current user are shown.
For logout functionality you should clear the token from local storage and then also logout from Sitefinity CMS using Sitefinity`s logout rest api. For more information, see Authentication sign out endpoint.

You can also take advantage of refresh tokens so as not to prompt the user again to authenticate if the current access token expires.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>OAuth2 client demo</title>
</head>
<body>
<div>
<input type="button" value="Load" id="button" />
</div>
<ul id="news"></ul>
</body>
</html>
<script>
const BASE_SITEFINITY_URL = "http://sf2:63197";
const SERVICE_URL = '/api/default/newsitems';
const CLIENTID = "SPAclient";
document.getElementById('button').addEventListener('click', function () {
getNews().then(function (result) {
var ul = document.getElementById('news');
ul.innerHTML = '';
for (var i = 0; i < result.value.length; i++) {
news = result.value[i];
ul.innerHTML += '<li><h3>' + news.Title + '</h3><em>' + news.PublicationDate + '</em><p>' + news.Content + '<p></li>';
}
}, function (error) {
console.log(error);
});
});
function getNews() {
const token = getToken();
return fetch(BASE_SITEFINITY_URL + SERVICE_URL, {
headers: {
'Authorization': token.type + ' ' + token.value
}
}).then(result => {
return result.json();
})
}
function getToken() {
const rawToken = window.localStorage.getItem("token")
if (rawToken) {
const token = JSON.parse(rawToken);
return token;
} else {
const currentBaseUrl = window.location.protocol + "//" + window.location.hostname + (window.location.port ? ':' + window.location.port : '');
// state parameter will be returned by the OAuth2 server when redirected back to the SPA,
// use it to keep session info
const redirectUri = encodeURIComponent(currentBaseUrl + "/callback.html");
const encodedReturnUrl = encodeURIComponent(encodeURIComponent(currentBaseUrl + "/index.html"));
const url = BASE_SITEFINITY_URL + `/sitefinity/oauth/authorize?response_type=token&client_id=${CLIENTID}&redirect_uri=${redirectUri}&state=${encodedReturnUrl}`;
// redirect to authorize endpoint in Siteifnity OAuth2 server
window.location.href = url;
}
}
</script>
view raw index.html hosted with ❤ by GitHub

 

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>Document</title>
</head>
<body>
</body>
</html>
<script>
const fragment = window.location.href.split("#")[1];
const params = fragment.split("&");
const parsedParams = {};
params.forEach(param => {
parsedParams[param.split("=")[0]] = param.split("=")[1];
});
const t = parsedParams["access_token"];
const expires = new Date();
expires.setSeconds(expires.getSeconds() + parsedParams["expires_in"] * 1000);
const returnUrl = decodeURIComponent(decodeURIComponent(parsedParams["state"]));
const token = {
value: t,
expirationTime: expires,
type: parsedParams["token_type"],
}
window.localStorage.setItem("token", JSON.stringify(token));
window.location.href = returnUrl;
</script>
view raw callback.html hosted with ❤ by GitHub

Want to learn more?

Increase your Sitefinity skills by signing up for our free trainings. Get Sitefinity-certified at Progress Education Community to boost your credentials.

Get started with Integration Hub | Sitefinity Cloud | Sitefinity SaaS

This free lesson teaches administrators, marketers, and other business professionals how to use the Integration hub service to create automated workflows between Sitefinity and other business systems.

Web Security for Sitefinity Administrators

This free lesson teaches administrators the basics about protecting yor Sitefinity instance and its sites from external threats. Configure HTTPS, SSL, allow lists for trusted sites, and cookie security, among others.

Foundations of Sitefinity ASP.NET Core Development

The free on-demand video course teaches developers how to use Sitefinity .NET Core and leverage its decoupled architecture and new way of coding against the platform.

Was this article helpful?