Update asp.net core Authentication outside of Startup.cs - c#

With respect to asp.net core identity management, we have a requirement to change the Microsoft ClientId and ClientSecret after our asp.net core app has started and, therefore, not in startup.cs. We have various identity management logins working fine with, for example this for Microsoft Azure:
.AddMicrosoftAccount(microsoftOptions =>
{
microsoftOptions.CorrelationCookie.HttpOnly = true;
microsoftOptions.CorrelationCookie.SecurePolicy = CookieSecurePolicy.Always;
microsoftOptions.ClientId = "removed";
microsoftOptions.ClientSecret = "removed";
})
We now need to change the ClientId and ClientSecret dynamically after the core application has started and what we can't figure out is how to access this from the services collection later in other pages so we can update them.
Any help appreciated.
Thanks.

ASP.NET Core provides IAuthenticationSchemeProvider interface to dynamically add/remove authentication schemes at runtime. You can inject this interface and add Microsoft Account auth schemes after the app has started.
Using Microsoft's demo app as reference, here's a basic implementation:
public class DynamicAuthController: ControllerBase
{
private IAuthenticationSchemeProvider _schemeProvider;
private IOptionsMonitorCache<MicrosoftAccountOptions> _optionsCache;
public DynamicAuthController(IAuthenticationSchemeProvider schemeProvider, IOptionsMonitorCache<MicrosoftAccountOptions> optionsCache)
{
_schemeProvider = schemeProvider;
_optionsCache = optionsCache;
}
[HttpPost]
public ActionResult Add()
{
var schemeName = "MicrosoftCustom1"; // must be unique for different schemes
var schemeOptions = new MicrosoftAccountOptions
{
ClientId = "ididid", // fetch credentials from another service or database
ClientSecret = "secretsecret",
CorrelationCookie =
{
HttpOnly = true,
SecurePolicy = CookieSecurePolicy.Always
}
};
var scheme = new AuthenticationScheme(schemeName, displayName:null, typeof(MicrosoftAccountHandler));
_schemeProvider.TryAddScheme(scheme);
_optionsCache.TryAdd(
schemeName,
schemeOptions
);
return Ok();
}
}

Related

ASP.NET Core custom authentication filter for Azure AD token validation

