Token received from authentication context not working - c#

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.

Related

.Net 6 API - Certificate Authentication - Not working 403 response received

I am working on an Azure Function that needs to send a certificate to an API that will authenticate the request and return the data accordingly.
I am successfully getting the certificate from Azure Key Vault as a X509Certificate2. This is making a call (will be making several) to my API I am getting a 403 response.
I have followed this configuration https://learn.microsoft.com/en-us/aspnet/core/security/authentication/certauth?view=aspnetcore-6.0 and my code currently looks like this
-- Program.cs
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
builder.Services.Configure<KestrelServerOptions>(options =>
{
options.ConfigureHttpsDefaults(options =>
options.ClientCertificateMode = ClientCertificateMode.AllowCertificate);
});
builder.Services.AddAuthentication(
CertificateAuthenticationDefaults.AuthenticationScheme)
.AddCertificate(options =>
{
options.Events = new CertificateAuthenticationEvents
{
OnCertificateValidated = context =>
{
var validationService = context.HttpContext.RequestServices
.GetRequiredService<ICertificateValidationService>();
if (validationService.ValidateCertificate(context.ClientCertificate).Result)
{
var claims = new[]
{
new Claim(
ClaimTypes.NameIdentifier,
context.ClientCertificate.Subject,
ClaimValueTypes.String, context.Options.ClaimsIssuer),
new Claim(
ClaimTypes.Name,
context.ClientCertificate.Subject,
ClaimValueTypes.String, context.Options.ClaimsIssuer)
};
context.Principal = new ClaimsPrincipal(
new ClaimsIdentity(claims, context.Scheme.Name));
context.Success();
}
return Task.CompletedTask;
},
OnAuthenticationFailed = failedContext =>
{
failedContext.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
return Task.CompletedTask;
}
};
});
-- Validation Service
public class CertificateValidationService : ICertificateValidationService
{
private readonly IKeyVaultCertificateService _keyVaultService;
private readonly KeyVaultConfiguration _keyVauyltConfiguration;
public CertificateValidationService(IKeyVaultCertificateService keyVaultService, IOptions<KeyVaultConfiguration> keyVauyltConfiguration)
{
_keyVaultService = keyVaultService;
_keyVauyltConfiguration = keyVauyltConfiguration.Value;
}
public async Task<bool> ValidateCertificate(X509Certificate2 clientCertificate)
{
X509Certificate2 expectedCertificate = await _keyVaultService.GetX509CertificateAsync(_keyVauyltConfiguration.CertificateName);
return clientCertificate.Thumbprint == expectedCertificate.Thumbprint;
}
}
-- Controller
[Authorize(AuthenticationSchemes = CertificateAuthenticationDefaults.AuthenticationScheme)]
public IActionResult GetCount()
{
/// removed
}
When the request is made I am not hitting the method ValidateCertificate and I am just getting a 403 response.
I am having the same when I make a request through Postman and send the certificate in the request there too.
I would be grateful if someone could help me as I am stuck without success right now
--- Edit 1
_client = new HttpClient();
if (_erpConfiguration.UserCertificateAuthentication)
{
X509Certificate2 certificate = _keyVaultCertificateService
.GetX509CertificateAsync(_keyVaultConfiguration.CertificateName).Result;
var handler = new HttpClientHandler();
handler.ClientCertificates.Add(certificate);
_client = new HttpClient(handler);
}
var countHttpResponse = await _client.GetAsync(fullUri);
--- End Edit 1
--- Edit 2
I am getting the certificate from KeyVault like this
public async Task<KeyVaultCertificateWithPolicy> GetCertificateWithPolicyAsync(string certificateName)
{
Response<KeyVaultCertificateWithPolicy>? certificate = await _certificateClient.GetCertificateAsync(certificateName);
return certificate;
}
public async Task<X509Certificate2> GetX509CertificateAsync(string certificateName)
{
KeyVaultCertificateWithPolicy certificate = await GetCertificateWithPolicyAsync(certificateName);
var certContent = certificate.Cer;
return new X509Certificate2(certContent);
}
--- End Edit 2
--- Edit 3
public async Task<X509Certificate2> GetX509CertificateAsync(string certificateName)
{
KeyVaultCertificateWithPolicy certificate = await GetCertificateWithPolicyAsync(certificateName);
// Return a certificate with only the public key if the private key is not exportable.
if (certificate.Policy?.Exportable != true)
{
return new X509Certificate2(certificate.Cer);
}
// Parse the secret ID and version to retrieve the private key.
string[] segments = certificate.SecretId.AbsolutePath.Split('/', StringSplitOptions.RemoveEmptyEntries);
if (segments.Length != 3)
{
throw new InvalidOperationException($"Number of segments is incorrect: {segments.Length}, URI: {certificate.SecretId}");
}
string secretName = segments[1];
string secretVersion = segments[2];
KeyVaultSecret secret = await _secretClient.GetSecretAsync(secretName, secretVersion, CancellationToken.None);
// For PEM, you'll need to extract the base64-encoded message body.
if (!"application/x-pkcs12".Equals(secret.Properties.ContentType,
StringComparison.InvariantCultureIgnoreCase))
{
throw new VerificationException("Unable to validate certificate");
}
byte[] pfx = Convert.FromBase64String(secret.Value);
return new X509Certificate2(pfx);
}
--- End Edit 3

