How do I update my cookie, having got a new access_token? - c#

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.

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:
Set 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)
Create your own CookieAuthenticationProvider that validates access token expiration
When token is expired (or close to be expired):
Get new access token using refresh token (if MSAL is used for OAuth - this is a simple IConfidentialClientApplication.AcquireTokenSilent() method call)
Build a fresh IIdentity object with the acquired access token using ISecurityTokenValidator.ValidateToken() method
Replace request context identity by the newly built identity
Call IAuthenticationManager.SignIn(properties, freshIdentity) to update the session cookie
Here 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)
{
// ...
}
}
}

Related

NET 6 WebAPI with Hotchocolate GraphQL not authorizing

I have a NET 6 api that I am using HotChocolate 12.7 in. My queries work, but when I am trying to add the [Authorize] decorator to a query, and send the request with the Bearer token, I am getting the unauthorized response. I can not get it to recognize a properly authenticated user's JWT.
Here is the Program.cs
using altitude_api.Entities;
using altitude_api.Models;
using altitude_api.Queries;
using altitude_api.Services;
using altitude_api.Services.Interfaces;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens;
using Serilog;
namespace altitude_api
{
public class Program
{
public static void Main(string[] args)
{
var configuration = new ConfigurationBuilder()
.AddJsonFile("appsettings.json")
.Build();
var key = configuration.GetSection("AppSettings").GetSection("SecretKey");
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthorization();
builder.Services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(opt =>
{
opt.SaveToken = true;
opt.RequireHttpsMetadata = false;
opt.TokenValidationParameters = new TokenValidationParameters()
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes((key.Value))),
};
});
builder.Services
.AddGraphQLServer()
.AddAuthorization()
.AddQueryType<Query>()
.AddTypeExtension<HealthQuery>();
var services = builder.Services;
// Add Services
var appSettingsSection = builder.Configuration.GetSection("AppSettings").Get<AppSettings>();
services.Configure<AppSettings>(configuration.GetSection("AppSettings"));
services.AddScoped<IAuthService, AuthService>();
var signinKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(key.Value));
Log.Logger = new LoggerConfiguration()
.ReadFrom.Configuration(configuration)
.CreateLogger();
Log.Information("App starting up");
services.AddAuthorization(options =>
{
options.DefaultPolicy = new AuthorizationPolicyBuilder(JwtBearerDefaults.AuthenticationScheme).RequireAuthenticatedUser().Build();
});
var allowedOrigins = appSettingsSection.AllowedOriginList;
services.AddDbContext<AltitudeContext>(
options => options.UseSqlServer(configuration.GetConnectionString("AltitudeContext")));
services.AddCors(options => options.AddPolicy("EnableCORS", build =>
{
build
.AllowAnyMethod()
.AllowAnyHeader()
.WithOrigins(allowedOrigins.ToString())
.AllowCredentials();
}));
var app = builder.Build();
// app.MapControllers();
app.UseRouting();
app.UseCors("EnableCORS");
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapGraphQL();
});
app.Run();
}
}
}
And here is the authentication code.
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using altitude_api.Entities;
using altitude_api.Models;
using altitude_api.Services.Interfaces;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using Novell.Directory.Ldap;
using Serilog;
namespace altitude_api.Services;
public class AuthService: IAuthService
{
private readonly AltitudeContext _altitudeContext;
private readonly AppSettings appSettings;
public AuthService(AltitudeContext altitudeContext, IOptions<AppSettings> _appSettings)
{
_altitudeContext = altitudeContext;
appSettings = _appSettings.Value;
}
public UserWithToken AuthenticateUser(string userName, string password)
{
var isValid = false;
var port = appSettings.LdapPort;
Log.Information($"Beginning to authenticate user {userName}");
using (var cn = new LdapConnection())
{
// connect
try
{
cn.Connect(appSettings.LdapServer, Convert.ToInt32(port));
cn.Bind(appSettings.ldapDomain + userName, password);
if (cn.Bound)
{
isValid = true;
Log.Information($"Successfully found user in LDAP {userName}");
}
}
catch (Exception ex)
{
Log.Error( ex,$"Error looking up {userName} in LDAP.");
throw ex;
}
}
return isValid ? GetToken(userName) : throw new Exception("Unable to authenticate user at this time");
}
public Users GetUser()
{
return _altitudeContext.Users.First(p => p.Username == "maxwell.sands");
}
public UserWithToken GetToken(string userName)
{
try
{
var roles = _altitudeContext.Roles;
var dbUser = _altitudeContext.Users.FirstOrDefault(p => p != null && p.Username == userName);
if (dbUser == null)
{
var ex = new Exception("User not found");
Log.Error(ex, "User is not found could not authenticate");
throw ex;
}
if(dbUser.ExpiryDttm < DateTime.Now)
{
var ex = new Exception("ERROR: User access expired.");
Log.Error(ex, "User is expired could not authenticate");
throw ex;
}
var role = (from rle in _altitudeContext.Roles
join m in _altitudeContext.UserRoles on rle.RoleId equals m.RoleId
join usr in _altitudeContext.Users on m.UserId equals usr.UserId
where usr.Username.ToLower() == dbUser.Username.ToLower()
select rle.RoleName).FirstOrDefault();
if (role == null)
{
var ex = new Exception("Role not found");
Log.Error(ex, "User is expired could not authenticate");
throw ex;
}
var secret = appSettings.SecretKey;
// authentication successful so generate jwt token
var tokenHandler = new JwtSecurityTokenHandler();
var key = Encoding.ASCII.GetBytes(secret);
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(new Claim[]
{
new Claim(ClaimTypes.Name, userName),
new Claim(ClaimTypes.Role, role),
}),
Expires = DateTime.UtcNow.AddDays(1),
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256)
};
var token = tokenHandler.CreateToken(tokenDescriptor);
UserWithToken webuser = new UserWithToken();
webuser.UserName = dbUser.Username;
webuser.FirstName = dbUser.FirstName;
webuser.LastName = dbUser.LastName;
webuser.Id = dbUser.UserId;
webuser.Role = role;
webuser.Token = tokenHandler.WriteToken(token);
Log.Information($"{webuser.FirstName} {webuser.LastName} was successfully logged in.");
return webuser;
}
catch(Exception e)
{
Log.Information(e, "There was an error loading the user");
throw new Exception("There was an issue loading the user", e);
}
}
}
And finally here is the endpoint I am require authorization on.
using HotChocolate.AspNetCore.Authorization;
namespace altitude_api.Queries;
[ExtendObjectType(typeof(Query))]
public class HealthQuery
{
[Authorize]
public bool HeartBeat()
{
return true;
}
}
In response to #Arjav Dave, yes I did find the answer to this, almost by dumb luck. The order of things in the Program.cs is very important. My code looks like the following...
builder.Services
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(options =>
{
// omitted
}).Services
.AddAuthorization();
builder.Services
.AddGraphQLServer()
.AddAuthorization()
.AddFiltering()
.AddSorting()
.AddQueryType<Query>()
and when I am setting up the app it looks like...
var app = builder.Build();
app.UseRouting();
app.UseCors("EnableCORS");
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapGraphQL();
});
app.Run();
Hope that helps.