I am currently working on a project where I need to work with Azure AD token, which is working fine. I register my application in Azure portal and use the client id in my project and this is working fine with this code (in startup.cs):
services.AddMicrosoftIdentityWebApiAuthentication(configuration, "AzureAd");
and these settings in appsettings.json:
"AzureAd": {
"Instance": "https://login.microsoftonline.com/",
"TenantId": "35234523452345",
"ClientId": "25234523452345",
"Audience": "api://25234523452345"
},
I want to token only for one API call but with the above code it will apply on all the API. Can I use a custom filter which will use the Azure token for authentication?
Can someone help me with a code sample?
i found solution for my question, i created a method which will validate azure token, and will return me user name from user claim
public static async Task<string> ValidateAzureToken(string token, AppConfigurationList appConfigurations)
{
var tenantId = "Azure Tenant Id here";
var audience = string.Format("api://{0}", "Azure Client Id here");
var azureClientKey = "Azure Client secret Key here";
var myIssuer = string.Format(CultureInfo.InvariantCulture, "https://sts.windows.net/{0}/", tenantId);
var securityKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(azureClientKey));
var stsDiscoveryEndpoint = string.Format(CultureInfo.InvariantCulture, "https://login.microsoftonline.com/{0}/.well-known/openid-configuration", tenantId);
var configManager = new ConfigurationManager<OpenIdConnectConfiguration>(stsDiscoveryEndpoint, new OpenIdConnectConfigurationRetriever());
var config = await configManager.GetConfigurationAsync();
var tokenHandler = new JwtSecurityTokenHandler();
var validationParameters = new TokenValidationParameters
{
ValidateAudience = true,
ValidAudience = audience,
ValidateIssuer = true,
ValidIssuer = myIssuer,
IssuerSigningKeys = config.SigningKeys,
ValidateLifetime = false,
IssuerSigningKey = securityKey,
};
_ = (SecurityToken)new JwtSecurityToken();
SecurityToken validatedToken;
try
{
var claims = tokenHandler.ValidateToken(token, validationParameters, out validatedToken);
return claims.Identity.Name.Split("#")[0];
}
catch (Exception e)
{
throw new TGUnauthorizedException("unauthorized domain user", e);
}
}
An ASP.NET Core web application that authenticates Azure AD users and calls a web API using OAuth 2.0 access tokens.
Here you can find the ASP.NET Core samples about JwtBearer. Here the Azure AD samples demonstrate WebApp calling WebApi (also bearer for the WebAPI, OIDC for the App FrontEnd.
Sample Code: https://github.com/Azure-Samples/active-directory-dotnet-webapp-webapi-openidconnect-aspnetcore
Reference SO Thread: Azure AD Authentication in ASP.NET Core 2.2
I think you can follow this document to set authentication for the specific api.
Based on the configuration you've done, you can manage your api controller like this
[Authorize]
public class HelloController : Controller
{
public IActionResult Index()
{
HttpContext.ValidateAppRole("Tiny.Read");
Student stu = new Student();
stu.age = 18;
return Json(stu) ;
}
}
The above verified the role based api permission, and if you prefer to use delegate api permission, you can set the api like this
[ApiController]
[Authorize]
public class HomeController : ControllerBase
{
[HttpGet]
[RequiredScope("User.Read")]
public ActionResult<IEnumerable<string>> Get()
{
return new string[] { "value1", "value2" };
}
}
Have you looked at the useWhen mechanism, which lets you apply a type of authentication to a particular condition - eg Path + Method:
app.UseWhen(
ctx => ctx.Request.Path.StartsWithSegments(new PathString("/api/mypath")) &&
ctx.Request.Method != "OPTIONS",
api => api.UseAuthentication()
);
My experimental .Net Core API does this and may give you some ideas. My objective was to write APIs in different technologies in the same way, and to take closer control over OAuth / claims processing.
In a real API it of course makes sense to keep code simple as much as you can, and of course to use proper libraries for any real security / crypto.

Which API security to use for an external API consumed by Blazor WASM

Context: Got an API running with a simple /auth call that expects email, password and some sort of db identifier. Which then returns a JWT token. This token can be used to request the other calls and know which database to access. The client is now in UWP which handles the UI and does the calls to the API. Not using Azure Api Management for now and not using the Microsoft Identity platform. Just a regular password hash check.
Recently, we wanted to switch from UWP to a Blazor WASM (client only) but haven't really found any suitable support to work with Bearer tokens and the documentation steers us towards four options.
AAD
AAD B2C
Microsoft Accounts
Authentication library (?)
Not all our users have Office 365 accounts.
Kind of lost in this new "Blazor space" since it's very different from our WPF & UWP projects and it doesn't seem to be fully documented yet.
Thanks.
Update code on request
Program.cs
public static async Task Main(string[] args)
{
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("app");
// Local storage access
builder.Services.AddBlazoredLocalStorage();
builder.Services.AddTransient<BaseAddressAuthorizationMessageHandler>();
builder.Services.AddTransient<IAccessTokenProvider, ApiTokenProvider>();
builder.Services
.AddHttpClient<IMambaClient, MambaClient>(client => client.BaseAddress = _baseUri)
.AddHttpMessageHandler(sp => sp.GetRequiredService<BaseAddressAuthorizationMessageHandler>()
.ConfigureHandler(new[] { _apiEndpointUrl }));
await builder.Build().RunAsync();
}
ApiTokenProvider.cs
public class ApiTokenProvider : IAccessTokenProvider
{
private readonly ILocalStorageService _localStorageService;
public ApiTokenProvider(ILocalStorageService localStorageService)
{
_localStorageService = localStorageService;
}
public async ValueTask<AccessTokenResult> RequestAccessToken()
{
var token = await _localStorageService.GetItemAsync<string>("Token");
AccessTokenResult accessTokenResult;
if (!string.IsNullOrEmpty(token))
{
accessTokenResult = new AccessTokenResult(AccessTokenResultStatus.Success, new AccessToken() { Value = token, Expires = new DateTimeOffset(DateTime.Now.AddDays(1)) }, "/");
}
else
{
accessTokenResult = new AccessTokenResult(AccessTokenResultStatus.RequiresRedirect, new AccessToken() { Value = token, Expires = new DateTimeOffset(DateTime.Now.AddDays(1)) }, "/login");
}
return await new ValueTask<AccessTokenResult>(accessTokenResult);
}
public ValueTask<AccessTokenResult> RequestAccessToken(AccessTokenRequestOptions options)
{
throw new NotImplementedException();
}
}
New question: How will I be able to call POST /auth now if this would work? I would get an error since I don't have a token yet for this TypedClient and adding another typed client isn't possible since I cannot give it a different name?

ASP.NET Core 2.0 Dynamic Authentication

When using .AddOpenIdConnect() within ConfigureServices, is it possible to change the ClientId and ClientSecret based on the host from the request?
I know the Startup itself doesn't have access to the HttpContext, but I was wondering if using a middleware would solve this where it would have access to the context.
I've tried following the below link, however my values are always null after it runs through the CustomAuthHandler
ASP.NET Core 2.0 authentication middleware
I believe you can achieve your goal assigning function to RedirectToIdentityProvider property.
Invoked before redirecting to the identity provider to authenticate.
This can be used to set ProtocolMessage.State that will be persisted
through the authentication process. The ProtocolMessage can also be
used to add or customize parameters sent to the identity provider.
public void ConfigureServices(IServiceCollection services)
{
services
.AddAuthentication()
.AddOpenIdConnect(options =>
{
options.Events.OnRedirectToIdentityProvider = context =>
{
// Retrieve identity from current HttpContext
var identity = context.HttpContext.User.Identity;
// Lookup for your client_id and client_secret
var clientId = "find your client id";
var clientSecret = "find your client secret";
// Assign client_id and client_secret
context.ProtocolMessage.ClientId = clientId;
context.ProtocolMessage.ClientSecret = clientSecret;
return Task.FromResult(0);
};
});
}
Related links
OpenIdConnectEvents.OnRedirectToIdentityProvider Property

Microsoft Authentication in ASP.NET Core 2 and Azure App Services

I have the following application at GitHub and have deployed it to https://stratml.services on an Azure App Service with Authentication defined as Microsoft Account with anymous requests requiring a Microsoft Account sign in. In "prod" this challenge occurs, however https://stratml.services/Home/IdentityName returns no content.
I have been following this and this however I do not want to use EntityFramework and from the latter's description it seems to imply if I configure my Authentication scheme correctly I do not have to.
This following code is in my Start class:
services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = MicrosoftAccountDefaults.AuthenticationScheme;
}).AddMicrosoftAccount(microsoftOptions =>
{
microsoftOptions.ClientId = Configuration["Authentication:AppId"];
microsoftOptions.ClientSecret = Configuration["Authentication:Key"];
microsoftOptions.CallbackPath = new PathString("/.auth/login/microsoftaccount/callback");
});
Update: Thanks to the first answer I was able to get, it now authorizes to Microsoft and attempts to feedback to my application however I receive the following error:
InvalidOperationException: No IAuthenticationSignInHandler is configured to handle sign in for the scheme: Cookies
Please visit https://stratml.services/Home/IdentityName and the GitHub has been updated.
services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = MicrosoftAccountDefaults.AuthenticationScheme;
}).AddCookie(option =>
{
option.Cookie.Name = ".myAuth"; //optional setting
}).AddMicrosoftAccount(microsoftOptions =>
{
microsoftOptions.ClientId = Configuration["Authentication:AppId"];
microsoftOptions.ClientSecret = Configuration["Authentication:Key"];
});
I have checked this issue on my side, based on my test, you could confgure your settings as follows:
Under the ConfigureServices method, add the cookie and MSA authentication services.
services.AddAuthentication(options =>
{
options.DefaultChallengeScheme = MicrosoftAccountDefaults.AuthenticationScheme;
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddCookie(option =>
{
option.Cookie.Name = ".myAuth"; //optional setting
})
.AddMicrosoftAccount(microsoftOptions =>
{
microsoftOptions.ClientId = Configuration["Authentication:AppId"];
microsoftOptions.ClientSecret = Configuration["Authentication:Key"];
});
Under the Configure method, add app.UseAuthentication().
TEST:
[Authorize]
public IActionResult Index()
{
return Content(this.User.Identity.Name);
}
When I checking your online website, I found that you are using the Authentication and authorization in Azure App Service and Authenticate with Microsoft account.
AFAIK, when using the app service authentication, the claims could not be attached to current user, you could retrieve the identity name via Request.Headers["X-MS-CLIENT-PRINCIPAL-NAME"] or you could follow this similar issue to manually attach all claims for current user.
In general, you could either manually enable authentication middle-ware in your application or just leverage the app service authentication provided by Azure without changing your code for enabling authentication. Moreover, you could Remote debugging web apps to troubleshoot with your application.
UPDATE:
For enable the MSA authentication in my code and test it when deployed to azure, I disabled the App Service Authentication, then deployed my application to azure web app. I opened a new incognito window and found that my web app could work as expected.
If you want to simulate the MSA login locally and use Easy Auth when deployed to azure, I assumed that you could set a setting value in appsettings.json and manually add the authentication middle-ware for dev and override the setting on azure, details you could follow here. And you could use the same application Id and configure the following redirect urls:
https://stratml.services/.auth/login/microsoftaccount/callback //for easy auth
https://localhost:44337/signin-microsoft //manually MSA authentication for dev locally
Moreover, you could follow this issue to manually attach all claims for current user. Then you could retrieve the user claims in the same way for the manually MSA authentication and Easy Auth.
If you are using App Service Authentication (EasyAuth), according to Microsoft documentation page:
App Service passes some user information to your application by using special headers. External requests prohibit these headers and will only be present if set by App Service Authentication / Authorization. Some example headers include:
X-MS-CLIENT-PRINCIPAL-NAME
X-MS-CLIENT-PRINCIPAL-ID
X-MS-TOKEN-FACEBOOK-ACCESS-TOKEN
X-MS-TOKEN-FACEBOOK-EXPIRES-ON
Code that is written in any language or framework can get the information that it needs from these headers. For ASP.NET 4.6 apps, the ClaimsPrincipal is automatically set with the appropriate values.
So basically, if you are using ASP.NET Core 2.0, you need to set the ClaimPrincipal manually. What you need to use in order to fetch this headers and set the ClaimsPrincipal is AuthenticationHandler
public class AppServiceAuthenticationOptions : AuthenticationSchemeOptions
{
public AppServiceAuthenticationOptions()
{
}
}
internal class AppServiceAuthenticationHandler : AuthenticationHandler<AppServiceAuthenticationOptions>
{
public AppServiceAuthenticationHandler(
IOptionsMonitor<AppServiceAuthenticationOptions> options,
ILoggerFactory logger,
UrlEncoder encoder,
ISystemClock clock) : base(options, logger, encoder, clock)
{
}
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
return Task.FromResult(FetchAuthDetailsFromHeaders());
}
private AuthenticateResult FetchAuthDetailsFromHeaders()
{
Logger.LogInformation("starting authentication handler for app service authentication");
if (Context.User == null || Context.User.Identity == null || Context.User.Identity.IsAuthenticated == false)
{
Logger.LogDebug("identity not found, attempting to fetch from the request headers");
if (Context.Request.Headers.ContainsKey("X-MS-CLIENT-PRINCIPAL-ID"))
{
var headerId = Context.Request.Headers["X-MS-CLIENT-PRINCIPAL-ID"][0];
var headerName = Context.Request.Headers["X-MS-CLIENT-PRINCIPAL-NAME"][0];
var claims = new Claim[] {
new Claim("http://schemas.microsoft.com/identity/claims/objectidentifier", headerId),
new Claim("name", headerName)
};
Logger.LogDebug($"Populating claims with id: {headerId} | name: {headerName}");
var identity = new GenericIdentity(headerName);
identity.AddClaims(claims);
var principal = new GenericPrincipal(identity, null);
var ticket = new AuthenticationTicket(principal,
new AuthenticationProperties(),
Scheme.Name);
Context.User = principal;
return AuthenticateResult.Success(ticket);
}
else
{
return AuthenticateResult.Fail("Could not found the X-MS-CLIENT-PRINCIPAL-ID key in the headers");
}
}
Logger.LogInformation("identity already set, skipping middleware");
return AuthenticateResult.NoResult();
}
}
You can then write an extension method for the middleware
public static class AppServiceAuthExtensions
{
public static AuthenticationBuilder AddAppServiceAuthentication(this AuthenticationBuilder builder, Action<AppServiceAuthenticationOptions> configureOptions)
{
return builder.AddScheme<AppServiceAuthenticationOptions, AppServiceAuthenticationHandler>("AppServiceAuth", "Azure App Service EasyAuth", configureOptions);
}
}
And add app.UseAuthentication(); in the Configure() method and put following in the ConfigureServices() method of your startup class.
services
.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = "AppServiceAuth";
options.DefaultChallengeScheme = "AppServiceAuth";
})
.AddAppServiceAuthentication(o => { });
If you need full claims details, you can retrieve it on the AuthenticationHandler by making request to /.auth/me and use the same cookies that you've received on the request.

