tracking down cause of 401 response from EF call - c#

I'm getting the following exception when attempting to browse to my site :
The configuration/staticcontent endpoint is implemented like so:
[HttpGet("staticcontent")]
public async Task<IActionResult> GetStaticContent()
{
return Ok(this.mapper.Map<StaticContentValueDto[]>(await this.staticContentValuesProvider.GetStaticContentValues()));
}
.. .and the implementation of GetStaticContentValues() as follows:
public async Task<IEnumerable<StaticContentValue>> GetStaticContentValues()
{
return await this.dbContext.StaticContentValues.ToArrayAsync();
}
I suspect there might be an issue with AD authentication?
public void ConfigureServices(IServiceCollection services)
{
services.AddApplicationInsightsTelemetry();
services.AddSpaStaticFiles(configuration => { configuration.RootPath = "ClientApp/dist"; });
services.AddMvcCore()
.AddAuthorization();
services.AddControllers().AddNewtonsoftJson(o=>
{
o.SerializerSettings.ContractResolver = new DefaultContractResolver();
o.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore;
});
string connection = Configuration.GetConnectionString("DefaultConnection");
string reportingConnection = Configuration.GetConnectionString("ReportingConnection");
services.AddDbContext<PnbIdentityDbContext>(options =>
options.UseSqlServer(connection));
//20 or so other adddbcontext for sql server here
//20 or so other adddbcontext for sql server here
services.AddIdentity<PnbIdentityUser, PnbIdentityRole>(options => {
options.Password.RequireDigit = true;
options.Password.RequiredLength = 8;
options.Password.RequireNonAlphanumeric = false;
options.Password.RequireUppercase = false;
options.Password.RequireLowercase = false;
})
.AddEntityFrameworkStores<PnbIdentityDbContext>()
.AddDefaultTokenProviders();
services.AddAutoMapper(typeof(CapsAutoMapperProfile));
SessionConfigurator.Configure(services);
AuthConfigurator.Configure(services, Configuration["Identity:TokenSecret"]);
DiConfigurator.Configure(services);
HangfireConfigurator.Configure(services, connection);
}
Please note that Identity:TokenSecret is set in appsettings.json.
What am I doing wrong? What is the reason for the 401 response?

any chance you can point me to where i can configure authentication ?
Thank you Ken W MSFT , Posting your suggestion as answer to help other community members.
"Yes, it is possible. Azure Static Web Apps provides a streamlined authentication experience. By default, you have access to a series of pre-configured providers, or the option to register a custom provider.
Any user can authenticate with an enabled provider. Once logged in, users belong to the anonymous and authenticated roles by default.
Authorized users gain access to restricted routes by rules defined in the staticwebapp.config.json file. Users join custom roles via provider-specific invitations, or through a custom Azure Active
Directory provider registration.
All authentication providers are enabled by default.
To restrict an authentication provider, block access with a custom route rule.
Pre-configured providers include:
Azure Active Directory
GitHub
Twitter
Reference:
https://learn.microsoft.com/en-us/azure/static-web-apps/authentication-authorization ''

Related

How do I get Azure Active Directory App Roles to show in Claims?