AcquireTokenByAuthorizationCode throw new exception in single tenant application using ASP.NET MVC using Azure Active Directory

I tried this tutorial because I want to use the Microsoft Graph API to create massive teams in Microsoft Teams.
The only difference with the tutorial is that I used the next choice in Authentication section of Azure AD admin center:
"Accounts in this organizational directory only (myuniversity only - Single tenant)"
Because of this I changed my code to use endpoint for single tenant
public void ConfigureAuth(IAppBuilder app)
{
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions());
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
ClientId = appId,
//Authority = "https://login.microsoftonline.com/common/v2.0",//
Authority = "https://login.microsoftonline.com/{tenantid}/v2.0",
Scope = $"openid email profile offline_access {graphScopes}",
RedirectUri = redirectUri,
PostLogoutRedirectUri = redirectUri,
TokenValidationParameters = new TokenValidationParameters
{
// For demo purposes only, see below
ValidateIssuer = false
// In a real multi-tenant app, you would add logic to determine whether the
// issuer was from an authorized tenant
//ValidateIssuer = true,
//IssuerValidator = (issuer, token, tvp) =>
//{
// if (MyCustomTenantValidation(issuer))
// {
// return issuer;
// }
// else
// {
// throw new SecurityTokenInvalidIssuerException("Invalid issuer");
// }
//}
},
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthenticationFailed = OnAuthenticationFailedAsync,
AuthorizationCodeReceived = OnAuthorizationCodeReceivedAsync
}
}
);
}
After authentication of the user the code I run to OnAuthorizationCodeReceivedAsync method but got an exception in AcquireTokenByAuthorizationCode method
Here is the method
private async Task OnAuthorizationCodeReceivedAsync(AuthorizationCodeReceivedNotification notification)
{
var idClient = ConfidentialClientApplicationBuilder.Create(appId)
.WithRedirectUri(redirectUri)
.WithClientSecret(appSecret)
.Build();
var accounts = await idClient.GetAccountsAsync();
string message;
string debug;
try
{
string[] scopes = graphScopes.Split(' ');
var result = await idClient.AcquireTokenByAuthorizationCode(scopes, notification.Code).ExecuteAsync();
message = "Access token retrieved.";
debug = result.AccessToken;
}
catch (MsalException ex)
{
message = "AcquireTokenByAuthorizationCodeAsync threw an exception";
debug = ex.Message;
}
var queryString = $"message={message}&debug={debug}";
if (queryString.Length > 2048)
{
queryString = queryString.Substring(0, 2040) + "...";
}
notification.HandleResponse();
notification.Response.Redirect($"/Home/Error?{queryString}");
}
The exception is:
AcquireTokenByAuthorizationCodeAsync threw an exception
AADSTS50194: Application 'application id'(ASP.NET Graph Tutorial) is
not configured as a multi-tenant application. Usage of the /common
endpoint is not supported for such applications created after
'10/15/2018'. Use a tenant-specific endpoint or configure the
application to be multi-tenant. Trace ID:
5f0fbf2e-5d63-40d4-a833-ca8627a02d00
Correlation ID: 3ec4ec7b-0c86-4e2b-a053-9823f977499d Timestamp:
2021-02-16 20:21:03Z
I want to use single-tenant authentication for my organization only
If you want to require AD token from one specific tenant with MSAL.NET, you can tell the SDK from which tenant to obtain the token by mentioning the specific Authority. For more details, please refer to here.
For example
private static string appId = ConfigurationManager.AppSettings["ida:AppId"];
private static string appSecret = ConfigurationManager.AppSettings["ida:AppSecret"];
private static string redirectUri = ConfigurationManager.AppSettings["ida:RedirectUri"];
private static string graphScopes = ConfigurationManager.AppSettings["ida:AppScopes"];
public void ConfigureAuth(IAppBuilder app)
{
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions());
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
ClientId = appId,
Authority = "https://login.microsoftonline.com/<your tenant id>/v2.0",
Scope = $"openid email profile offline_access {graphScopes}",
RedirectUri = redirectUri,
PostLogoutRedirectUri = redirectUri,
TokenValidationParameters = new TokenValidationParameters
{
// For demo purposes only, see below
ValidateIssuer = false
// In a real multi-tenant app, you would add logic to determine whether the
// issuer was from an authorized tenant
//ValidateIssuer = true,
//IssuerValidator = (issuer, token, tvp) =>
//{
// if (MyCustomTenantValidation(issuer))
// {
// return issuer;
// }
// else
// {
// throw new SecurityTokenInvalidIssuerException("Invalid issuer");
// }
//}
},
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthenticationFailed = OnAuthenticationFailedAsync,
AuthorizationCodeReceived = OnAuthorizationCodeReceivedAsync
}
}
);
}
private async Task OnAuthorizationCodeReceivedAsync(AuthorizationCodeReceivedNotification notification)
{
var idClient = ConfidentialClientApplicationBuilder.Create(appId)
.WithRedirectUri(redirectUri)
.WithClientSecret(appSecret)
.WithAuthority("https://login.microsoftonline.com/<your tenant id>")
.Build();
string message;
string debug;
try
{
string[] scopes = graphScopes.Split(' ');
var result = await idClient.AcquireTokenByAuthorizationCode(
scopes, notification.Code).ExecuteAsync();
message = "Access token retrieved.";
debug = result.AccessToken;
}
catch (MsalException ex)
{
message = "AcquireTokenByAuthorizationCodeAsync threw an exception";
debug = ex.Message;
}
var queryString = $"message={message}&debug={debug}";
if (queryString.Length > 2048)
{
queryString = queryString.Substring(0, 2040) + "...";
}
notification.HandleResponse();
notification.Response.Redirect($"/Home/Error?{queryString}");
}
private Task OnAuthenticationFailedAsync(AuthenticationFailedNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions> notification)
{
notification.HandleResponse();
string redirect = $"/Home/Error?message={notification.Exception.Message}";
if (notification.ProtocolMessage != null && !string.IsNullOrEmpty(notification.ProtocolMessage.ErrorDescription))
{
redirect += $"&debug={notification.ProtocolMessage.ErrorDescription}";
}
notification.Response.Redirect(redirect);
return Task.FromResult(0);
}

