Sitecore Experience Manager

Use bearer tokens in client applications

Abstract

Sitecore Identity provides a mechanism for Sitecore login.

This topic describes how you use bearer token authentication and the Sitecore Identity server to securely access an API from a MVC client.

Bearer token authentication involves three things:

  • The Sitecore Identity (SI) server. The SI server issues access tokens in JWT (JSON Web Token) format by default. Every relevant platform today has support for validating JWT tokens. You can consider access and bearer token as the same thing.

  • An API application.

  • An MVC client application. The application requests an access token from the SI server and uses it to gain access to the API.

The procedures described in this topic use an example that is based on these assumptions:

  • The SI server runs on https://localhost:44356/. Change this to your actual URL if you use the examples.

  • The MVC client has MvcClient as id and it is configured on the SI Server in the following way:

    <?xml version="1.0" encoding="utf-8"?>
    <Settings>
      <Sitecore>
        <IdentityServer>
          <Clients>
            <SampleMvcClient>
              <ClientId>MvcClient</ClientId>
              <ClientName>Sample MVC client</ClientName>
              <AccessTokenType>0</AccessTokenType>
              <AllowOfflineAccess>true</AllowOfflineAccess>
              <AlwaysIncludeUserClaimsInIdToken>true</AlwaysIncludeUserClaimsInIdToken>
              <AccessTokenLifetimeInSeconds>120</AccessTokenLifetimeInSeconds>
              <IdentityTokenLifetimeInSeconds>120</IdentityTokenLifetimeInSeconds>
              <AllowAccessTokensViaBrowser>true</AllowAccessTokensViaBrowser>
              <RequireConsent>false</RequireConsent>
              <RequireClientSecret>false</RequireClientSecret>
              <AllowedGrantTypes>
                <AllowedGrantType1>client_credentials</AllowedGrantType1>
                <AllowedGrantType2>hybrid</AllowedGrantType2>
              </AllowedGrantTypes>
              <RedirectUris>
                <RedirectUri1>{AllowedCorsOrigin}/signin-oidc</RedirectUri1>
              </RedirectUris>
              <PostLogoutRedirectUris>
                <PostLogoutRedirectUri1>{AllowedCorsOrigin}/signout-callback-oidc</PostLogoutRedirectUri1>
              </PostLogoutRedirectUris>
              <AllowedCorsOrigins>
                <AllowedCorsOriginsGroup1>http://localhost:54567</AllowedCorsOriginsGroup1>
              </AllowedCorsOrigins>
              <AllowedScopes>
                <AllowedScope1>openid</AllowedScope1>
                <AllowedScope2>sitecore.profile</AllowedScope2>
                <AllowedScope3>sitecore.profile.api</AllowedScope3>
              </AllowedScopes>
              <UpdateAccessTokenClaimsOnRefresh>true</UpdateAccessTokenClaimsOnRefresh>
            </SampleMvcClient>
          </Clients>
        </IdentityServer>
      </Sitecore>
    </Settings>
    

This topic describes how to:

Protect the API

This section outlines the basic scenario of protecting an API, using the SI server and bearer token authentication. Protecting a ASP.NET Core-based API is only a matter of configuring the JWT bearer authentication handler in DI, and adding the authentication middleware to the pipeline.

To protect the API:

  1. Use the ASP.NET Core Web API template to create a new project in Visual Studio, and configure the application URL in the launch profile. The example used here assumes that you configure the API with http://localhost:55600/ as the URL. Replace this URL with the URL you actually use in your own solution.

  2. Add package references to the project:

    <Project Sdk="Microsoft.NET.Sdk.Web">
      <PropertyGroup>
        <TargetFramework>netcoreapp2.1</TargetFramework>
      </PropertyGroup>
      <ItemGroup>
        <PackageReference Include="Microsoft.AspNetCore.App" />
      </ItemGroup>
    </Project>
  3. Configure the Startup class:

    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvcCore()
                .AddAuthorization()
                .AddJsonFormatters();
    
            services.AddAuthentication("Bearer")
                .AddJwtBearer("Bearer", options =>
                {
                    options.Authority = "https://localhost:44356";
                    options.RequireHttpsMetadata = false;
    
                    options.Audience = "sitecore.profile.api";
                });
        }
    
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            app.UseAuthentication();
            app.UseMvc();
        }
    }
    

    The AddAuthentication method adds the authentication services and configures Bearer as the default scheme. The AddJwtBearer method adds the SI Server access token validation handler so that the authentication services can use it. The UseAuthentication method adds the authentication middleware to the pipeline, so that authentication is performed automatically for every call into the host.

  4. Add a new controller to your API project:

    [Route("[controller]")]
    [Authorize]
    public class IdentityController : ControllerBase
    {
        [HttpGet]
        public IActionResult Get()
        {
            return new JsonResult(from c in User.Claims
                select new
                {
                    c.Type,
                    c.Value
                });
        }
    }
    

