ASP.NET Core 2.0 Dynamic Authentication - c#

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

Related

Update asp.net core Authentication outside of Startup.cs

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();
}
}

Is there any way to change client_id and client_secret on the fly in IdentityServer4

We created a multi-tenant SaaS application and I need to change client_id dynamically. How can i change client_id and client_secret in OnRedirectToIdentityProvider event ?
As mentioned in the comment for #d_f it's a wrong approach.
The right approach to do that is passing the tenant id from client to IdentityServer using the event : OnRedirectToIdentityProvider
Example
options.Events.OnRedirectToIdentityProvider = (loginRedirectContext) =>
{
var servicesProvider = loginRedirectContext.HttpContext.RequestServices;
var tenantInfo = servicesProvider.GetRequiredService<IRequestContextTenantInfo>();
loginRedirectContext.ProtocolMessage.SetParameter("tenantId", tenantInfo.Id);
return Task.FromResult(0);
};
In The Identity Server You can read it in Login Action method like this :
var _interaction = context.RequestServices.GetRequiredService<IIdentityServerInteractionService>();
var returnUrl = context.Request.Query["ReturnUrl"].ToString();
var authContext = await _interaction.GetAuthorizationContextAsync(returnUrl);
tenantId = authContext.Parameters["tenantid"];
Now based on tenant id you have to know which tenant you have to connect with to verify the login process.
It's also a good practice to inject a specific custom middleware in IdentityServer4 to handle the multitenancy in the early stage in the request pipeline
Example in Configure method in Startup :
app.UseMultiTenancy();
app.UseIdentityServer();
and UseMultiTenancy() method will read the tenant parameter that is sent from client.

How to integrate swagger with Azure Active Directory OAuth

I'm trying to setup Swagger in my AspNetCore 2.1 application using Azure Active Directory V2 but I cannot seem to get it right. I am able to configure the setup so that swagger prompts, redirects and successfully authenticates my client/user but when passing the bearer token to the server results in the error Bearer error="invalid_token", error_description="The signature is invalid". I have created a GitHub repository with the project I am trying to get work with all its configuration (https://github.com/alucard112/auth-problem)
I have managed to get the V1 endpoint working, by setting the resource to the Client Id of the AAD app, which results in the JWT token having the 'aud' set to the app client Id. In the V2 endpoint the 'aud' is being set to what I think is the Graph API resource '00000003-0000-0000-c000-000000000000'. I believe this is my problem at the moment, although am not 100 % sure. The V2 endpoints don't seem to have a way to define the audience like the V1 did unless of course there is some oversight from my side.
My Startup file is structured as follows:
The authentication is setup as the following:
services.AddAuthentication(AzureADDefaults.BearerAuthenticationScheme)
.AddAzureADBearer(options => Configuration.Bind("AzureAd", options));
services.Configure<JwtBearerOptions>(AzureADDefaults.JwtBearerAuthenticationScheme, options =>
{
options.Authority = $"https://login.microsoftonline.com/{tenantId}";
options.TokenValidationParameters = new TokenValidationParameters
{
// In multi-tenant apps you should disable issuer validation:
ValidateIssuer = false,
// In case you want to allow only specific tenants,
// you can set the ValidIssuers property to a list of valid issuer ids
// or specify a delegate for the IssuerValidator property, e.g.
// IssuerValidator = (issuer, token, parameters) => {}
// the validator should return the issuer string
// if it is valid and throw an exception if not
};
});
And the swagger is setup as follows:
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new Info
{
Title = "Protected Api",
});
c.OperationFilter<SecurityRequirementsOperationFilter>();
//IMATE - StevensW
// Define the OAuth2.0 scheme that's in use (i.e. Implicit Flow)
c.AddSecurityDefinition("oauth2", new OAuth2Scheme
{
Type = "oauth2",
Flow = "implicit",
AuthorizationUrl = $"https://login.microsoftonline.com/{tenantId}/oauth2/v2.0/authorize",
TokenUrl = $"https://login.microsoftonline.com/common/{tenantId}/v2.0/token",
Scopes = new Dictionary<string, string>
{
{ "openid", "Unsure" },
{ "profile", "Also Unsure" }
}
});
});
app.UseSwagger();
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1");
c.OAuthClientId(Configuration.GetValue<string>("AzureAd:ClientId"));
c.OAuthAppName("Protected API");
// c.OAuthUseBasicAuthenticationWithAccessCodeGrant();
// NEVER set the client secret here. It will ve exposed in the html of the swagger page if you "view source" and its not needed for OpenID Auth
// c.OAuthClientSecret(Configuration.GetValue<string>("AzureAd:ClientId"));
});
I am hoping to configure the swagger UI to use AAD's V2 endpoint and allow for a multi-tenant login that allows successfully authenticated API calls to be executed. Any help or direction would be greatly appreciated.
I ended up fixing the problem I was having. Working through this post helped me understand my mistakes.
The first mistake was my actual AAD app registration. I had not set a scope for the application under "Expose an API". Because they deprecated the resource property in V2, the way you would set the resource was to create a scope with the format api"//{application ID}/{scope_name}. After I made this change my AAD application was now correctly configured.
After that, I needed to add an additional section to my startup file:
return services.Configure<JwtBearerOptions>(AzureADDefaults.JwtBearerAuthenticationScheme, options =>
{
// This is an Azure AD v2.0 Web API
options.Authority += "/v2.0";
// The valid audiences are both the Client ID (options.Audience) and api://{ClientID}
options.TokenValidationParameters.ValidAudiences = new string[] { options.Audience, $"api://{options.Audience}" };
options.TokenValidationParameters.ValidateIssuer = false;
});
Note: the link above provided an alternative solution to turning off the validation of the issuer if anyone is interested.
My AppSettings file was also simplified by only needing to define the Instance, TenantId, and ClientId.
Then from a swagger perspective, I just needed to add an additional scope to the security definition matching the one I created in my AAD application.
c.AddSecurityDefinition("oauth2", new OAuth2Scheme
{
Type = "oauth2",
Flow = "implicit",
AuthorizationUrl = "https://login.microsoftonline.com/common/oauth2/v2.0/authorize",
TokenUrl = "https://login.microsoftonline.com/common/oauth2/v2.0/token",
Scopes = new Dictionary<string, string>
{
{ "openid", "Sign In Permissions" },
{ "profile", "User Profile Permissions" },
{ $"api://{clientId}/access_as_user", "Application API Permissions" }
}
});
After these changes my application is now working as expected.
for v2 endpoint, update the accessTokenAcceptedVersion in Manifest of AAD from null to 2. It will work.

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.