Refresh token expired as soon as access token

I am implementing JWT refresh token, and setting different time expire for refresh token, but it is taking expire time same as access token
var refreshTokenId = Guid.NewGuid().ToString("n");
DateTime refreshTokenLifeTime = context.OwinContext.Get<DateTime>("as:clientRefreshTokenLifeTime");
To save in database
RefreshToken refreshToken = new RefreshToken();
refreshToken.Token = refreshTokenId;
refreshToken.PrivateKey = context.SerializeTicket();
refreshToken.ExpiryDate = refreshTokenLifeTime;
End saving Db
context.Ticket.Properties.IssuedUtc = DateTime.Now;
context.Ticket.Properties.ExpiresUtc = refreshTokenLifeTime;
context.SetToken(refreshTokenId);
context.SetToken(context.SerializeTicket());
Any help what I am doing wrong?
The refresh token does not extend the time of expiration, this is called sliding expiration and you cannot do it with access tokens. I have used the refresh token to update user Roles, not the expiration time.
Check this Link for Slidingexpiration
I used the below code to refresh token and persisting it
public class SimpleRefreshTokenProvider : IAuthenticationTokenProvider
{
public async Task CreateAsync(AuthenticationTokenCreateContext context)
{
var clientid = context.Ticket.Properties.Dictionary["as:client_id"];
if (string.IsNullOrEmpty(clientid))
{
return;
}
var refreshTokenId = Guid.NewGuid().ToString("n");
using (AuthRepository _repo = new AuthRepository())
{
var refreshTokenLifeTime = context.OwinContext.Get<string>("as:clientRefreshTokenLifeTime");
var token = new RefreshToken()
{
Id = Helper.GetHash(refreshTokenId),
ClientId = clientid,
Subject = context.Ticket.Identity.Name,
IssuedUtc = DateTime.UtcNow,
ExpiresUtc = DateTime.UtcNow.AddMinutes(Convert.ToDouble(refreshTokenLifeTime))
};
context.Ticket.Properties.IssuedUtc = token.IssuedUtc;
context.Ticket.Properties.ExpiresUtc = token.ExpiresUtc;
token.ProtectedTicket = context.SerializeTicket();
var result = await _repo.AddRefreshToken(token);
if (result)
{
context.SetToken(refreshTokenId);
}
}
}
public async Task ReceiveAsync(AuthenticationTokenReceiveContext context)
{
var allowedOrigin = context.OwinContext.Get<string>("as:clientAllowedOrigin");
context.OwinContext.Response.Headers.Add("Access-Control-Allow-Origin", new[] { allowedOrigin });
string hashedTokenId = Helper.GetHash(context.Token);
using (AuthRepository _repo = new AuthRepository())
{
var refreshToken = await _repo.FindRefreshToken(hashedTokenId);
if (refreshToken != null )
{
//Get protectedTicket from refreshToken class
context.DeserializeTicket(refreshToken.ProtectedTicket);
var result = await _repo.RemoveRefreshToken(hashedTokenId);
}
}
}
}
Now the request context contains all the claims stored previously for this user, and you need to add the logic which allows you to issue new claims or update the existing claims and contain them into the new access token generated before
you need the add the below code in the AuthorizationServerProvider Class you have.
public override Task GrantRefreshToken(OAuthGrantRefreshTokenContext context)
{
var originalClient = context.Ticket.Properties.Dictionary["as:client_id"];
var currentClient = context.ClientId;
if (originalClient != currentClient)
{
context.SetError("invalid_clientId", "Refresh token is issued to a different clientId.");
return Task.FromResult<object>(null);
}
// Change auth ticket for refresh token requests
var newIdentity = new ClaimsIdentity(context.Ticket.Identity);
newIdentity.AddClaim(new Claim("newClaim", "newValue"));
var newTicket = new AuthenticationTicket(newIdentity, context.Ticket.Properties);
context.Validated(newTicket);
return Task.FromResult<object>(null);
}
This is wrong
DateTime refreshTokenLifeTime = context.OwinContext.Get<DateTime>("as:clientRefreshTokenLifeTime");
you are reading the lifetime, not setting it to any new value.