I'm using Azure Active Directory to provide authentication to the Backoffice on my website running Umbraco version 11.0.
This is working nicely and I can log in but I want to improve the experience by using app roles within Azure to manage the user's group within Umbraco.
My Azure setup
I've created an App Registration within Azure with the following configuration:
Added a Redirection URI:
URI: https://localhost:44391/umbraco-signin-microsoft/
Enabled Access tokens (used for implicit flows)
Enabled ID tokens (used for implicit and hybrid flows)
Supported account types: Accounts in this organizational directory only (Example only - Single tenant)
Added App Roles
Administrator
Editor
In Enterprise Applications, I've also added the App Roles above to my users:
My code
Login Provider
namespace Example.Api.Features.Authentication.Extensions;
public static class UmbracoBuilderExtensions
{
public static IUmbracoBuilder ConfigureAuthentication(this IUmbracoBuilder builder)
{
builder.Services.ConfigureOptions<OpenIdConnectBackOfficeExternalLoginProviderOptions>();
builder.AddBackOfficeExternalLogins(logins =>
{
const string schema = MicrosoftAccountDefaults.AuthenticationScheme;
logins.AddBackOfficeLogin(
backOfficeAuthenticationBuilder =>
{
backOfficeAuthenticationBuilder.AddMicrosoftAccount(
// the scheme must be set with this method to work for the back office
backOfficeAuthenticationBuilder.SchemeForBackOffice(OpenIdConnectBackOfficeExternalLoginProviderOptions.SchemeName) ?? string.Empty,
options =>
{
//By default this is '/signin-microsoft' but it needs to be changed to this
options.CallbackPath = "/umbraco-signin-microsoft/";
//Obtained from the AZURE AD B2C WEB APP
options.ClientId = "CLIENT_ID";
//Obtained from the AZURE AD B2C WEB APP
options.ClientSecret = "CLIENT_SECRET";
options.TokenEndpoint = $"https://login.microsoftonline.com/TENANT/oauth2/v2.0/token";
options.AuthorizationEndpoint = $"https://login.microsoftonline.com/TENANT/oauth2/v2.0/authorize";
});
});
});
return builder;
}
}
Auto-linking accounts
namespace Example.Api.Features.Configuration;
public class OpenIdConnectBackOfficeExternalLoginProviderOptions : IConfigureNamedOptions<BackOfficeExternalLoginProviderOptions>
{
public const string SchemeName = "OpenIdConnect";
public void Configure(string name, BackOfficeExternalLoginProviderOptions options)
{
if (name != "Umbraco." + SchemeName)
{
return;
}
Configure(options);
}
public void Configure(BackOfficeExternalLoginProviderOptions options)
{
options.AutoLinkOptions = new ExternalSignInAutoLinkOptions(
// must be true for auto-linking to be enabled
autoLinkExternalAccount: true,
// Optionally specify default user group, else
// assign in the OnAutoLinking callback
// (default is editor)
defaultUserGroups: new[] { Constants.Security.EditorGroupAlias },
// Optionally you can disable the ability to link/unlink
// manually from within the back office. Set this to false
// if you don't want the user to unlink from this external
// provider.
allowManualLinking: false
)
{
// Optional callback
OnAutoLinking = (autoLinkUser, loginInfo) =>
{
// You can customize the user before it's linked.
// i.e. Modify the user's groups based on the Claims returned
// in the externalLogin info
autoLinkUser.IsApproved = true;
},
OnExternalLogin = (user, loginInfo) =>
{
// You can customize the user before it's saved whenever they have
// logged in with the external provider.
// i.e. Sync the user's name based on the Claims returned
// in the externalLogin info
return true; //returns a boolean indicating if sign in should continue or not.
}
};
// Optionally you can disable the ability for users
// to login with a username/password. If this is set
// to true, it will disable username/password login
// even if there are other external login providers installed.
options.DenyLocalLogin = true;
// Optionally choose to automatically redirect to the
// external login provider so the user doesn't have
// to click the login button. This is
options.AutoRedirectLoginToExternalProvider = true;
}
}
In this file, I'd ideally do as the comment says and i.e. Modify the user's groups based on the Claims returned in the externalLogin info.
Also registered in my Startup file
services.AddUmbraco(_env, _config)
.AddBackOffice()
.AddWebsite()
.AddComposers()
.ConfigureAuthentication()
.Build();
I've attempted to give the following permissions to the application, with no luck:
Current state of play is that I can login just fine but if I debug externalInfo, there's nothing in there about the users having either the Administrator or Editor App Role as configured above.
My gut feeling is that I'm missing something with the Azure Active Directory setup but I've tried a few different configurations and can't seem to get the App Roles to come back.
Thanks,
Ben
EDIT - 15.02.2023:
I can see that the roles come back when I hit the https://login.microsoftonline.com/{tenantId}/oauth2/v2.0/token endpoint using client_credentials as the grant_type. It looks like the .NET application using authorization_code instead. I've decoded the token retrieved from this and it doesn't contain the roles.
I wonder if there's some kind of configuration on the .NET application that allows me to add the roles.
To get App Roles in token claims, you can use client credentials flow to generate access tokens by granting admin consent.
I tried to reproduce the same in my environment via Postman and got the below results:
I registered one Azure AD web application and created App roles like below:
Now I assigned these App roles to users under it's Enterprise application like below:
Add these App roles in API permissions of application like below:
You can see App roles under Application permissions like below:
Make sure to grant admin consent to above permissions like this:
While generating access token, scope should be your Application ID URI ending with /.default
Now, I generated access token using client credentials flow via Postman with below parameters:
POST https://login.microsoftonline.com/<tenantID>/oauth2/v2.0/token
client_id: <appID>
grant_type:client_credentials
scope: api://<appID>/.default
client_secret: secret
Response:
When I decoded the above token in jwt.ms, I got App roles in roles claim successfully like below:
Note that App roles are Application permissions that will work only with flows like client credentials which do not involve user interaction.
So, if you use delegated flows like authorization code flow, username password flow, etc..., you won't get App roles in token claims.
UPDATE:
You can use below c# code in getting access token from client credentials flow like this:
using Microsoft.Identity.Client;
var clientID = "bbb739ad-98a4-4566-8408-dxxxxxxxx3b";
var clientSecret = "K.k8Q~hwtxxxxxxxxxxxxxxxU";
var tenantID = "fb134080-e4d2-45f4-9562-xxxxxx";
var authority = $"https://login.microsoftonline.com/{tenantID}";
var clientApplication = ConfidentialClientApplicationBuilder.Create(clientID)
.WithClientSecret(clientSecret)
.WithAuthority(authority)
.Build();
var scopes = new string[] { "api://bbb739ad-98a4-4566-8408-xxxxxx/.default" };
var authenticationResult = await clientApplication.AcquireTokenForClient(scopes)
.ExecuteAsync()
.ConfigureAwait(false);
var accesstoken = authenticationResult.AccessToken;
Console.WriteLine(accesstoken);
Response:
When I decoded the above token, it has roles claim with App roles like below:
To solve this, I ended up swapping out the AddMicrosoftAccount AuthenticationBuilder in favour of AddOpenIdConnect. This appears to respect the claims in the tokens.
This is the code I am now using in the ConfigureAuthentication method.
public static IUmbracoBuilder ConfigureAuthentication(this IUmbracoBuilder builder)
{
// Register OpenIdConnectBackOfficeExternalLoginProviderOptions here rather than require it in startup
builder.Services.ConfigureOptions<OpenIdConnectBackOfficeExternalLoginProviderOptions>();
builder.AddBackOfficeExternalLogins(logins =>
{
logins.AddBackOfficeLogin(
backOfficeAuthenticationBuilder =>
{
backOfficeAuthenticationBuilder.AddOpenIdConnect(
// The scheme must be set with this method to work for the back office
backOfficeAuthenticationBuilder.SchemeForBackOffice(OpenIdConnectBackOfficeExternalLoginProviderOptions.SchemeName),
options =>
{
options.CallbackPath = "/umbraco-signin-microsoft/";
// use cookies
options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
// pass configured options along
options.Authority = "https://login.microsoftonline.com/{tenantId}/v2.0";
options.ClientId = "{clientId}";
options.ClientSecret = "{clientSecret}";
// Use the authorization code flow
options.ResponseType = OpenIdConnectResponseType.Code;
options.AuthenticationMethod = OpenIdConnectRedirectBehavior.RedirectGet;
// map claims
options.TokenValidationParameters.NameClaimType = "name";
options.TokenValidationParameters.RoleClaimType = "role";
options.RequireHttpsMetadata = true;
options.GetClaimsFromUserInfoEndpoint = true;
options.SaveTokens = true;
options.UsePkce = true;
options.Scope.Add("email");
});
});
});
return builder;
}

