I'm working on an SPA web app, and im using IdentityServer4 code flow to handle the authorization. So I have the following components:
https://localhost:5001
https://localhost:5001
http://localhost:8100
Rightnow im trying to authenticate the mobile app users, however the user interaction login screen keeps redirecting, and im getting login_required
.
Tracing back the calls, this is what im getting:
/connect/authorize
endpoint in a webview - Checkhttps://localhost:5001/auth/login?returnUrl=%2Fconnect%2Fauthorize%2Fcallback%3Fredirect_uri%3Dhttp%253A%252F%252Flocalhost%253A8100%252Fauth%252Fcallback%26client_id%3Dcharla-mobile%26response_type%3Dcode%26state%3Dpq1nokeuVj%26scope%3Dcharla-api%2520openid%2520profile%2520offline_access%26code_challenge%3DJxDVsm2YnMAbvOuemWWXjYLLt-Mi1TpHoO7zhDkCWSI%26code_challenge_method%3DS256
- Checklogin
endpoint in the AccountController.cs
- Checkhttps://localhost:5001/auth/login?returnUrl=%2Fconnect%2Fauthorize%2Fcallback%3Fredirect_uri%3Dhttp%253A%252F%252Flocalhost%253A8100%252Fauth%252Fcallback%26client_id%3Dcharla-mobile%26response_type%3Dcode%26state%3Dpq1nokeuVj%26scope%3Dcharla-api%2520openid%2520profile%2520offline_access%26code_challenge%3DJxDVsm2YnMAbvOuemWWXjYLLt-Mi1TpHoO7zhDkCWSI%26code_challenge_method%3DS256
- Checkhttps://localhost:5001/auth/login?returnUrl=%2Fconnect%2Fauthorize%2Fcallback%3Fredirect_uri%3Dhttp%253A%252F%252Flocalhost%253A8100%252Fauth%252Fcallback%26client_id%3Dcharla-mobile%26response_type%3Dcode%26state%3Dpbx8alT61z%26scope%3Dcharla-api%2520openid%2520profile%2520offline_access%26code_challenge%3DrGappKbnVpUNzlNHst4t5RlHephWFfJTVXuwtpQ8tZI%26code_challenge_method%3DS256
- Problem here IdentityServer can't sense that the user is now signed in. I keep getting the login_required
error in the debug terminal.So here is my setup:
Startup.cs
namespace Charla
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddIdentityCore<ApplicationUser>(options => { });
new IdentityBuilder(typeof(ApplicationUser), typeof(IdentityRole), services)
.AddRoleManager<RoleManager<IdentityRole>>()
.AddSignInManager<SignInManager<ApplicationUser>>()
.AddEntityFrameworkStores<ConverseContext>();
/*services.AddIdentity<ApplicationUser, IdentityRole>()
.AddRoleManager<RoleManager<IdentityRole>>()
.AddSignInManager<SignInManager<ApplicationUser>>()
.AddEntityFrameworkStores<ConverseContext>()
.AddDefaultTokenProviders();*/
var migrationsAssembly = typeof(Startup).GetTypeInfo().Assembly.GetName().Name;
var builder = services
.AddIdentityServer(SetupIdentityServer)
.AddDeveloperSigningCredential()
.AddConfigurationStore(options =>
{
options.ConfigureDbContext = b => b.UseMySql(Configuration.GetConnectionString("DefaultConnection"),
sqloptions => {
sqloptions.ServerVersion(new Version(10, 1, 37), ServerType.MariaDb); // replace with your Server Version and Type
sqloptions.MigrationsAssembly(migrationsAssembly);
});
})
.AddOperationalStore(options =>
{
options.ConfigureDbContext = b => b.UseMySql(Configuration.GetConnectionString("DefaultConnection"),
sqloptions => {
sqloptions.ServerVersion(new Version(10, 1, 37), ServerType.MariaDb); // replace with your Server Version and Type
sqloptions.MigrationsAssembly(migrationsAssembly);
});
});
/*services.AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme)
.AddIdentityServerAuthentication(options => {
options.Authority = Configuration.GetValue<string>("IdentityServer:Jwt:Authority");
options.RequireHttpsMetadata = false;
options.ApiName = "charla-api";
});*/
services.AddAuthentication(opt => {
opt.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
opt.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(jwt =>{
jwt.Authority = Configuration.GetValue<string>("IdentityServer:Jwt:Authority");
jwt.RequireHttpsMetadata = false;
jwt.TokenValidationParameters.ValidateAudience = false;
jwt.TokenValidationParameters.ValidTypes = new[] { "at+jwt" };
});
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IServiceProvider serviceProvider)
{
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseSpaStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseIdentityServer();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller}/{action=Index}/{id?}");
//endpoints.MapRazorPages();
endpoints.MapHub<ChatHub>("/hub");
});
}
private static void SetupIdentityServer(IdentityServerOptions options)
{
options.UserInteraction.LoginUrl = "/auth/login";
options.UserInteraction.LoginReturnUrlParameter = "returnUrl";
options.UserInteraction.LogoutUrl = "/logout";
options.UserInteraction.ErrorUrl= "/error/identity";
options.Events.RaiseErrorEvents = true;
options.Events.RaiseInformationEvents = true;
options.Events.RaiseFailureEvents = true;
options.Events.RaiseSuccessEvents = true;
// see https://identityserver4.readthedocs.io/en/latest/topics/resources.html
//options.EmitStaticAudienceClaim = true;
//identityServerOptions.Authentication.CookieLifetime = TimeSpan.FromDays(1);
}
}
}
}
AccountController.cs
[Route("api/[controller]")]
[ApiController]
[AllowAnonymous]
public class AccountController : ControllerBase
{
[HttpPost("login")]
public async Task<IActionResult> Login(UserResource model) {
var result = await signInManager.PasswordSignInAsync(model.email, model.password, isPersistent: true, lockoutOnFailure: false);
var context = await interaction.GetAuthorizationContextAsync(model.return_url);
if (result.Succeeded) {
var uo = db.Users.Include(q => q.UserOrganization).Single( q => q.Email == model.email ).UserOrganization.First();
uo.LastLogin = DateTime.UtcNow;
await db.SaveChangesAsync();
// let identity server know that we loggedin
await identityEvents.RaiseAsync(new UserLoginSuccessEvent(
model.email, uo.UserId, model.email, clientId: context?.Client.ClientId
));
//return Redirect(model.return_url);
return Ok( new{
email = model.email,
return_url = context.RedirectUri
} );
}
await identityEvents.RaiseAsync(new UserLoginFailureEvent(model.email, "invalid credentials", clientId:context?.Client.ClientId));
return NotFound(new {});
}
IdentityConfig.cs - I'm using EF tables, but it was seeded from the below:
using IdentityServer4;
using IdentityServer4.Models;
using System.Collections.Generic;
namespace Charla
{
public static class IdentityConfig
{
public static IEnumerable<IdentityResource> IdentityResources =>
new IdentityResource[]
{
new IdentityResources.OpenId(),
new IdentityResources.Profile(),
};
public static IEnumerable<ApiResource> GetApiResources()
{
return new List<ApiResource>
{
new ApiResource("charla-api", "Charla API Resource")
};
}
public static IEnumerable<ApiScope> ApiScopes =>
new ApiScope[]
{
new ApiScope("charla-api", "Charla API Scope")
};
public static IEnumerable<Client> Clients =>
new Client[]
{
// charla web app
new Client
{
ClientId = "charla-spa",
ClientName = "Charla Web App",
RequireClientSecret = false,
AllowOfflineAccess = true,
AllowAccessTokensViaBrowser = true,
AllowedGrantTypes = GrantTypes.Code,
RedirectUris = {
"https://localhost:5001/authentication/login-callback",
"https://app-dev.getcharla.com/authentication/login-callback",
"https://app.getcharla.com/authentication/login-callback"
},
//FrontChannelLogoutUri = "https://localhost:5001/authentication/logout-callback",
PostLogoutRedirectUris = {
"https://localhost:5001/authentication/logout-callback",
"https://app-dev.getcharla.com/authentication/logout-callback",
"https://app.getcharla.com/authentication/logout-callback"
},
AllowedScopes = {
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
IdentityServerConstants.StandardScopes.OfflineAccess,
"charla-api"
}
},
// mobile client
new Client
{
ClientId = "charla-mobile",
ClientName = "Charla Mobile Apps",
RequireClientSecret = false,
AllowedGrantTypes = GrantTypes.Code,
AllowAccessTokensViaBrowser = true,
AllowOfflineAccess = true,
RedirectUris = {
"https://getcharla.com/ios_redirect",
"http://localhost:8100/auth/callback"
},
PostLogoutRedirectUris = {
"https://getcharla.com/ios_redirect_endsession",
"http://localhost:8100/auth/endsession"
},
AllowedScopes = {
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
IdentityServerConstants.StandardScopes.OfflineAccess,
"charla-api"
},
AllowedCorsOrigins = { "http://localhost:8100", "https://localhost:5001", "http://localhost:5000" }
},
};
}
}
login.component.ts - The function in the web component that fires the call to the login
endpoint and that is doing the redirection:
ngOnInit(): void {
this.route.queryParams.subscribe(params => {
this.returnUrl = params['returnUrl'] || '/';
});
}
/**
* Form Submit
*/
submit() {
const controls = this.loginForm.controls;
const authData = {
email: controls['email'].value,
password: controls['password'].value
};
this.auth
.login(authData.email, authData.password, this.returnUrl)
.pipe(
finalize(() => {
this.loading = false;
this.cdr.markForCheck();
})
)
.subscribe(
result => {
window.location = this.returnUrl;
this.router.navigateByUrl(this.returnUrl); // Main page
},
err => {
console.log(err);
if (( 'status' in err) && ( err.status === 404)){
this.authNoticeService.setNotice(this.translate.instant('AUTH.VALIDATION.INVALID_LOGIN'), 'danger');
}
}
);
}
Have no clue why its not working. I read I should be using HttpContent.SignInAsync
in the login
web api endpoint, but im already using var result = await signInManager.PasswordSignInAsync(model.email, model.password, isPersistent: true, lockoutOnFailure: false);
so I assume thats enough.
Other choices I made I'm not sure are right, like using AddIdentityCore
instead of AddIdentity
. Should I be adding AddAspNetIdentity<ApplicationUser>()
?
By following the trace you provided that could be happening because of the new rules around Cookies on the Browsers.
On new ASP.NET Core applications the samesite=none
attribute is automatically added, but the Browsers require that you specify the Secure
attribute too otherwise the Set-Cookie will be blocked.
To configure your IdentityServer add the following block of code inside the Configure
method in the Startup
class:
app.UseCookiePolicy(new CookiePolicyOptions
{
HttpOnly = HttpOnlyPolicy.None,
MinimumSameSitePolicy = SameSiteMode.None,
Secure = CookieSecurePolicy.Always
});
When using the secure attribute HTTPS must be used otherwise it will fail
Another SO question regarding the topic: Session cookie set `SameSite=None; Secure;` does not work
And some information by Microsoft on ASP.NET Core: https://docs.microsoft.com/pt-br/aspnet/core/security/samesite?view=aspnetcore-5.0
I experienced a similar issue with my SPA, when logging out and logging back in again. It seems the issue was caused by a Visual Studio update. If you navigate to the project folder using the command prompt and use dotnet run
' or even better dotnet watch run
(so you can make changes to the .net core code while it's running) you can circumnavigate the issue; buy not running the code through IIS in visual studio.
If this works for you update the Properties Launch to be Project (which also uses dotnet run) and you can then debug as well.
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.