How to add custom headers to an open id connect authentication request in OWIN middleware

I have a system where I have an MVC website calling a web api. I have used OAUTH and OPENID Connect for my authentication/authorization. I have setup an identity server in another web api project using Thinktecture's IdentityServer3. In the MVC project I am doing the redirect to the identity server in the OWIN Startup class. This is all pretty standard stuff and is working fine.
I have now been asked to put the web api and the identity server behind Azure API Management. This also, is fine as all I need to do from my MVC project is add my subscription key from API management as a header ("Ocp-Apim-Subscription-Key") to any request to the web api. So far so good.
The problem is I now need to add this header to any requests to the identity server and I can't work out how, other than writing my own middleware. My Startup class looks like this:
public class Startup
{
public void Configuration(IAppBuilder app)
{
DependencyResolver.SetResolver(new SimpleInjectorDependencyResolver(IocHelper.GetContainer()));
JwtSecurityTokenHandler.InboundClaimTypeMap = new Dictionary<string, string>();
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = "Cookies"
});
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
ClientId = ConfigurationHelper.GetClientId(),
Authority = ConfigurationHelper.GetSecurityTokenServiceUrl(),
RedirectUri = ConfigurationHelper.GetPortalHomePageUrl(),
PostLogoutRedirectUri = ConfigurationHelper.GetPortalHomePageUrl(),
ResponseType = "code id_token",
Scope = "openid profile public_api",
TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = "name",
RoleClaimType = "role"
},
SignInAsAuthenticationType = "Cookies",
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthorizationCodeReceived = async n =>
{
// use the code to get the access and refresh token
var tokenClient = new TokenClient(ConfigurationHelper.GetTokenEndpointUrl(), ConfigurationHelper.GetClientId(), ConfigurationHelper.GetClientSecret());
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(ConfigurationHelper.GetUserInfoUrl());
var userInfoResponse = await userInfoClient.GetAsync(tokenResponse.AccessToken);
if (userInfoResponse.IsError)
{
throw new Exception(userInfoResponse.Error);
}
// create new identity
var id = new ClaimsIdentity(n.AuthenticationTicket.Identity.AuthenticationType);
foreach (var c in userInfoResponse.Claims)
{
id.AddClaim(new Claim(c.Type, c.Value));
}
id.AddClaim(new Claim("access_token", tokenResponse.AccessToken));
id.AddClaim(new Claim("expires_at", DateTime.Now.AddSeconds(tokenResponse.ExpiresIn * 2).ToLocalTime().ToString(CultureInfo.InvariantCulture)));
id.AddClaim(new Claim("id_token", n.ProtocolMessage.IdToken));
id.AddClaim(new Claim("sid", n.AuthenticationTicket.Identity.FindFirst("sid").Value));
var claimsIdentity = new ClaimsIdentity(id.Claims, n.AuthenticationTicket.Identity.AuthenticationType, "name", "role");
//claimsIdentity.IsAuthenticated = true;
n.AuthenticationTicket = new AuthenticationTicket(claimsIdentity, n.AuthenticationTicket.Properties);
},
RedirectToIdentityProvider = n =>
{
// if signing out, add the id_token_hint
if (n.ProtocolMessage.RequestType != OpenIdConnectRequestType.LogoutRequest)
{
return Task.FromResult(0);
}
var idTokenHint = n.OwinContext.Authentication.User.FindFirst("id_token");
if (idTokenHint != null)
{
n.ProtocolMessage.IdTokenHint = idTokenHint.Value;
}
// DOESN'T WORK
n.OwinContext.Request.Headers.Append("Ocp-Apim-Subscription-Key", "MY KEY GOES HERE");
// ALSO DOESN'T WORK
n.Request.Headers.Append("Ocp-Apim-Subscription-Key", "MY KEY GOES HERE");
return Task.FromResult(0);
}
}
});
}
}
Is there a way to hi-jack the request to the identity server and add my own header in there?