Identity Server and web api for user management

I'm using Identity Server3 for my project, I currently have a website and api being protected by the Id server, this is working fine however because I'm storing the users in the Id Server database I can't really change any user's data from the website like changing the profile picture or any claim value.
In order to solve this I'm thinking in creating an API on top of IdServer, this API will manage the users, changing a password, retrieving users or changing anything related to a user basically, I want to create this API on the sample project where I have my IdServer using Owin mapping.
Right now I have my idServer in the /identity route like this
public class Startup
{
public void Configuration(IAppBuilder app)
{
Log.Logger = new LoggerConfiguration().MinimumLevel.Debug().WriteTo.Trace().CreateLogger();
app.Map("/identity", idserverApp =>
{
var efConfig = new EntityFrameworkServiceOptions
{
ConnectionString = "IdSvr3Config"
};
var options = new IdentityServerOptions
{
SiteName = "Identity server",
IssuerUri = ConfigurationManager.AppSettings["idserver:stsIssuerUri"],
PublicOrigin = ConfigurationManager.AppSettings["idserver:stsOrigen"],
SigningCertificate = Certificate.Get(),
Factory = Factory.Configure(efConfig),
AuthenticationOptions = AuthOptions.Configure(app),
CspOptions = new CspOptions { Enabled = false },
EnableWelcomePage=false
};
new TokenCleanup(efConfig, 3600 * 6).Start();
idserverApp.UseIdentityServer(options);
});
app.UseIdServerApi();
}
}
My api "middleware" is as this
**public static class IdServerApiExtensions
{
public static void UseIdServerApi(this IAppBuilder app, IdServerApiOptions options = null)
{
if (options == null)
options = new IdServerApiOptions();
if (options.RequireAuthentication)
{
var dic = app.Properties;
JwtSecurityTokenHandler.InboundClaimTypeMap = new Dictionary<string, string>();
app.UseIdentityServerBearerTokenAuthentication(
new IdentityServerBearerTokenAuthenticationOptions
{
Authority = "https://localhost:44302/identity", //here is the problem, it says not found even though I already mapped idServer to /identity
RequiredScopes = new[]
{
"idserver-api"
},
ValidationMode=ValidationMode.ValidationEndpoint
});
}
var config = new HttpConfiguration();
WebApiConfig.Register(config,options.RequireAuthentication);
app.UseNinjectMiddleware(() => NinjectConfig.CreateKernel.Value);
app.UseNinjectWebApi(config);
app.Use<IdServerApiMiddleware>(options);
}
}**
I know is possible I just don't know if it is a good idea to have the api on the same project or not and also I don't know how to do it.
Is it a good idea to create this API to manage the users? If not what could I use?
How can I create the proper settings to fire up the API under /api and at the same time use IdServer to protect this api?
Update
Adding ValidationMode=ValidationMode.ValidationEndpoint, fixed my problem, thanks to Scott Brady
Thanks
The general advice from the Identity Server team is to run any admin pages or API as a separate project (Most recent example). Best practice would be only to give your Identity Server and identity management applications access to your identity database/store.
To manage your users, yes, you could write your own API. Other options would be to contain it to a single MVC website or to use something like Identity Manager.
You can still use the same application approach however, using the OWIN map. To secure this you could use the IdentityServer3.AccessTokenValidation package, using code such as:
app.UseIdentityServerBearerTokenAuthentication(new IdentityServerBearerTokenAuthenticationOptions
{
Authority = ConfigurationManager.AppSettings["idserver:stsOrigen"],
RequiredScopes = new[] { "adminApi" },
ValidationMode = ValidationMode.ValidationEndpoint
});

Categories