ASP.NET Core authenticating with Azure Active Directory and persisting custom Claims across requests

I have a default ASP.NET Core website created within Visual Studio 2017. I have chosen to authenticate using an Azure Active Directory.
I run the site and can successfully login using an account in the Active Directory.
I can retrieve Claim information provided by Active Directory, e.g. by calling the following line I get the 'name'.
User.Claims.FirstOrDefault(c => c.Type == "name")?.Value;
I want to add a custom claim - CompanyId = 123456 for the logged in user.
I'm able to add a custom claim however it is only available on the page where the claim is set.
Claim claim = new Claim("CompanyId", "123456", ClaimValueTypes.String);
((ClaimsIdentity)User.Identity).AddClaim(claim);
My understanding is that I somehow need to update the token that has been issued by Active Directory or set the claim before the token is issued. I'm unsure how to do this.
I suspect this needs to be done in the AccountController at SignIn()
// GET: /Account/SignIn
[HttpGet]
public IActionResult SignIn()
{
return Challenge(
new AuthenticationProperties { RedirectUri = "/" }, OpenIdConnectDefaults.AuthenticationScheme);
}
I've read numerous articles and samples about this scenario (including https://github.com/ahelland/AADGuide-CodeSamples/tree/master/ClaimsWebApp) however have not managed to solve how to persist the Claim across requests.
I have successfully managed to persist custom Claims using ASP.NET Identity as the Authentication Provider, but this appears to be because the custom Claim is saved to the database..
OnTokenValidated offers you the chance to modify the ClaimsIdentity obtained from the incoming token , code below is for your reference :
private Task TokenValidated(TokenValidatedContext context)
{
Claim claim = new Claim("CompanyId", "123456", ClaimValueTypes.String);
(context.Ticket.Principal.Identity as ClaimsIdentity).AddClaim(claim);
return Task.FromResult(0);
}
Setting the OpenIdConnectEvents:
Events = new OpenIdConnectEvents
{
OnRemoteFailure = OnAuthenticationFailed,
OnAuthorizationCodeReceived = OnAuthorizationCodeReceived,
OnTokenValidated = TokenValidated
}
Then in controller using :
var companyId= User.Claims.FirstOrDefault(c => c.Type == "CompanyId")?.Value;
For those who would like more detail, the code provided is placed in Startup.cs
In the Configure method add/edit:
app.UseOpenIdConnectAuthentication(new OpenIdConnectOptions
{
ClientId = Configuration["Authentication:AzureAd:ClientId"],
Authority = Configuration["Authentication:AzureAd:AADInstance"] + Configuration["Authentication:AzureAd:TenantId"],
CallbackPath = Configuration["Authentication:AzureAd:CallbackPath"],
Events = new OpenIdConnectEvents
{
OnTokenValidated = TokenValidated
}
});
The private Task TokenValidated method is in the body of Startup.cs
The following sample is a good reference.
https://github.com/Azure-Samples/active-directory-dotnet-webapp-openidconnect-aspnetcore-v2/blob/master/WebApp-OpenIDConnect-DotNet/Startup.cs

Categories