How to Get token from Duende Identity Server (IdentityServer4)

There are a few days I am reading the Duende Identity server (IdentityServer4) so I know about the different concepts and their usages such as Scopes, Resources, Client ...
The area I am confused about it is the clients. So I integrated the AspIdentity as an ApplicationUser in the IdentityServer (you can find the configs below in the code sections) but when I want to call the /connect/token which is a pre-defined endpoint from Duende, it needs to add ClientId and Secret but I want to use Username and the password of my registered user.
So the idea that comes to my mind is to Create a custom endpoint: after validating the user's credentials using SignInManager then I will find the Users client and then sign in to the Duende IdentityServer however I tried to do that but it is a bit inconvenience way to have an HTTP-call again to the same service to get the token of the User.
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlite(connectionString));
builder.Services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
builder.Services.AddSwaggerGen();
builder.Services
.AddIdentityServer(options =>
{
options.Events.RaiseErrorEvents = true;
options.Events.RaiseInformationEvents = true;
options.Events.RaiseFailureEvents = true;
options.Events.RaiseSuccessEvents = true;
options.EmitStaticAudienceClaim = true;
})
.AddAspNetIdentity<ApplicationUser>()
.AddConfigurationStore(options =>
{
options.ConfigureDbContext = b =>
b.UseSqlite(connectionString, dbOpts => dbOpts.MigrationsAssembly(typeof(Program).Assembly.FullName));
})
.AddOperationalStore(options =>
{
options.ConfigureDbContext = b =>
b.UseSqlite(connectionString, dbOpts => dbOpts.MigrationsAssembly(typeof(Program).Assembly.FullName));
options.EnableTokenCleanup = true;
options.RemoveConsumedTokens = true;
});
builder.Services.AddAuthentication();
if I can solve this issue in a convenient way so the other steps are very obvious and straightforward.
The clientID and secrets are meant to identify the application that wants to connect to IdentityServer and not the user; why can you not use clientID/secret for what it is intended for?
Also, the main purpose of OpenID connect is to not let the client application ever touch or see the user's username/password. That is why we delegate the authentication to IdentityServer.

