I am trying to create azure key vault using .net core 2.1 with OpenIdConnect.
What I have tried :-
I have tried to refer following stack-overflow already question-answer
Creating Azure Key Vault using .NET assembly (Microsoft.Azure.KeyVault)
Azure Key Vault - programmatic creation
and Others
Nuget package :- Microsoft.Azure.Management.KeyVault
Code :-
private async Task AddKeyVaultAsync()
{
var clientId = "xxxx";
var tenantId = "xxxx";
var clientSecret = "xxxx";
var objectId = "xxxx";
var subscriptionId = "xxx";
// The resource group to create the vault in.
string resourceGroupName = "Vaults-Resource-Group";
// The name of the vault to create.
string vaultName = "web-app-01-vault";
var parameters = new VaultCreateOrUpdateParameters()
{
Location = "southeast asia",
Properties = new VaultProperties()
{
TenantId = Guid.Parse(tenantId),
AccessPolicies = new List<AccessPolicyEntry>()
{
new AccessPolicyEntry
{
TenantId = Guid.Parse(tenantId),
ObjectId = objectId,
Permissions = new Permissions
{
Secrets = new List<string> { "all" },
Keys = new string[] { "all" }
}
}
}
}
};
//problem in following line
var tokenCredentials = new TokenCloudCredentials(subscriptionId, token);
var keyVaultManagementClient = new KeyVaultManagementClient(tokenCredentials);
// Create the vault
await keyVaultManagementClient.Vaults.CreateOrUpdateAsync(resourceGroupName, vaultName, parameters);
}
but I am stuck at
//problem in the following line
var tokenCredentials = new TokenCloudCredentials(subscriptionId, token);
How can i create token(parameter in TokenCloudCredentials) and TokenCloudCredentials?
Which Nuget package shall i use to create TokenCloudCredentials?
I have also tried to use :-
IConfidentialClientApplication confidentialClientApplication = ConfidentialClientApplicationBuilder
.Create(clientId)
.WithTenantId(tenantId)
.WithClientSecret(clientSecret)
.Build();
to create KeyVaultManagementClient. but I am not sure that how can do it?
Is there any other(better) way to create KeyVaultManagementClient?
The code shows how to get an access token using client credential flow.
var app = ConfidentialClientApplicationBuilder.Create(config.ClientId)
.WithAuthority(AzureCloudInstance.AzurePublic, "{tenantID}")
.WithClientSecret(config.ClientSecret)
.Build();
string[] scopes = new string[] { "https://graph.microsoft.com/.default" };
var result = await app.AcquireTokenForClient(scopes).ExecuteAsync();
var token = result.accessToken;
For more details, see here.
Update:
The sample of creating key vault using .net core 2.1
Before accessing Key Vault from code ensure that MSI(Managed Service Identity) is configured in Azure
To enable the use of Azure Key Vault you need to install below
packages.
PM> Install-Package Azure.Security.KeyVault.Secrets
PM> Install-Package Microsoft.Extensions.Configuration.AzureKeyVault
PM> Install-Package Azure.Identity
PM> Install-Package Azure.Extensions.AspNetCore.Configuration.Secrets
Enable App Configuration in Program.cs — Update the
CreateWebHostBuilder method to use App Configuration by calling the
config.AddAzureAppConfiguration() method.
#region Imports
using Microsoft.AspNetCore.Hosting;
using Microsoft.Azure.KeyVault;
using Microsoft.Azure.Services.AppAuthentication;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Configuration.AzureKeyVault;
using Microsoft.Extensions.Hosting;
#endregion
namespace AzureKeyVaultLabs.Web
{
public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureAppConfiguration((context, config) =>
{
var settings = config.Build();
if (!context.HostingEnvironment.IsDevelopment())
{
var keyVaultEndpoint = settings["AzureKeyVaultEndpoint"];
if (!string.IsNullOrEmpty(keyVaultEndpoint))
{
var azureServiceTokenProvider = new AzureServiceTokenProvider();
var keyVaultClient = new KeyVaultClient(new KeyVaultClient.AuthenticationCallback(azureServiceTokenProvider.KeyVaultTokenCallback));
config.AddAzureKeyVault(keyVaultEndpoint, keyVaultClient, new DefaultKeyVaultSecretManager());
}
}
}
}
For Azure Function Apps
public class Startup : FunctionsStartup
{
public override void ConfigureAppConfiguration(IFunctionsConfigurationBuilder builder)
{
if (builder != null)
{
//give your app configuration store endpoint
string connectionString = Environment.GetEnvironmentVariable("AppConfigurationConnectionString");
if (!string.IsNullOrEmpty(connectionString))
{
builder.ConfigurationBuilder.AddAzureAppConfiguration(connectionString);
}
var settings = builder.ConfigurationBuilder.Build();
var keyVaultEndpoint = settings["VaultName"];// Add key vault name in configuration
if (!string.IsNullOrEmpty(keyVaultEndpoint))
{
builder.ConfigurationBuilder
.SetBasePath(Environment.CurrentDirectory)
.AddAzureKeyVault(new Uri(keyVaultEndpoint), new DefaultAzureCredential())
.AddEnvironmentVariables()
.Build();
}
}
}
Related
I am trying my hand at gRPC in C# and wanted to do authentication integration with aspnet core .Net 6. I am following the tutorial on microsoft for setting up a gRPC application.
The application works with no issues until I attempt to add in the authentication. I am working with a self-signed cert, that I have added to my local user store as well as my trusted root store just in case the cert was rejected due to revocation. I've tried a lot of things to help troubleshoot the issue, but with no luck. I am not sure where to go now and would appreciate any pointers/tips.
Kestrel Web Server gRPC File
using Microsoft.AspNetCore.Authentication.Certificate;
using Microsoft.AspNetCore.Server.Kestrel.Core;
using Microsoft.AspNetCore.Server.Kestrel.Https;
using grpcServerTest.Services;
using System.Security.Claims;
using System.Security.Cryptography.X509Certificates;
namespace grpcServerTest
{
public class Program
{
public static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
// Additional configuration is required to successfully run gRPC on macOS.
// For instructions on how to configure Kestrel and gRPC clients on macOS, visit https://go.microsoft.com/fwlink/?linkid=2099682
// Add services to the container.
builder.Services.AddGrpc();
//builder.Services.AddAuthorization();
builder.Services.AddAuthentication(
CertificateAuthenticationDefaults.AuthenticationScheme)
.AddCertificate(options =>
{
options.AllowedCertificateTypes = CertificateTypes.All;
options.ValidateCertificateUse = false;
options.RevocationFlag = System.Security.Cryptography.X509Certificates.X509RevocationFlag.ExcludeRoot;
options.RevocationMode = System.Security.Cryptography.X509Certificates.X509RevocationMode.NoCheck;
options.Events = new CertificateAuthenticationEvents
{
OnChallenge = context =>
{
return Task.CompletedTask;
},
OnAuthenticationFailed = context =>
{
return Task.CompletedTask;
},
OnCertificateValidated = context =>
{
if (true)
{
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;
}
};
});
builder.Services.Configure<KestrelServerOptions>(options =>
{
options.ConfigureHttpsDefaults(options =>
{
options.SslProtocols = System.Security.Authentication.SslProtocols.Tls12;
options.CheckCertificateRevocation = false;
//options.ServerCertificate = GetClientCertificate();
options.ClientCertificateValidation = (cert, chain, errors) =>
{
Console.WriteLine("Client Validation Called");
errors = System.Net.Security.SslPolicyErrors.None;
return true;
};
});
});
var app = builder.Build();
app.UseCertificateForwarding();
app.UseAuthentication();
//app.UseAuthorization();
// Configure the HTTP request pipeline.
app.MapGrpcService<GreeterService>();
app.MapGet("/", () => "Communication with gRPC endpoints must be made through a gRPC client. To learn how to create a client, visit: https://go.microsoft.com/fwlink/?linkid=2086909");
app.Run();
}
}
}
gRPC Client Code
// See https://aka.ms/new-console-template for more information
using Grpc.Core;
using Grpc.Net.Client;
using GrpcTest;
using System.Security.Cryptography.X509Certificates;
internal class Program
{
private static async Task Main(string[] args)
{
Console.WriteLine("Hello, World!");
var x509 = GetClientCertificate();
var handler = new HttpClientHandler();
handler.SslProtocols = System.Security.Authentication.SslProtocols.Tls12 | System.Security.Authentication.SslProtocols.Tls11 | System.Security.Authentication.SslProtocols.Tls;
handler.ClientCertificateOptions = ClientCertificateOption.Manual;
handler.ClientCertificates.Add(x509);
handler.UseProxy = false;
Console.ReadKey();
using var channel = GrpcChannel.ForAddress("https://localhost:7283", new GrpcChannelOptions()
{
HttpHandler = handler,
DisposeHttpClient = true
});
var client = new Greeter.GreeterClient(channel);
var reply = await client.SayHelloAsync(
new HelloRequest { Name = "GreeterClient" });
Console.WriteLine("Greeting: " + reply.Message);
Console.WriteLine("Press any key to exit...");
Console.ReadKey();
}
private static X509Certificate2 GetClientCertificate()
{
X509Store userCaStore = new X509Store(StoreName.My, StoreLocation.CurrentUser);
try
{
userCaStore.Open(OpenFlags.ReadOnly);
X509Certificate2Collection certificatesInStore = userCaStore.Certificates;
X509Certificate2Collection findResult = certificatesInStore.Find(X509FindType.FindBySubjectName, "grpctest", true);
X509Certificate2 clientCertificate = null!;
if (findResult.Count == 1)
{
clientCertificate = findResult[0];
}
else
{
throw new Exception("Unable to locate the correct client certificate.");
}
return clientCertificate;
}
catch
{
throw;
}
finally
{
userCaStore.Close();
}
}
}
Is this .NET 6 or 7?
See the parts relevant to gRPC and full chain support in Kestrel at https://learn.microsoft.com/en-us/aspnet/core/release-notes/aspnetcore-7.0?view=aspnetcore-7.0
With .NET 6 you might need to use something like https://github.com/MarkCiliaVincenti/TlsCertificateLoader
I am a dingus. I was passing the public cert to the HttpHandler in the request instead of a public and private key to actually perform authentication.
I am not sure if this is the appropriate answer or if I have a weird configuration value somewhere that I do not see, but I was able to get certificates to be passed through.
The issue, Kestrel seems to have been blocking the certificate, despite my attempt to bypass it by configuring the default HTTPS connection. Once I manually created my own listener in builder and did not rely on the default configuration, it seem that it worked without an issue.
This is the code I used for my builder, albeit maybe overboard and I do not recommend it for production use without removing my security bypass.
builder.Services.Configure<KestrelServerOptions>(options =>
{
options.Listen(IPAddress.Loopback, 8080, listenOptions =>
{
listenOptions.UseConnectionLogging();
listenOptions.UseHttps(options =>
{
options.ClientCertificateMode = ClientCertificateMode.RequireCertificate;
options.CheckCertificateRevocation = false;
options.AllowAnyClientCertificate();
options.ClientCertificateValidation = (cert, chain, errors) =>
{
Console.WriteLine("Client Validation Called");
errors = System.Net.Security.SslPolicyErrors.None;
return true;
};
});
});
});
How to validate uploaded ARM Template using azure .net SDK or Fluent API ?
I want to validate my uploaded ARM template like azure portal do using azure .net SDK or Fluent API ?
For reference please see below image azure is showing message if ARM template not valid so same thing i want to implement using any .net API or REST API.
#Jim Below error I am getting:
If you want to validate your arm template, please refer to the following steps
Create a service principal and assign Contributor role to the sp
az ad sp create-for-rbac -n "MyApp"
Install Package
Install-Package Microsoft.Azure.Management.ResourceManager.Fluent -Version 1.34.0
Code
string clientId = "23****9c";
string clientSecret = "?s****/k";
string tenantDomain = "";
string subscription = "";
var creds= SdkContext.AzureCredentialsFactory.FromServicePrincipal(clientId, clientSecret, tenantDomain, AzureEnvironment.AzureGlobalCloud);
var restClient = RestClient.Configure()
.WithEnvironment(AzureEnvironment.AzureGlobalCloud)
.WithCredentials(creds)
.WithLogLevel(HttpLoggingDelegatingHandler.Level.BodyAndHeaders)
.Build();
ResourceManagementClient managementClient = new ResourceManagementClient(restClient);
managementClient.SubscriptionId = subscription;
//Validates whether the specified template is syntactically correct and will be accepted by Azure Resource Manager..
DeploymentValidateResultInner res = await managementClient.Deployments.ValidateAsync("<groupName>", "<deployName>", new DeploymentInner()
{
Location = "",
Properties = new DeploymentProperties()
{
ParametersLink = new ParametersLink("uri"),
TemplateLink = new TemplateLink("")
}
});
Console.WriteLine(res.Error.Message);
// get changes that will be made by the deployment if executed at the scope of resource group
WhatIfOperationResultInner res1 = await managementClient.Deployments.WhatIfAsync("<groupName>", "<deployName>", new DeploymentWhatIf() {
Location="",
Properties= new DeploymentWhatIfProperties() {
ParametersLink = new ParametersLink("uri"),
TemplateLink = new TemplateLink("")
}
});
foreach (var change in res1.Changes) {
//
}
I like that the accepted answer adds the "what if" to validation. However, Microsoft.Azure.Management.ResourceManager is deprecated, and it took me a bit to figure out a way to validate an ARM template using the replacement library: Azure.ResourceManager.
Here's a code snippet that provides template validation using the new library (it doesn't include the what-if call):
var credential = new DefaultAzureCredential();
var subscriptionId = Environment.GetEnvironmentVariable("AZURE_SUBSCRIPTION_ID");
var client = new ArmClient(credential, subscriptionId);
var deploymentContent = new ArmDeploymentContent(new ArmDeploymentProperties(ArmDeploymentMode.Incremental)
{
Template = BinaryData.FromString(templateContent),
Parameters = BinaryData.FromObjectAsJson(new
{
hostingPlanName = new
{
value = hostingPlanName
},
webSiteName = new
{
value = webAppName
},
skuName = new
{
value = webSkuName
},
skuCapacity = new
{
value = webSkuCapacity
},
})
});
var resourceGroupId = ResourceGroupResource.CreateResourceIdentifier(SubscriptionId!, resourceGroupName);
// This ArmDeploymentResource resource may or may not exist, but it doesn't matter - it's just a placeholder for validation
var deploymentResourceId = ArmDeploymentResource.CreateResourceIdentifier(resourceGroupId, deploymentName);
var armDeployment = client.GetArmDeploymentResource(deploymentResourceId);
var validateOperation = await armDeployment.ValidateAsync(WaitUntil.Completed, toValidate, _cancellationToken);
var validateResult = validateOperation.Value;
if (validateResult.Error != null)
{
_logger.LogEndOperation(loggerOpKey, false, validateResult.Error.Message ?? "Validation errored");
_logger.LogError(JsonConvert.SerializeObject(validateResult.Error, Formatting.Indented));
return false;
}
// Log this if you want:
string deploymentDetails = $"Deployment: {deploymentName} ProvisioningState:{validateResult.Properties.ProvisioningState}\n"
+ $" started:{validateResult.Properties.Timestamp} duration:{validateResult.Properties.Duration}\n"
+ $" correlationId:{validateResult.Properties.CorrelationId}\n"
+ $" outputs:{JsonConvert.SerializeObject(validateResult.Properties.Outputs)}";
bool succeeded = validateResult.Properties.ProvisioningState == "Succeeded";
return succeeded;
I am building a process to send emails from my application. I signed up for a Office 365 org in Azure to test this. In my App registration, I granted the following permissions. The admin consent has been granted for these permissions.
Next I wrote this code, which is based off a github project.
using Microsoft.Graph;
using Microsoft.Graph.Auth;
using Microsoft.Identity.Client;
using System;
using System.Collections.Generic;
using System.Web.UI;
namespace DemoOutlookMail
{
public partial class _Default : Page
{
protected void send_Click(object sender, System.EventArgs e)
{
const string tenantId = "APP REGISTRATION TENANT ID";
const string redirectUri = "https://localhost:44316/";
const string clientSecret = "APP REGISTRATION SECRET";
const string clientId = "APP REGISTRATION CLIENT ID";
const string AuthorityFormat = "https://login.microsoftonline.com/{0}/v2.0";
IConfidentialClientApplication daemonClient;
daemonClient = ConfidentialClientApplicationBuilder.Create(clientId)
.WithAuthority(string.Format(AuthorityFormat, tenantId))
.WithRedirectUri(redirectUri)
.WithClientSecret(clientSecret)
.Build();
ClientCredentialProvider authProvider = new ClientCredentialProvider(daemonClient,"Mail.Send");
GraphServiceClient graphClient = new GraphServiceClient(authProvider);
SendEmail(graphClient);
}
private void SendEmail(GraphServiceClient graphClient)
{
var message = new Message
{
Subject = "Meet for lunch?",
Body = new ItemBody
{
ContentType = BodyType.Text,
Content = "The new cafeteria is open."
},
ToRecipients = new List<Recipient>()
{
new Recipient
{
EmailAddress = new EmailAddress
{
Address = "AdeleV#testariesllc.onmicrosoft.com"
}
}
},
CcRecipients = new List<Recipient>()
{
new Recipient
{
EmailAddress = new EmailAddress
{
Address = "AlexW#testariesllc.onmicrosoft.com"
}
}
}
};
var saveToSentItems = false;
try
{
graphClient.Me.SendMail(message, saveToSentItems).Request().PostAsync();
}
catch(Exception e)
{
Label1.Text = e.Message;
}
}
}
}
This code runs but doesn't send any emails, doesn't throw any exceptions. What am I missing?
The original github project is https://github.com/Azure-Samples/ms-identity-aspnet-daemon-webapp
Because your using Application permission the /me endpoint is supported
Calling the /me endpoint requires a signed-in user and therefore a delegated permission.
Application permissions are not supported when using the /me endpoint.
https://learn.microsoft.com/en-us/graph/api/user-get?view=graph-rest-1.0&tabs=http
So in your code you need to specify the user your going to be sending as it should be an easy fix eg
graphClient.Users["user#domain.com"].SendMail(message, saveToSentItems).Request().PostAsync();
I am trying to create a domain shared contact in GSuite via the Contacts API in C# but unable to figure out how to POST the atom XML entry to the Feed URL as mentioned here : https://developers.google.com/admin-sdk/domain-shared-contacts/#Creating
I have tried following the older GData way mentioned here https://developers.google.com/gdata/client-cs but I get a "Execution of authentication request returned unexpected result: 404" error.
static void Main(string[] args)
{
Console.WriteLine("Hello !! ");
//Get Auth
OAuth2Parameters p = ContactsAuth();
//Create a domain shared contact
try
{
RequestSettings settings = new RequestSettings("GSuiteAdminApp", p);
ContactsRequest cr = new ContactsRequest(settings);
ContactEntry cn = new ContactEntry();
Name n = new Name();
n.GivenName = "Ice";
n.FamilyName = "Cold001";
n.FullName = "Ice Cold001";
EMail e = new EMail();
e.Rel = "http://schemas.google.com/g/2005#work";
e.Primary = true;
e.Address = "ice.cold001#xyz.com";
cn.Name = n;
cn.Emails.Add(e);
}
catch (Exception e44)
{
Console.WriteLine(e44.Message);
}
}
//Auth for Contacts API
public static OAuth2Parameters ContactsAuth()
{
string clientId = "xxxxxxxxxxxxxx.apps.googleusercontent.com";
string clientSecret = "xxxxxxxxxxxxx";
string[] scopes = new string[] { "https://www.google.com/m8/feeds/" };
try
{
UserCredential credential = GoogleWebAuthorizationBroker.AuthorizeAsync(new ClientSecrets
{
ClientId = clientId,
ClientSecret = clientSecret
}, scopes, "super-admin#mydomain.com", CancellationToken.None, new FileDataStore("C:\\Temp\\A\\SharedContactsOauth")).Result;
// Translate the Oauth permissions to something the old client libray can read
OAuth2Parameters parameters = new OAuth2Parameters();
parameters.AccessToken = credential.Token.AccessToken;
parameters.RefreshToken = credential.Token.RefreshToken;
return parameters;
}
catch (Exception ex33)
{
Console.WriteLine(ex33.Message);
return null;
}
}
This gives a "request failed" error.
I was finally able to figure it out by stringing along code snippets from few different sources and some modifications of my own. Linda Lawton's https://www.daimto.com/google-contacts-with-c/ for the OAuth2 part using older GData API. Google's documentation on Contacts API v3.0 https://developers.google.com/contacts/v3/ for mechanics of using the .NET client library for contacts and their (bit sketchy) documentation on "domain shared contacts", especially on using the proper FeedUri and Atom entries for new contact https://developers.google.com/admin-sdk/domain-shared-contacts/#Creating.
Basically what it boils down to is this -
Use a GSuite Super Admin account to authrorize to Contacts API using OAuth2.0, then use GData Contacts .NET client library to create the new contact
by suppying your Gsuite domain in the method and you are done.
Here's the full code which I have it working for me now:
using System;
using System.Threading;
using Google.Contacts;
using Google.GData.Contacts;
using Google.GData.Client;
using Google.GData.Extensions;
using Google.Apis.Auth;
using Google.Apis.Auth.OAuth2;
using Google.Apis.Util.Store;
namespace SharedContactsAPI
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Hello !! ");
//Get Auth
OAuth2Parameters p = ContactsAuth();
////Create a domain shared contact
try
{
RequestSettings settings = new RequestSettings("GSuiteAdminApp", p);
ContactsRequest contactreq = new ContactsRequest(settings);
Console.WriteLine("Attempting to create a Domain Shared Contact in GSuite");
Console.WriteLine(" ");
CreateContact(contactreq);
}
catch (Exception e44)
{
Console.WriteLine(e44.Message);
}
}
//Create Shared Contact
public static Contact CreateContacttest(ContactsRequest cr)
{
Contact newEntry = new Contact();
// Set the contact's name.
newEntry.Name = new Name()
{
FullName = "Ice Cold005",
GivenName = "Ice",
FamilyName = "Cold005"
};
newEntry.Content = "Notes";
// Set the contact's e-mail addresses.
newEntry.Emails.Add(new EMail()
{
Primary = true,
Rel = ContactsRelationships.IsWork,
Address = "ice.cold005#xyz.com"
});
//Insert the contact
Uri feedUri = new Uri(ContactsQuery.CreateContactsUri("test.com"));
Contact createdEntry = cr.Insert(feedUri, newEntry);
Console.WriteLine("New Contact created successfully with ContactID = " + createdEntry.Id);
return createdEntry;
}
//Auth for Contacts API
public static OAuth2Parameters ContactsAuthtest()
{
string clientId = "xxxxxxxxxxxxxxxxxxxxx.apps.googleusercontent.com";
string clientSecret = "xxxxxxxxxxxxxxxxxxxxxxxxxx";
string[] scopes = new string[] { "https://www.google.com/m8/feeds/contacts/" };
try
{
UserCredential credential = GoogleWebAuthorizationBroker.AuthorizeAsync(new ClientSecrets
{
ClientId = clientId,
ClientSecret = clientSecret
}, scopes, "super-admin#test.com", CancellationToken.None, new FileDataStore("C:\\Temp\\A\\SharedContactsOauth")).Result;
// Translate the Oauth permissions to something the old client libray can read
OAuth2Parameters parameters = new OAuth2Parameters();
parameters.AccessToken = credential.Token.AccessToken;
parameters.RefreshToken = credential.Token.RefreshToken;
return parameters;
}
catch (Exception ex33)
{
Console.WriteLine(ex33.Message);
return null;
}
}
}
}
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)
{
// ...
}
}
}