If you navigate to the controller (http://localhost:55600/identity), you get a 401 status code in return. This shows that your API requires a credential and is protected by the SI Server.

Configure the MVC client

To configure the MVC client to use the protected API:

  1. Configure the application URL in the launch profile.The following example assumes that you have configured the API with http://localhost:54567/ as the URL. Replace this URL with the URL you actually use in your own solution in the AllowedCorsOriginsGroup1 node in the SI server client configuration.

  2. Add package references to the project:

    <Project Sdk="Microsoft.NET.Sdk.Web">
      <PropertyGroup>
        <TargetFramework>netcoreapp2.1</TargetFramework>
      </PropertyGroup>
    
      <ItemGroup>
        <PackageReference Include="Microsoft.AspNetCore.All" Version="[2.1.3]" />
        <PackageReference Include="Microsoft.AspNetCore.Authentication.Abstractions" Version="[2.1.1.0]" />
        <PackageReference Include="IdentityModel" Version="[3.8.0]" />
      </ItemGroup>
    </Project>
    
  3. In the ConfigureServices method in the Startup class of the MVC application, add support for cookies and OpenID Connect authentication:

    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc();
    
            services.AddAuthentication(options =>
                {
                    options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
                    options.DefaultChallengeScheme = "oidc";
                })
                .AddCookie(options =>
                {
                    options.ExpireTimeSpan = TimeSpan.FromMinutes(60);
                    options.Cookie.Name = "mvcimplicit";
                })
                .AddOpenIdConnect("oidc", options =>
                {
                    options.ClientId = "MvcClient";
                    options.Authority = "https://localhost:44356";
                    options.RequireHttpsMetadata = false;
                    options.GetClaimsFromUserInfoEndpoint = true;
                    options.ResponseType = "code token";
    
                    options.Scope.Clear();
                    options.Scope.Add("openid");
                    options.Scope.Add("sitecore.profile");
                    options.Scope.Add("offline_access");
                    options.Scope.Add("sitecore.profile.api");
    
                    options.SaveTokens = true;
    
                    options.TokenValidationParameters = new TokenValidationParameters
                    {
                        NameClaimType = JwtClaimTypes.Name,
                        RoleClaimType = JwtClaimTypes.Role,
                    };
                });
        }
    
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            app.UseDeveloperExceptionPage();
            app.UseStaticFiles();
            app.UseAuthentication();
            app.UseMvcWithDefaultRoute();
        }
    }
    

    The AddAuthentication method adds authentication services. A cookie is the primary way to authenticate a user because Cookies is specified as the DefaultScheme. Because the DefaultChallengeScheme is specified as oidc, when the user logins, the OpenID Connect scheme has to be used.

    The AddCookie method adds the handler that can process cookies.

    The AddOpenIdConnect method configures the handler that performs the OpenID Connect protocol. The Authority property specifies that the SI server is trusted. You can identify this client with the ClientId property. The SignInScheme method issues a cookie, using the cookie handler, once the OpenID Connect protocol is complete. The SaveTokens method persists the tokens from SI server in the cookie (you need them later). Specify the ResponseType property as code token (this effectively means to use hybrid flow).

  4. Call the UseAuthentication method in the Configure method to make sure that the authentication services execute for each request. You must add the authentication middleware before MVC in the pipeline.

  5. To trigger the authentication handshake, add the [Authorize] attribute to the Home controller:

    [Authorize]
    public class Home : Controller
    {
        private static readonly HttpClient HttpClient = new HttpClient();
     
        public async Task<IActionResult> Index()
        {
            string accessToken = await HttpContext.GetTokenAsync("access_token");
            string refreshToken = await HttpContext.GetTokenAsync("refresh_token");
    
            return Content($"Current user: <span id=\"UserIdentityName\">{User.Identity.Name ?? "anonymous"}</span><br/>" +
                $"<div>Access token: {accessToken}</div><br/>" +
                $"<div>Refresh token: {refreshToken}</div><br/>"
                , "text/html");
        }
    }
    
    

    If you now navigate to this controller in a browser, a redirect attempt is made to SI server and authentication if succesful, this page is shown:

    Current user: sitecore\Admin
    Access token: eyJhbG......L8A
    Refresh token: 4cdf3b4d873a65135553afdf420a47dbc898ba0c1c0ece2407bbbf2bde02a68b
    
  6. Add this action to the Home controller to call the API with a bearer token:

    [Route("/callapi")]
        public async Task<IActionResult> CallApi()
        {
            string accessToken = await HttpContext.GetTokenAsync("access_token");
    
            var request = new HttpRequestMessage(HttpMethod.Get, "http://localhost:55600/identity");
            request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
            HttpResponseMessage response = await HttpClient.SendAsync(request);
    
            if (response.StatusCode != HttpStatusCode.OK)
            {
                return Content(response.ToString());
            }
    
            return Content($"{await response.Content.ReadAsStringAsync()}");
        }
    
  7. You obtain a bearer (access) token from the HttpContext with the GetTokenAsync method by passing the access_token argument. This is how you add the access token to the request header:

    request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
    

    Navigate to http://localhost:54567/callapi url. The answer should be like the following:

    [
    {
    "type": "nbf",
    "value": "1543572239"
    },
    {
    "type": "exp",
    "value": "1543572359"
    },
    {
    "type": "iss",
    "value": "https://localhost:44356"
    },
    {
    "type": "aud",
    "value": "https://localhost:44356/resources"
    },
    {
    "type": "aud",
    "value": "sitecore.profile.api"
    },
    {
    "type": "client_id",
    "value": "MvcClient"
    },
    {
    "type": "name",
    "value": "sitecore\\Admin"
    },
    ........
    

You can change the calling URL (http://localhost:55600/identity) to call another API, for example the Sitecore item service:

{
"ItemID": "110d559f-dea5-42ea-9c1c-8a5df7e70ef9",
"ItemName": "Home",
"ItemPath": "/sitecore/content/Home",
"ParentID": "0de95ae4-41ab-4d01-9eb0-67441b7c2450",
"TemplateID": "76036f5e-cbce-46d1-af0a-4143f9b557aa",
"TemplateName": "Sample Item",
"CloneSource": null,
"ItemLanguage": "en",
"ItemVersion": "1",
"DisplayName": "Home",
"HasChildren": "False",
"ItemIcon": "/temp/iconcache/network/16x16/home.png",
"ItemMedialUrl": "/-/icon/Network/48x48/home.png.aspx",
"ItemUrl": "~/link.aspx?_id=110D559FDEA542EA9C1C8A5DF7E70EF9&amp;amp;_z=z",
"Text": "&lt;p style.......e&lt;/a&gt;&lt;/p&gt;\r",
"Title": "Sitecore Experience Platform"
}

After some time (specified as the AccessTokenLifetimeInSeconds parameter in the client configuration on the SI server), you get a result like the following:

StatusCode: 401, ReasonPhrase:
'Unauthorized', Version: 1.1, Content:
System.Net.Http.HttpConnection+HttpConnectionResponseContent, Headers:
{
  Server:
Kestrel
 
WWW-Authenticate: Bearer error="invalid_token",
error_description="The token is expired"
  X-SourceFiles:
=?UTF-8?B?Qzpcclxfd1xpc1xzYW1wbGVzXEFwaVxpZGVudGl0eQ==?=
  X-Powered-By:
ASP.NET
  Date: Fri, 30
Nov 2018 07:42:20 GMT
 
Content-Length: 0
}

That means the access token expired and you need to get a new one. The next section describes how to do this.

Exchange refresh and access tokens

The SI server has a token endpoint you use to request tokens programmatically. The server supports a subset of the OpenID Connect and OAuth 2.0 token request parameters. The OpenID documentation has a full list.

To requests tokens:

  1. Add the following action to the home controller:

    [Route("/exchange")]
    public async Task<IActionResult> Exchange()
    {
        var disco = await DiscoveryClient.GetAsync("https://localhost:44356");
        if (disco.IsError) throw new Exception(disco.Error);
    
        var tokenClient = new TokenClient(disco.TokenEndpoint, "MvcClient", "secret");
        var rt = await HttpContext.GetTokenAsync("refresh_token");
        var tokenResult = await tokenClient.RequestRefreshTokenAsync(rt);
    
        if (!tokenResult.IsError)
        {
            var expiresAt = (DateTime.UtcNow + TimeSpan.FromSeconds(tokenResult.ExpiresIn)).ToString("o", CultureInfo.InvariantCulture);
    
            var authService = HttpContext.RequestServices.GetRequiredService<IAuthenticationService>();
            AuthenticateResult authenticateResult = await authService.AuthenticateAsync(HttpContext, null);
            AuthenticationProperties properties = authenticateResult.Properties;
    
            properties.UpdateTokenValue(OpenIdConnectParameterNames.RefreshToken, tokenResult.RefreshToken);
            properties.UpdateTokenValue(OpenIdConnectParameterNames.AccessToken, tokenResult.AccessToken);
            properties.UpdateTokenValue(OpenIdConnectParameterNames.ExpiresIn, expiresAt);
    
            await authService.SignInAsync(HttpContext, null, authenticateResult.Principal, authenticateResult.Properties);
    
            return Redirect("/");
        }
    
        return BadRequest();
    }
    

    You use an instance of the TokenClient class to request new tokens from the SI server. You need to have a valid refresh token before this request. After a successful request (RequestRefreshTokenAsync), this refresh token is invalidated and you have to update refresh and access tokens in the cookie with new ones.

  2. Call the AuthenticateAsync method to obtain authentication properties. The UpdateTokenValue method updates the tokens and also the expiration timestamp in the properties, and finally the SignInAsync method saves the authentication cookie.

Now the GetTokenAsync method returns updated access or refresh tokens.