Use .NET Core Identity with an API

i've created an API and set up JWT auth from the same API (I chose not to use IdentityServer4).
I did this through services.AddAuthentication
And then I created tokens in the controller and it works.
However I now want to add registration etc. But i prefer not to write my own code for hashing passwords, handling registration emails etc.
So I came across ASP.NET Core Identity and it seems like what I need, except from that it adds some UI stuff that I dont need (because its just an API and the UI i want completely independent).
But on MSDN is written:
ASP.NET Core Identity adds user interface (UI) login functionality to
ASP.NET Core web apps. To secure web APIs and SPAs, use one of the
following:
Azure Active Directory
Azure Active Directory B2C (Azure AD B2C)
IdentityServer4
So is it really a bad idea to use Core Identity just for hashing and registration logic for an API? Cant I just ignore the UI functionality? It's very confusing because I'd rather not use IdentityServer4 or create my own user management logic.
Let me just get off my chest that the bundling Identity does with the UI, the cookies and the confusing various extension methods that add this or that, but don't add this or that, is pretty annoying, at least when you build modern web APIs that need no cookies nor UI.
In some projects I also use manual JWT token generation with Identity for the membership features and user/password management.
Basically the simplest thing to do is to check the source code.
AddDefaultIdentity() adds authentication, adds the Identity cookies, adds the UI, and calls AddIdentityCore(); but has no support for roles:
public static IdentityBuilder AddDefaultIdentity<TUser>(this IServiceCollection services, Action<IdentityOptions> configureOptions) where TUser : class
{
services.AddAuthentication(o =>
{
o.DefaultScheme = IdentityConstants.ApplicationScheme;
o.DefaultSignInScheme = IdentityConstants.ExternalScheme;
})
.AddIdentityCookies(o => { });
return services.AddIdentityCore<TUser>(o =>
{
o.Stores.MaxLengthForKeys = 128;
configureOptions?.Invoke(o);
})
.AddDefaultUI()
.AddDefaultTokenProviders();
}
AddIdentityCore() is a more stripped down version that only adds basic services, but it doesn't even add authentication, and also no support for roles (here you can already see what individual services are added, to change/override/remove them if you want):
public static IdentityBuilder AddIdentityCore<TUser>(this IServiceCollection services, Action<IdentityOptions> setupAction)
where TUser : class
{
// Services identity depends on
services.AddOptions().AddLogging();
// Services used by identity
services.TryAddScoped<IUserValidator<TUser>, UserValidator<TUser>>();
services.TryAddScoped<IPasswordValidator<TUser>, PasswordValidator<TUser>>();
services.TryAddScoped<IPasswordHasher<TUser>, PasswordHasher<TUser>>();
services.TryAddScoped<ILookupNormalizer, UpperInvariantLookupNormalizer>();
services.TryAddScoped<IUserConfirmation<TUser>, DefaultUserConfirmation<TUser>>();
// No interface for the error describer so we can add errors without rev'ing the interface
services.TryAddScoped<IdentityErrorDescriber>();
services.TryAddScoped<IUserClaimsPrincipalFactory<TUser>, UserClaimsPrincipalFactory<TUser>>();
services.TryAddScoped<UserManager<TUser>>();
if (setupAction != null)
{
services.Configure(setupAction);
}
return new IdentityBuilder(typeof(TUser), services);
}
Now that kind of makes sense so far, right?
But enter AddIdentity(), which appears to be the most bloated, the only one that supports roles directly, but confusingly enough it doesn't seem to add the UI:
public static IdentityBuilder AddIdentity<TUser, TRole>(
this IServiceCollection services,
Action<IdentityOptions> setupAction)
where TUser : class
where TRole : class
{
// Services used by identity
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = IdentityConstants.ApplicationScheme;
options.DefaultChallengeScheme = IdentityConstants.ApplicationScheme;
options.DefaultSignInScheme = IdentityConstants.ExternalScheme;
})
.AddCookie(IdentityConstants.ApplicationScheme, o =>
{
o.LoginPath = new PathString("/Account/Login");
o.Events = new CookieAuthenticationEvents
{
OnValidatePrincipal = SecurityStampValidator.ValidatePrincipalAsync
};
})
.AddCookie(IdentityConstants.ExternalScheme, o =>
{
o.Cookie.Name = IdentityConstants.ExternalScheme;
o.ExpireTimeSpan = TimeSpan.FromMinutes(5);
})
.AddCookie(IdentityConstants.TwoFactorRememberMeScheme, o =>
{
o.Cookie.Name = IdentityConstants.TwoFactorRememberMeScheme;
o.Events = new CookieAuthenticationEvents
{
OnValidatePrincipal = SecurityStampValidator.ValidateAsync<ITwoFactorSecurityStampValidator>
};
})
.AddCookie(IdentityConstants.TwoFactorUserIdScheme, o =>
{
o.Cookie.Name = IdentityConstants.TwoFactorUserIdScheme;
o.ExpireTimeSpan = TimeSpan.FromMinutes(5);
});
// Hosting doesn't add IHttpContextAccessor by default
services.AddHttpContextAccessor();
// Identity services
services.TryAddScoped<IUserValidator<TUser>, UserValidator<TUser>>();
services.TryAddScoped<IPasswordValidator<TUser>, PasswordValidator<TUser>>();
services.TryAddScoped<IPasswordHasher<TUser>, PasswordHasher<TUser>>();
services.TryAddScoped<ILookupNormalizer, UpperInvariantLookupNormalizer>();
services.TryAddScoped<IRoleValidator<TRole>, RoleValidator<TRole>>();
// No interface for the error describer so we can add errors without rev'ing the interface
services.TryAddScoped<IdentityErrorDescriber>();
services.TryAddScoped<ISecurityStampValidator, SecurityStampValidator<TUser>>();
services.TryAddScoped<ITwoFactorSecurityStampValidator, TwoFactorSecurityStampValidator<TUser>>();
services.TryAddScoped<IUserClaimsPrincipalFactory<TUser>, UserClaimsPrincipalFactory<TUser, TRole>>();
services.TryAddScoped<IUserConfirmation<TUser>, DefaultUserConfirmation<TUser>>();
services.TryAddScoped<UserManager<TUser>>();
services.TryAddScoped<SignInManager<TUser>>();
services.TryAddScoped<RoleManager<TRole>>();
if (setupAction != null)
{
services.Configure(setupAction);
}
return new IdentityBuilder(typeof(TUser), typeof(TRole), services);
}
All in all what you probably need is the AddIdentityCore(), plus you have to use AddAuthentication() on your own.
Also, if you use AddIdentity(), be sure to run your AddAuthentication() configuration after calling AddIdentity(), because you have to override the default authentication schemes (I ran into problems related to this, but can't remember the details).
(Another tidbit of information that might be interesting for people reading this is the distinction between SignInManager.PasswordSignInAsync(), SignInManager.CheckPasswordSignInAsync() and UserManager.CheckPasswordAsync(). These are all public methods you can find and call for authorization purposes. PasswordSignInAsync() implements two-factor signin (also sets cookies; probably only when using AddIdentity() or AddDefaultIdentity()) and calls CheckPasswordSignInAsync(), which implements user lockout handling and calls UserManager.CheckPasswordAsync(), which just checks the password. So to get a proper authentication it's better not to call UserManager.CheckPasswordAsync() directly, but to do it through CheckPasswordSignInAsync(). But, in a single-factor JWT token scenario, calling PasswordSignInAsync() is probably not needed (and you can run into redirect issues). If you have included UseAuthentication()/AddAuthentication() in the Startup with the proper JwtBearer token schemes set, then the next time the client sends a request with a valid token attached, the authentication middleware will kick in, and the client will be 'signed in'; i.e. any valid JWT token will allow client to access controller actions protected with [Authorize].)
And IdentityServer is thankfully completely separate from Identity. In fact the decent implementation of IdentityServer is to use it as a standalone literal identity server that issues tokens for your services. But since ASP.NET Core has no token generation capability built-in, a lot of people end up running this bloated server inside their apps just to be able to use JWT tokens, even though they have a single app and they have no real use for a central authority. I don't mean to hate on it, it's a really great solution with a lot of features, but it would be nice to have something simpler for the more common use cases.
You just configure Identity to use JWT bearer tokens. In my case I'm using encrypted token, so depending on your use-case you may want to adjust the configuration:
// In Startup.ConfigureServices...
services.AddDefaultIdentity<ApplicationUser>(
options =>
{
// Configure password options etc.
})
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
// Configure authentication
services.AddAuthentication(
opt =>
{
opt.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
opt.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
opt.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = false,
ValidateAudience = false,
TokenDecryptionKey =
new SymmetricSecurityKey(Encoding.UTF8.GetBytes("my key")),
RequireSignedTokens = false, // False because I'm encrypting the token instead
ValidateLifetime = true,
ClockSkew = TimeSpan.Zero
};
});
// Down in Startup.Configure add authn+authz middlewares
app.UseAuthentication();
app.UseAuthorization();
Then generate a token when the user wants to sign in:
var encKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("my key"));
var encCreds = new EncryptingCredentials(encKey, SecurityAlgorithms.Aes256KW, SecurityAlgorithms.Aes256CbcHmacSha512);
var claimsIdentity = await _claimsIdentiyFactory.CreateAsync(user);
var desc = new SecurityTokenDescriptor
{
Subject = claimsIdentity,
Expires = DateTime.UtcNow.AddMinutes(_configuration.Identity.JwtExpirationMinutes),
Issuer = _configuration.Identity.JwtIssuer,
Audience = _configuration.Identity.JwtAudience,
EncryptingCredentials = encCreds
};
var token = new JwtSecurityTokenHandler().CreateEncodedJwt(desc);
// Return it to the user
You can then use the UserManager to handle creating new users and retrieving users, while SignInManager can be used to check for valid login/credentials before generating the token.

