Use bearer tokens in client applications
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:
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.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>
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 configuresBearer
as the default scheme. TheAddJwtBearer
method adds the SI Server access token validation handler so that the authentication services can use it. TheUseAuthentication
method adds the authentication middleware to the pipeline, so that authentication is performed automatically for every call into the host.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:
Configure the application URL in the launch profile. The following example assumes that you have configured the MVC client with
http://localhost:54567/
as the URL. Replace this URL with the URL you actually use in your own solution in theAllowedCorsOriginsGroup1
node in the SI server client configuration.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>
In the
ConfigureServices
method in theStartup
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 becauseCookies
is specified as the DefaultScheme. Because theDefaultChallengeScheme
is specified asoidc
, when the user logs in, 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. TheAuthority
property specifies that the SI server is trusted. You can identify this client with theClientId
property. The SignInScheme method issues a cookie, using the cookie handler, once the OpenID Connect protocol is complete. TheSaveTokens
method persists the tokens from SI server in the cookie (you need them later). Specify theResponseType
property ascode token
(this effectively means to use hybrid flow).Call the
UseAuthentication
method in theConfigure
method to make sure that the authentication services execute for each request. You must add the authentication middleware before MVC in the pipeline.To trigger the authentication handshake, add the
[Authorize]
attribute to theHome
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, if authentication is succesful, this page is shown:
Current user: sitecore\Admin Access token: eyJhbG......L8A Refresh token: 4cdf3b4d873a65135553afdf420a47dbc898ba0c1c0ece2407bbbf2bde02a68b
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()}"); }
You obtain a bearer (access) token from the
HttpContext
with theGetTokenAsync
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 will be like this:[ { "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;_z=z", "Text": "<p style.......e</a></p>\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:
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.Call the
AuthenticateAsync
method to obtain authentication properties. TheUpdateTokenValue
method updates the tokens and also the expiration timestamp in the properties, and finally theSignInAsync
method saves the authentication cookie.
Now the GetTokenAsync
method returns updated access or refresh tokens.