Having used a refresh token to get a new access token, I want to update my client side cookie with that access token.
My client is able to sign in and call my REST API using ajax, however when that first authorization expires, naturally the API calls no longer work.
I have a .NET web application which consumes its own REST API. The API is a part of the same project. It does not have its own startup configuration.
As the cookie is being sent in the header of each request it needs to have the new unexpired access token so that I don't get 'User unauthorized' for the request.
Right now I am able to get a new token using my refresh token but the value of the cookie has not changed, so I believe I need to update my cookie to reflect the new access token before the client sends any requests.
Here's a look at my hybrid client:
using IdentityModel.Client;
using Microsoft.Owin.Security;
using Microsoft.Owin.Security.Cookies;
using Microsoft.Owin.Security.OpenIdConnect;
using Owin;
using System;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Net;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
namespace Cts.HomeService.Web.App_Start
{
public class Startup
{
public void Configuration(IAppBuilder app)
{
var identityServerSection = (IdentityServerSectionHandler)System.Configuration.ConfigurationManager.GetSection("identityserversection");
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = "Cookies",
CookieManager = new Microsoft.Owin.Host.SystemWeb.SystemWebChunkingCookieManager()
});
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
ClientId = "localTestClient",
Authority = "http://localhost:5000",
RedirectUri = identityServerSection.Identity.RedirectUri,
Scope = "openid profile offline_access",
ResponseType = "code id_token",
RequireHttpsMetadata = false,
PostLogoutRedirectUri = identityServerSection.Identity.RedirectUri,
TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = "name",
RoleClaimType = "role",
},
SignInAsAuthenticationType = "Cookies",
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthorizationCodeReceived = async n =>
{
var tokenClient = new TokenClient(
"http://localhost:5000/connect/token",
"localTestClient",
"");
var tokenResponse = await tokenClient.RequestAuthorizationCodeAsync(
n.Code, n.RedirectUri);
if (tokenResponse.IsError)
{
throw new Exception(tokenResponse.Error);
}
// use the access token to retrieve claims from userinfo
var userInfoClient = new UserInfoClient(
"http://localhost:5000/connect/userinfo");
var userInfoResponse = await userInfoClient.GetAsync(tokenResponse.AccessToken);
// create new identity
var id = new ClaimsIdentity(n.AuthenticationTicket.Identity.AuthenticationType);
id.AddClaims(userInfoResponse.Claims);
id.AddClaim(new Claim("access_token", tokenResponse.AccessToken));
id.AddClaim(new Claim("expires_at", DateTime.Now.AddSeconds(tokenResponse.ExpiresIn).ToLocalTime().ToString()));
id.AddClaim(new Claim("refresh_token", tokenResponse.RefreshToken));
id.AddClaim(new Claim("id_token", tokenResponse.IdentityToken));
id.AddClaim(new Claim("sid", n.AuthenticationTicket.Identity.FindFirst("sid").Value));
n.AuthenticationTicket = new AuthenticationTicket(
new ClaimsIdentity(id.Claims, n.AuthenticationTicket.Identity.AuthenticationType, "name", "role"),
n.AuthenticationTicket.Properties);
},
RedirectToIdentityProvider = n =>
{
{
// so here I'll grab the access token
if (isAccessTokenExpired()) {
var cancellationToken = new CancellationToken();
var newAccessToken = context.GetNewAccessTokenAsync(refresh_token, null, cancellationToken);
// now what?
}
// if signing out, add the id_token_hint
if (n.ProtocolMessage.RequestType == OpenIdConnectRequestType.Logout)
{
var idTokenHint = n.OwinContext.Authentication.User.FindFirst("id_token");
if (idTokenHint != null)
{
n.ProtocolMessage.IdTokenHint = idTokenHint.Value;
}
}
return Task.FromResult(0);
}
}
}
});
}
}
}
I've looked into a lot of things but the value of my cookie always stays the same. I've considered deleting the old cookie and just building the new cookie manually, but that requires encrypting it the right way and it smells funny, surely not the idiomatic way to do it.
I feel there must be something simple I am missing. I would expect a simple "UpdateCookie(newToken)" kind of method and I have tried SignIn() and SignOut() but these have not worked out for me, seemingly not interacting with the cookie at all in fact.
To refresh the access token, select the Refresh access token API call within the Authorization folder of the Postman collection. Next, click the Send button to request a new access_token .
Go to Services > Azure Partner (NCE) > Manage Refresh Token. In the Automatic Update group, click Update Refresh Token. The login page of the Microsoft Partner Center will open in a new browser window.
It's not possible to get a refresh token from an access token. A refresh token can only be retrieved by authorizing the app via the OAuth app authorization flow. (The "Generate" button on an app's page on the App Console does not offer the ability to get a refresh token; that only returns an access token.)
This is how I got mine to work, add the following lines:
SecurityTokenValidated = context =>
{
context.AuthenticationTicket.Properties.AllowRefresh = true;
context.AuthenticationTicket.Properties.IsPersistent = true;
}
Then in AuthorizationCodeReceived add this to the end:
HttpContext.Current.GetOwinContext().Authentication.SignIn(new AuthenticationProperties
{
ExpiresUtc = DateTimeOffset.UtcNow.AddSeconds(tokenResponse.ExpiresIn),
AllowRefresh = true,
IssuedUtc = DateTime.UtcNow,
IsPersistent = true
}, newIdentity);
Where newIdentity is your claims identity, hope this helps.
I recently stuck with the same question and the solution is:
UseTokenLifetime = false
in OpenIdConnectAuthenticationOptions
used to configure OAuth middleware (otherwise session cookie lifetime will be set to access token lifetime, which is usually one hour)CookieAuthenticationProvider
that validates access token expirationIConfidentialClientApplication.AcquireTokenSilent()
method call)IIdentity
object with the acquired access token using ISecurityTokenValidator.ValidateToken()
methodIAuthenticationManager.SignIn(properties, freshIdentity)
to update the session cookieHere is the full solution to make refresh tokens work with OWIN cookie middleware:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Security.Principal;
using System.Threading;
using System.Threading.Tasks;
using EPiServer.Logging;
using Microsoft.Identity.Client;
using Microsoft.IdentityModel.Tokens;
using Microsoft.Owin.Host.SystemWeb;
using Microsoft.Owin.Security.Cookies;
using Microsoft.Owin.Security.OpenIdConnect;
using Owin;
namespace MyApp
{
public class OwinStartup
{
public void Configuration(IAppBuilder app)
{
var openIdConnectOptions = new OpenIdConnectAuthenticationOptions
{
UseTokenLifetime = false,
// ...
};
var msalAppBuilder = new MsalAppBuilder();
var refreshTokenHandler = new RefreshTokenHandler(msalAppBuilder, openIdConnectOptions);
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
CookieManager = new SystemWebChunkingCookieManager(),
Provider = new RefreshTokenCookieAuthenticationProvider(refreshTokenHandler)
});
}
}
public class RefreshTokenCookieAuthenticationProvider : CookieAuthenticationProvider
{
private readonly RefreshTokenHandler _refreshTokenHandler;
private static readonly ILogger _log = LogManager.GetLogger();
public RefreshTokenCookieAuthenticationProvider(RefreshTokenHandler refreshTokenHandler)
{
_refreshTokenHandler = refreshTokenHandler;
}
public override async Task ValidateIdentity(CookieValidateIdentityContext context)
{
var exp = context.Identity?.FindFirst("exp")?.Value;
if (string.IsNullOrEmpty(exp))
{
return;
}
var utcNow = DateTimeOffset.UtcNow;
var expiresUtc = DateTimeOffset.FromUnixTimeSeconds(long.Parse(exp));
var maxMinsBeforeExpires = TimeSpan.FromMinutes(2);
if (expiresUtc - utcNow >= maxMinsBeforeExpires)
{
return;
}
try
{
var freshIdentity = await _refreshTokenHandler.TryRefreshAccessTokenAsync(context.Identity);
if (freshIdentity != null)
{
context.ReplaceIdentity(freshIdentity);
context.OwinContext.Authentication.SignIn(context.Properties, (ClaimsIdentity) freshIdentity);
}
else
{
context.RejectIdentity();
}
}
catch (Exception ex)
{
_log.Error("Can't refresh user token", ex);
context.RejectIdentity();
}
}
}
public class RefreshTokenHandler
{
private readonly MsalAppBuilder _msalAppBuilder;
private readonly OpenIdConnectAuthenticationOptions _openIdConnectOptions;
public RefreshTokenHandler(
MsalAppBuilder msalAppBuilder,
OpenIdConnectAuthenticationOptions openIdConnectOptions)
{
_msalAppBuilder = msalAppBuilder;
_openIdConnectOptions = openIdConnectOptions;
}
public async Task<IIdentity> TryRefreshAccessTokenAsync(IIdentity identity, CancellationToken ct = default)
{
try
{
var idToken = await GetFreshIdTokenAsync(identity, ct);
var freshIdentity = await GetFreshIdentityAsync(idToken, ct);
return freshIdentity;
}
catch (MsalUiRequiredException)
{
return null;
}
}
private async Task<string> GetFreshIdTokenAsync(IIdentity identity, CancellationToken ct)
{
var principal = new ClaimsPrincipal(identity);
var app = _msalAppBuilder.BuildConfidentialClientApplication(principal);
var accounts = await app.GetAccountsAsync();
var result = await app.AcquireTokenSilent(new[] {"openid"}, accounts.FirstOrDefault()).ExecuteAsync(ct);
return result.IdToken;
}
private async Task<IIdentity> GetFreshIdentityAsync(string idToken, CancellationToken ct)
{
var validationParameters = await CreateTokenValidationParametersAsync(ct);
var principal = _openIdConnectOptions.SecurityTokenValidator.ValidateToken(idToken, validationParameters, out _);
var identity = (ClaimsIdentity) principal.Identity;
return identity;
}
// This is additional code for cases with multiple issuers - can be skipped if this configuration is static
private async Task<TokenValidationParameters> CreateTokenValidationParametersAsync(CancellationToken ct)
{
var validationParameters = _openIdConnectOptions.TokenValidationParameters.Clone();
var configuration = await _openIdConnectOptions.ConfigurationManager.GetConfigurationAsync(ct);
validationParameters.ValidIssuers = (validationParameters.ValidIssuers ?? new string[0])
.Union(new[] {configuration.Issuer})
.ToList();
validationParameters.IssuerSigningKeys = (validationParameters.IssuerSigningKeys ?? new SecurityKey[0])
.Union(configuration.SigningKeys)
.ToList();
return validationParameters;
}
}
// From official samples: https://github.com/Azure-Samples/active-directory-b2c-dotnet-webapp-and-webapi/blob/master/TaskWebApp/Utils/MsalAppBuilder.cs
public class MsalAppBuilder
{
public IConfidentialClientApplication BuildConfidentialClientApplication(ClaimsPrincipal currentUser)
{
// ...
}
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With