Azure Active Directory always redirects to '~/.auth/login/done' when deployed to Azure despite working on localhost

So I'm developing an ASP.NET Core app (.NET Core 2.0), hosted as App Service on Azure. I want to implement authentication with Azure AD, using a single tenant (so only account from our company). I actually added all necessary code, registered the app and configured everything in App Service (at least I think so) and it even works without any problems when I run it locally. The problem occur when I publish the app to Azure and try to log in there. Instead of being redirected to the view of my choice, I am being redirected to '~/.auth/login/done' and I see this:
https://imgur.com/a/6OeKUNy
So as I said, I registered the app and added app url and reply urls, they look like this:
[app_name].azurewebsites.net/.auth/login/aad/callback
https://localhost:44359/.auth/login/aad/callback
I configured the app service itself in Authentication/Authorization section, with advanced settings. Currently the fields Client Secret and Allowed token audiences are empty.
I don't want user to be authenticated right after he enters the website, but when he clicks a certain button, so I set "Allow anonymous requests(no action)".
I added necessary code for it to work:
AccountController:
[Authorize]
[Route("[controller]/[action]")]
public class AccountController : Controller
{
private readonly OmsIntegrationContext _context;
private readonly IUserService _userService;
public AccountController(OmsIntegrationContext context, IUserService userservice)
{
_context = context;
_userService = userservice;
}
[HttpGet]
[AllowAnonymous]
public IActionResult LoginAzureAd(string returnUrl)
{
var redirectUrl = Url.Action(nameof(ChangeRequestController.Index), "ChangeRequest");
return Challenge(new AuthenticationProperties { RedirectUri = redirectUrl },
OpenIdConnectDefaults.AuthenticationScheme);
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task LogoutAzureAd()
{
if (User.Identity.IsAuthenticated)
{
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
await HttpContext.SignOutAsync(OpenIdConnectDefaults.AuthenticationScheme);
}
}
Startup.cs:
services.AddAuthentication(x =>
{
x.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
x.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
x.DefaultSignOutScheme = CookieAuthenticationDefaults.AuthenticationScheme;
x.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddOpenIdConnect(options =>
{
var azureAdConfig = Configuration.GetSection("AzureAd");
options.Authority = azureAdConfig.GetValue<string>("Instance") +
azureAdConfig.GetValue<string>("Domain");
options.ClientId = azureAdConfig.GetValue<string>("ClientId");
options.ResponseType = OpenIdConnectResponseType.IdToken;
options.CallbackPath = azureAdConfig.GetValue<string>("Callback");
options.SignedOutRedirectUri = "https://appname.azurewebsites.net/";
options.TokenValidationParameters.NameClaimType = "name";
options.UseTokenLifetime = true;
})
.AddCookie()
.AddSalesforceAuthentications(AuthConfigs);
services.ConfigureApplicationCookie(x =>
{
x.Cookie.Name = ".SalesForce.Cookie"; //This isn't my code. Could this cause problems?
});
Note: Generally salesforce auth is used in this project, Azure AD is supposed to function only for a certain module, for users who don't have SF account. It's not my idea, but I have to do that this way
appsettings.json:
"AzureAd": {
"ClientId": "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
"TenantId": "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
"Instance": "https://login.microsoftonline.com/",
"Domain": "XXXXX.onmicrosoft.com",
"Callback": "/.auth/login/aad/callback"
}
What I want to achieve and what I have achieved when running on localhost is that when user enters the web app, he clicks a button, is authenticated with Azure AD using company account and the redirected to ~/ChangeRequest/Index. But I cannot make it work after deploy. I found similar issue here:
https://forums.asp.net/t/2155544.aspx?AAD+REPLY+URL+issue+signin+oidc+vs+auth+login+aad+callback+Azure+Government+
but the way this guy solved it is not an option for me. Any ideas?
I managed to fix the issue. Apparently one cannot configure this auth in App Service and in one's code at the same time. All I had to do was to turn off authentication on Azure Portal, in the Authentication/Authorization section of App Service.
It's my first question on stackoverflow and I don't really know if should delete this question. If so, then please tell me.

Asp.net Core 2 enable multi tenancy using Identity Server 4

I have an IDP (Identity Server 4) hosted with multiple bindings: auth.company1.com and auth.company2.com
I also have an API protected from that IDP. So in order to access the API I need to get the access token from the IDP. This is configured at startup class at the API level like this:
services.AddAuthentication("Bearer")
.AddIdentityServerAuthentication(options =>
{
options.Authority = "https://auth.company1.com/";
options.RequireHttpsMetadata = true;
options.ApiName = "atb_api";
});
How can I configure options.Authority dynamically so it allows authority from multiple domains https://auth.company1.com/ and https://auth.company2.com/ ?
I solved this.
At the protecting API level at the startup class I have this configuration:
services.AddAuthentication("Bearer")
.AddIdentityServerAuthentication(options =>
{
options.Authority = "https://shared-domain-for-every-tenant/";
options.RequireHttpsMetadata = true;
options.ApiName = "atb_api";
});
The magic happens at the IDP level (IdentityServer4), while configuring the IdentityServer I add the option IssuerUri like this:
services.AddIdentityServer(options => {
options.IssuerUri = "https://shared-domain-for-every-tenant/";
})..AddDeveloperSigningCredential() ...other configurations ...
When I navigate to https://auth.company1.com/.well-known/openid-configuration
the returned document is like this:
{
"issuer": "https://shared-domain-for-every-tenant/",
"jwks_uri": "https://auth.company1.com/.well-known/openid-configuration/jwks",
"authorization_endpoint": "https://auth.company1.com/connect/authorize",
"token_endpoint": "https://auth.company1.com/connect/token",
"userinfo_endpoint": "https://auth.company1.com/connect/userinfo",
...
}
Notice the issure is a static url while all the other endpoints are specific to the tenant that made the request. This allows the API to validate the access token and also have different endpoints for each tenant (I need this to show a different login screen for each of them).
Hope it helps someone out there :)

Categories

Resources