Token received from authentication context not working

I am setting up a multi tenant application and I am having issues creating a GraphServiceClient.
I have to following AuthorizationCodeReceived:
AuthorizationCodeReceived = async context =>
{
var tenantId =
context.AuthenticationTicket.Identity.FindFirst("http://schemas.microsoft.com/identity/claims/tenantid").Value;
var authenticationContext = new AuthenticationContext("https://login.microsoftonline.com/"+ tenantId);
await authenticationContext.AcquireTokenByAuthorizationCodeAsync(
context.Code,
new Uri("http://localhost:21925"),
new ClientCredential(ClientId, ClientSecret),
"https://graph.microsoft.com");
}
This works perfectly to authenticate the user. I am using fiddler, and I see that a new bearer token was given by login.microsoftonline.com/{tenantid}/oauth2/token
When creating a new Graph Service Client I use the following factory method:
public IGraphServiceClient CreateGraphServiceClient()
{
var client = new GraphServiceClient(
new DelegateAuthenticationProvider(
async requestMessage =>
{
string token;
var currentUserId = ClaimsPrincipal.Current.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier").Value;
var currentUserHomeTenantId = ClaimsPrincipal.Current.FindFirst("http://schemas.microsoft.com/identity/claims/tenantid").Value;
var authenticationContext = new AuthenticationContext("https://login.microsoftonline.com/" + currentUserHomeTenantId + "/");
var clientCredential = new ClientCredential(_configuration.ClientId, _configuration.ClientSecret);
try
{
var authenticationResult = await authenticationContext.AcquireTokenSilentAsync(
GraphResourceId,
clientCredential,
new UserIdentifier(currentUserId, UserIdentifierType.UniqueId));
token = authenticationResult.AccessToken;
}
catch (AdalSilentTokenAcquisitionException e)
{
var result = await authenticationContext.AcquireTokenAsync(GraphResourceId, clientCredential);
token = result.AccessToken;
}
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("bearer", token);
}));
return client;
}
This method always throws an AdalSilentAcquisitionException and the AcquireTokenAsync retrieves a new token.
With this token, I am not able to request 'Me' on the graph.
I get the following exception: message=Resource 'some guid' does not exist or one of its queried reference-property objects are not present.
However, if I am debugging and I change the token before it is passed to the header, with the value of the one I got previously right after login in (received from login.microsoftonline.com/{tenantid}/oauth2/token ) then the API call works.
Does anyone know what I am doing wrong? He can I get the acquiretokensilently working?
Update: I have updated the code samples. I have removed the custom cache, and now everything seems to work.
How can I make a custom cache based on the http sessions, making sure the AcquireTokenSilently works.
Preview of not working token cache:
public class WebTokenCache : TokenCache
{
private readonly HttpContext _httpContext;
private readonly string _cacheKey;
public WebTokenCache()
{
_httpContext = HttpContext.Current;
var claimsPrincipal = (ClaimsPrincipal) HttpContext.Current.User;
_cacheKey = BuildCacheKey(claimsPrincipal);
AfterAccess = AfterAccessNotification;
LoadFromCache();
}
private string BuildCacheKey(ClaimsPrincipal claimsPrincipal)
{
var clientId = claimsPrincipal.FindFirst("aud").Value;
return $"{claimsPrincipal.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier").Value}_TokenCache";
}
private void LoadFromCache()
{
var token = _httpContext.Cache[_cacheKey];
if (token == null) return;
Deserialize((byte[]) token);
}
private void AfterAccessNotification(TokenCacheNotificationArgs args)
{
if (!HasStateChanged) return;
if (Count > 0)
{
_httpContext.Cache[_cacheKey] = Serialize();
}
else
{
_httpContext.Cache.Remove(_cacheKey);
}
HasStateChanged = false;
}
}
I am trying use the code above and it works well form me.
Please ensure that the GraphResourceId is https://graph.microsoft.com(This resource is requested first time in your startUp class) since the method AcquireTokenSilentAsync will try to retrieve the token from cache based on the resrouce.

Categories

Resources