Cookies are not refreshed in .NET Core 3.1

I have an MVC .NET Core 3.1 application that uses Open ID connect for authentication and stores identity & tokens in cookies. The tokens need to be refreshed as they are used in some API requests our application does. I subscribe to ValidatePrincipal event and refresh the tokens there. The request goes OK, but cookies are not updated for some reason.
Startup.cs:
services.Configure<CookiePolicyOptions>(options =>
{
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
options.OnAppendCookie = (e) =>
{
e.CookieOptions.Domain = <some domain>
};
});
...
.AddCookie(options =>
{
options.ExpireTimeSpan = TimeSpan.FromDays(30);
options.Cookie.Domain = <some domain>;
options.Cookie.IsEssential = true;
options.Cookie.Name = <some name>
options.EventsType = typeof(CookieAuthEvents);
})
CookieAuthEvents.cs (constructor, member declarations and logging are omitted):
public override async Task ValidatePrincipal(CookieValidatePrincipalContext context)
{
if (context.Principal.Identity.IsAuthenticated)
{
var now = DateTime.UtcNow;
var tokenExpiration = context.Properties.GetTokenExpiration();
if (now > tokenExpiration)
{
await UpdateCookies(context);
}
}
}
private async Task UpdateCookies(CookieValidatePrincipalContext context)
{
var refreshToken = context.Properties.GetTokenValue(OpenIdConnectGrantTypes.RefreshToken);
if (String.IsNullOrEmpty(refreshToken))
{
return;
}
var response = await GetTokenClient().RequestRefreshTokenAsync(refreshToken);
if (!response.IsError)
{
WriteCookies(context, response);
}
else
{
context.RejectPrincipal();
}
}
private void WriteCookies(CookieValidatePrincipalContext context, TokenResponse response)
{
var tokens = new List<AuthenticationToken>
{
new AuthenticationToken
{
Name = OpenIdConnectParameterNames.IdToken,
Value = response.IdentityToken
},
new AuthenticationToken
{
Name = OpenIdConnectParameterNames.AccessToken,
Value = response.AccessToken
},
new AuthenticationToken
{
Name = OpenIdConnectParameterNames.RefreshToken,
Value = response.RefreshToken
},
new AuthenticationToken
{
Name = OpenIdConnectParameterNames.TokenType,
Value = response.TokenType
}
};
var expiresAt = DateTime.UtcNow.AddSeconds(response.ExpiresIn);
tokens.Add(new AuthenticationToken
{
Name = "expires_at",
Value = expiresAt.ToString("o", CultureInfo.InvariantCulture)
});
var newPrincipal = GetNewPrincipal(context);
context.ReplacePrincipal(newPrincipal);
context.Properties.StoreTokens(tokens);
context.ShouldRenew = true;
}
private static ClaimsPrincipal GetNewPrincipal(CookieValidatePrincipalContext context)
{
var claims = context.Principal.Claims.Select(c => new Claim(c.Type, c.Value)).ToList();
var authTimeClaim = claims.FirstOrDefault(claim => claim.Type.Same("auth_time"));
if (authTimeClaim != null)
{
claims.Remove(authTimeClaim);
}
claims.Add(new Claim("auth_time", DateTime.UtcNow.UnixTimestamp().ToString()));
return new ClaimsPrincipal(new ClaimsIdentity(claims, context.Principal.Identity.AuthenticationType));
}
The main problem is this works perfectly fine locally. All the calls are fine, cookies are refreshed correctly. But when the app is run from dev machine (we host it in Azure App Service) RequestRefreshTokenAsync call is successful, but the cookies are not updated therefore all the next calls are made with an old tokens leading to 400 invalid_grant error. So basically what I see in logs is:
RequestRefreshTokenAsync successful. The old refresh_token is used to get a new one
ValidatePrincipal successful. Here the cookies should be rewritten (including a new refresh_token)
Next request - RequestRefreshTokenAsync failed. The old refresh_token is used (even though it's invalid).
I tried playing with cookies configurations and placing semaphores and locks inside ValidatePrincipal method but none of it worked.
Does anyone have an idea what can cause that?

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.

List secrets in a KeyVault without logging in for every secret?

I've successfully managed to list all of the secrets in an Azure KeyVault - however I need to make a call to get a token each time I want to get the next secret.
How do I store the credentials so I only have to login once during the loop?
public async Task<List<string>> getsecretslist(string url)
{
var kv = new KeyVaultClient(new KeyVaultClient.AuthenticationCallback(GetToken));
List<string> secretlist = new List<string>();
var all = kv.GetSecretsAsync(url);
var myId = "";
foreach (Microsoft.Azure.KeyVault.Models.SecretItem someItem in all.Result)
{
myId = someItem.Id;
var mOtherThing = someItem.Identifier;
var yep = await kv.GetSecretAsync(mOtherThing.ToString());
secretlist.Add(yep.Value);
}
return secretlist;
}
In your GetToken callback method you need to cache the access token as long as it is valid and not expired. Then your callback will return the cached access token instead of doing the authentication again. The following code snippet will use the ADAL default token cache (e.g. TokenCache.DefaultShared).
public static async Task<string> GetToken(string authority, string resource, string scope)
{
var assertionCert = new ClientAssertionCertificate(clientId, certificate);
var context = new AuthenticationContext(authority, TokenCache.DefaultShared);
var result = await context.AcquireTokenAsync(resource, assertionCert).ConfigureAwait(false);
return result.AccessToken;
}
The best way that i found is to save the token you obtained in your GetToken function, for example:
var authenticationContext = new AuthenticationContext(authority, TokenCache.DefaultShared);
var authenticationResult = await authenticationContext.AcquireTokenAsync(resource, KeyVaultUserClientId, new Uri(KeyVaultRedirectUri), new PlatformParameters(PromptBehavior.SelectAccount)).ConfigureAwait(false);
return authenticationResult.AccessToken;
Then i simply altered the getter for the client so it will check for the expiry, if its still valid (should have expiration of 60 minutes) it will return a simpler client which returns the lastAuthenticationResult
private static KeyVaultClient KeyVaultClient
{
get
{
if (lastAuthenticationResult != null && DateTime.UtcNow.AddSeconds(5) < lastAuthenticationResult.ExpiresOn)
{
if (m_cachedKeyVaultClient != null)
{
return m_cachedKeyVaultClient;
}
else
{
return new KeyVaultClient(getCachedToken);
}
}
if (m_keyVaultClient == null)
m_keyVaultClient = new KeyVaultClient(GetAccessTokenAsync);
return m_keyVaultClient;
}
}
private static async Task<string> getCachedToken(string authority, string resource, string scope)
{
return lastAuthenticationResult.AccessToken;
}
You don't need to call GetSecretAsync inside your loop. The secrets are already included in your Result set from calling GetSecretsAsync. This is why you are being authenticated repeatedly.
Here is a simple change to your loop to do what you are looking for.
var all = kv.GetSecretsAsync(url).GetAwaiter().GetResult();
foreach (var secret in all.Value)
{
secretlist.Add(secret.Id);
}

Office 365 AdalError.FailedToAcquireTokenSilently

So I've been working for a couple days attempting to create a working example of an application grabbing data, such as a list of the user's contacts, and display it. My attempts so far have be unsuccessful. I have followed a few guides and have read many forums but I can't seem to get them to work.
I got the feeling I have one of the following issues.
Either my application or I do not have the proper permissions on my employer's domain
I am missing a concept
I have a grammatical error in my code.
I have incorrect settings in portal.azure.com or manage.windowsazure.com for my application
As for the where my asp.net project kept breaking here is the code, look at var authResult = await authContext.AcquireTokenSilentAsync(discServResouceId, cc, userid);. I got the original from here AuthenticationHelper
internal class AuthenticationHelper
{
internal static async Task<OutlookServicesClient> EnsureOutlookServicesClientCreatedAsync(string capabilityName)
{
var signInUserId = ClaimsPrincipal.Current.FindFirst(ClaimTypes.NameIdentifier).Value;
var userObjectId =
ClaimsPrincipal.Current.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier").Value;
var authContext = new AuthenticationContext(SettingsHelper.Authority, new ADALTokenCache(signInUserId));
try
{
var discClient = new DiscoveryClient(SettingsHelper.DiscoveryServiceEndpointUri,
async () =>
{
var cid = SettingsHelper.ClientId;
var appkey = SettingsHelper.AppKey;
var discServResouceId = SettingsHelper.DiscoveryServiceResourceId;
var cc = new ClientCredential(cid, appkey);
var userid = new UserIdentifier(userObjectId, UserIdentifierType.UniqueId);
var authResult = await authContext.AcquireTokenSilentAsync(discServResouceId, cc, userid);
//AcquireTokenSilentAsync is where my application throws an
//AdalException with the error code "AdalError.FailedToAcquireTokenSilently"
return authResult.AccessToken;
});
var dcr = await discClient.DiscoverCapabilityAsync(capabilityName);
return new OutlookServicesClient(dcr.ServiceEndpointUri,
async () =>
{
var authResult = await authContext.AcquireTokenSilentAsync(dcr.ServiceResourceId,
new ClientCredential(SettingsHelper.ClientId,
SettingsHelper.AppKey),
new UserIdentifier(userObjectId,
UserIdentifierType.UniqueId));
return authResult.AccessToken;
});
}
catch (AdalException exception)
{
//Handle token acquisition failure
if (exception.ErrorCode == AdalError.FailedToAcquireTokenSilently)
{
Debug.Print(exception.ErrorCode);
authContext.TokenCache.Clear();
//throw exception;
}
return null;
}
catch (Microsoft.Office365.Discovery.DiscoveryFailedException ex)
{
Debug.Print(ex.Message);
return null;
}
}
}
As far as I know everything is being given to AcquireTokenSilentAsync correctly. For that reason I think it's a permission issue with active directory. Any help is appreciated.

Categories

Resources