I followed a link to achieve google SSO github.com/aspnet/Security/issues/1370. But even after successful login it is taking me to redirect uri mentioned in authentication property. It is not taking to the callback url. Could someone help on this? Our application is a .net core 3.1 with IdentityServer4.
Am expecting signinoauth2 API to be hit after google login, but thats not happening.
I could see a network call from browser with below format and getting correlation error.
https://localhost:44368/signinoauth2?state=&code=&scope=***&prompt=none
Exception: Correlation failed.
Show raw exception details
Exception: An error was encountered while handling the remote login.
Microsoft.AspNetCore.Authentication.RemoteAuthenticationHandler.HandleRequestAsync()
Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)
Soulbook.Api.Startup+<>c+<b__5_1>d.MoveNext() in Startup.cs
await next.Invoke();
Swashbuckle.AspNetCore.SwaggerUI.SwaggerUIMiddleware.Invoke(HttpContext httpContext)
Swashbuckle.AspNetCore.Swagger.SwaggerMiddleware.Invoke(HttpContext httpContext, ISwaggerProvider swaggerProvider)
Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context)
PFB my code for reference,
[HttpGet]
[Authorize(AuthenticationSchemes = GoogleDefaults.AuthenticationScheme)]
[Route("/Feed")]
public ActionResult Feed()
{
return Ok();
}
[HttpGet]
[Route("/signin")]
public ActionResult SignIn()
{
var authProperties = new AuthenticationProperties
{
RedirectUri = "/"
};
return new ChallengeResult(GoogleDefaults.AuthenticationScheme, authProperties);
}
[HttpPost]
[Route("/signinoauth2")]
public ActionResult<LoginResponse> signinoauth2Async([FromForm]object data)
{
return Ok();
}
Startup.cs
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.AspNetCore.Authentication.Google;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc.Authorization;
services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = GoogleDefaults.AuthenticationScheme;
})
.AddCookie(o => {
o.LoginPath = "/signin";
o.LogoutPath = "/signout";
o.ExpireTimeSpan = TimeSpan.FromDays(7);
})
.AddGoogle(o => {
o.ClientId = "***";
o.ClientSecret = "**";
o.SaveTokens = true;
o.CallbackPath = "/signinoauth2";
});
services.AddMvc(config =>
{
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.AddAuthenticationSchemes(GoogleDefaults.AuthenticationScheme)
.Build();
config.Filters.Add(new AuthorizeFilter(policy));
}).AddNewtonsoftJson();
EDIT: Having signinoauth2 in any one of the below formats also doesnt help.
[HttpGet]
[Route("/signinoauth2")]
public ActionResult<LoginResponse> signinoauth2Async(string state, string code, string scope, string prompt)
{
return Ok();
}
[HttpPost]
[Route("/signinoauth2")]
public ActionResult<LoginResponse> signinoauth2Async(string state, string code, string scope, string prompt)
{
return Ok();
}
I assume that you want to get Google user information in your enpoint?
Then what you have to do is configure the external authentication properties. And thanks to this you are going to be able to get the user on your redirect endpoint.
[HttpGet("login/google/")]
[AllowAnonymous]
public async Task<IActionResult> LoginGoogle()
{
var properties = _signInManager.ConfigureExternalAuthenticationProperties(GoogleDefaults.AuthenticationScheme, "/api/identity/google-redirect");
return Challenge(properties, GoogleDefaults.AuthenticationScheme);
}
What you configured on startup is a callback route which gets handled by Middleware and never hits the endpoint in your controller. What you want to achive is get user on redirect route like this
[HttpGet("google-redirect")]
[AllowAnonymous]
public async Task<IActionResult> CallbackGoogle()
{
var info = await _signInManager.GetExternalLoginInfoAsync();
return Ok();
}
It sounds like you aren't actually being properly authenticated, if you were the app would redirect to the landing page whose controller I assume has an [Authorize] attribute. Could you have possibly forgotten to add yourself as a user in the db that your identity server is referencing?
I've implemented asp.net core Identity authentifaiction and it's working fine with my web application. In the startup.cs file, I have the following:
services.ConfigureApplicationCookie(options =>
{
// Cookie settings
options.Cookie.HttpOnly = true;
options.ExpireTimeSpan = TimeSpan.FromMinutes(30);
options.LoginPath = "/Identity/Account/Login";
options.AccessDeniedPath = "/Identity/Account/AccessDenied";
options.SlidingExpiration = true;
});
And in the Login.chtml.cs, I've the the login method:
public async Task<IActionResult> OnPostAsync(string returnUrl = null)
{
if (ModelState.IsValid)
{
var result = await _signInManager.PasswordSignInAsync(Input.Email, Input.Password, Input.RememberMe, lockoutOnFailure: true);
if (result.Succeeded)
{
//...
}
else
{
//....
}
}
return Page();
}
Now I'm putting in place a WPF client in which I want to authenticate my users using the AspNetCore.Identity login procedure. Any suggestion about how to proceed will be highly appreciated.
Finally, I decided to go with IdentityServer4 in order to have a centralized login and workflow for the WPF client and other clients that I may need later.
I have setup an dotnet angular project and then implemented authentication as follows in the StartUp.cs file.
public void ConfigureServices(IServiceCollection services)
{
services.AddScoped<IPasswordHasher<CustomUser>,
PasswordHasherWithOldMembershipSupport<CustomUser>>();
services.AddIdentity<CustomUser, IdentityRole>()
.AddEntityFrameworkStores<AuthenticationContext<CustomUser>>()
.AddDefaultUI()
.AddDefaultTokenProviders();
var connection = configuration.GetConnection("Authentication");
services.AddDbContext<AuthenticationContext<CustomUser>>(options =>
options.UseSqlServer(connection));
services.AddTransient<IEmailSender, AuthMessageSender>();
services.AddTransient<AuthMessageSender.ISmsSender, AuthMessageSender>();
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
...
app.UseAuthentication();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller}/{action=Index}/{id?}");
});
...
}
IdentityHostingStartUp.cs file which runs upon startup to configure authentication.
public class IdentityHostingStartup : IHostingStartup
{
public void Configure(IWebHostBuilder builder)
{
builder.ConfigureServices((context, services) =>
{
services.AddAuthentication();
services.Configure<IdentityOptions>(options =>
{
// Password settings.
options.Password.RequireDigit = true;
options.Password.RequireLowercase = true;
options.Password.RequireNonAlphanumeric = true;
options.Password.RequireUppercase = true;
options.Password.RequiredLength = 6;
options.Password.RequiredUniqueChars = 1;
// Lockout settings.
options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(5);
options.Lockout.MaxFailedAccessAttempts = 5;
options.Lockout.AllowedForNewUsers = true;
// User settings.
options.User.RequireUniqueEmail = false;
});
services.ConfigureApplicationCookie(options =>
{
// Cookie settings
options.Cookie.HttpOnly = true;
options.ExpireTimeSpan = TimeSpan.FromMinutes(15);
options.LoginPath = "/Identity/Account/Login";
options.AccessDeniedPath = "/Identity/Account/AccessDenied";
options.SlidingExpiration = true;
});
});
}
}
I have a custom redirect in my angular code to go to the authenticate page if the user is not logged in.
import { Inject, Injectable } from '#angular/core';
import { HttpClient } from '#angular/common/http';
#Injectable()
export class AuthService {
constructor(http: HttpClient, #Inject('BASE_URL') baseUrl: string) {
http.get<Boolean>(baseUrl + "api/Home/Status").subscribe((authenticated) => {
if (!authenticated) {
window.location.href = baseUrl + "/Identity/Account/Login";
}
});
}
And finally, my HomeController code to check the authenticated status of the logged in user.
[HttpGet("[action]"), AllowAnonymous]
public Boolean Status()
{
var user = _accessor.HttpContext.User;
return User.Identity.IsAuthenticated;
}
The Status(or any other called api controller actions) action always has a null user name, user claims, and IsAuthenticated always returns false even after logging in.
This is driving me up the wall. I have read as many post and tried as many options as I could find and nothing seems to work.
At some point, I noticed that the user name was filled as expected. I thought it was solved. However, since then it has stopped working even though I haven't changed anything and I can't solve this issue.
This happens when your Debug settings are set to EnableSSL unchecked. This would also become and issue if you were to deploy without SSL enabled. As soon as I enabled SSL as in the picture below the application identity cookie was included in the ajax requests.
I've succesfully used WinForms sample from IdentityModel.OidcClient v2 to invoke an API secured with IdentityServer4.
IS is configured with two external providers, Google and ADFS; implementation is based on IS4 quickstarts.
Authentication works fine, WinForms application receives a valid refresh token and is able to invoke a secured API, but I'm confused by the external login callback behavior.
After succesful login, the embedded browser closes and default browser is opened (Chrome in my laptop), and reaches the ExternalLoginCallback.
Then the WinForms gets the refresh token, but then chrome tab stays open and is redirected to the IS login page.
How can I prevent showing / close chrome browser window?
Do I have to tweak the ExternalLogin action?
Update
Adding client code and lib/server info:
WinForm client with
IdentityModel v 3.0.0
IdentityModel.OidcClient 2.4.0
asp.net mvc server with
IdentityServer4 version 2.1.1
IdentityServer4.EntityFramework 2.1.1
Following WinForm client code:
public partial class SampleForm : Form
{
private OidcClient _oidcClient;
private HttpClient _apiClient;
public SampleForm()
{
InitializeComponent();
var options = new OidcClientOptions
{
Authority = "http://localhost:5000",
ClientId = "native.hybrid",
ClientSecret = "secret",
Scope = "openid email offline_access myscope myapi1 myapi2",
RedirectUri = "http://localhost/winforms.client",
ResponseMode = OidcClientOptions.AuthorizeResponseMode.FormPost,
Flow = OidcClientOptions.AuthenticationFlow.Hybrid,
Browser = new WinFormsEmbeddedBrowser()
};
_oidcClient = new OidcClient(options);
}
private async void LoginButton_Click(object sender, EventArgs e)
{
AccessTokenDisplay.Clear();
OtherDataDisplay.Clear();
var result = await _oidcClient.LoginAsync(new LoginRequest());
if (result.IsError)
{
MessageBox.Show(this, result.Error, "Login", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
else
{
AccessTokenDisplay.Text = result.AccessToken;
var sb = new StringBuilder(128);
foreach (var claim in result.User.Claims)
{
sb.AppendLine($"{claim.Type}: {claim.Value}");
}
if (!string.IsNullOrWhiteSpace(result.RefreshToken))
{
sb.AppendLine($"refresh token: {result.RefreshToken}");
}
OtherDataDisplay.Text = sb.ToString();
_apiClient = new HttpClient(result.RefreshTokenHandler);
_apiClient.BaseAddress = new Uri("http://localhost:5003/");
}
}
private async void LogoutButton_Click(object sender, EventArgs e)
{
//await _oidcClient.LogoutAsync(trySilent: Silent.Checked);
//AccessTokenDisplay.Clear();
//OtherDataDisplay.Clear();
}
private async void CallApiButton_Click(object sender, EventArgs e)
{
if (_apiClient == null)
{
return;
}
var result = await _apiClient.GetAsync("identity");
if (result.IsSuccessStatusCode)
{
OtherDataDisplay.Text = JArray.Parse(await result.Content.ReadAsStringAsync()).ToString();
}
else
{
OtherDataDisplay.Text = result.ReasonPhrase;
}
}
}
Update 2
ExternalLoginCallback code:
public async Task<IActionResult> ExternalLoginCallback()
{
// read external identity from the temporary cookie
var result = await HttpContext.AuthenticateAsync(IdentityServer4.IdentityServerConstants.ExternalCookieAuthenticationScheme);
if (result?.Succeeded != true)
{
_logger.LogError(result.Failure, "External athentication error.");
throw new Exception("External authentication error");
}
// retrieve claims of the external user
var externalUser = result.Principal;
var claims = externalUser.Claims.ToList();
....LOOKING FOR THE USER (OMITTED FOR BREVITY)....
var additionalClaims = new List<Claim>();
// if the external system sent a session id claim, copy it over
// so we can use it for single sign-out
var sid = claims.FirstOrDefault(x => x.Type == JwtClaimTypes.SessionId);
if (sid != null)
{
additionalClaims.Add(new Claim(JwtClaimTypes.SessionId, sid.Value));
}
// if the external provider issued an id_token, we'll keep it for signout
AuthenticationProperties props = null;
var id_token = result.Properties.GetTokenValue("id_token");
if (id_token != null)
{
props = new AuthenticationProperties();
props.StoreTokens(new[] { new AuthenticationToken { Name = "id_token", Value = id_token } });
}
// issue authentication cookie for user
await _events.RaiseAsync(new UserLoginSuccessEvent(provider, userId, user.Id.ToString(), user.Username));
await HttpContext.SignInAsync(user.Id.ToString(), user.Username, provider, props, additionalClaims.ToArray());
_logger.LogInformation("User {user} logged in with external provider.", userId);
// delete temporary cookie used during external authentication
await HttpContext.SignOutAsync(IdentityServer4.IdentityServerConstants.ExternalCookieAuthenticationScheme);
// validate return URL and redirect back to authorization endpoint or a local page
var returnUrl = result.Properties.Items["returnUrl"];
if (_interaction.IsValidReturnUrl(returnUrl) || Url.IsLocalUrl(returnUrl))
{
return Redirect(returnUrl);
}
return Redirect("~/");
}
Client configuration on IdentityServer, serialized:
{
"Enabled": true,
"ClientId": "native.hybrid",
"ProtocolType": "oidc",
"RequireClientSecret": true,
"ClientName": "Application",
"LogoUri": null,
"RequireConsent": false,
"AllowRememberConsent": true,
"AllowedGrantTypes": [
"hybrid"
],
"RequirePkce": false,
"AllowPlainTextPkce": false,
"AllowAccessTokensViaBrowser": true,
"RedirectUris": [
"http://localhost/winforms.client"
],
"FrontChannelLogoutUri": null,
"FrontChannelLogoutSessionRequired": true,
"BackChannelLogoutUri": null,
"BackChannelLogoutSessionRequired": true,
"AllowOfflineAccess": true,
"AllowedScopes": [
"openid",
"email",
"profile",
"myscope",
"offline_access",
"myapi1",
"myapi2"
],
"AlwaysIncludeUserClaimsInIdToken": false,
"IdentityTokenLifetime": 300,
"AccessTokenLifetime": 3600,
"AuthorizationCodeLifetime": 300,
"AbsoluteRefreshTokenLifetime": 2592000,
"SlidingRefreshTokenLifetime": 1296000,
"ConsentLifetime": null,
"RefreshTokenUsage": 1,
"UpdateAccessTokenClaimsOnRefresh": false,
"RefreshTokenExpiration": 1,
"AccessTokenType": 0,
"EnableLocalLogin": true,
"IdentityProviderRestrictions": [
"Google",
"WsFederation"
],
"IncludeJwtId": false,
"Claims": [],
"AlwaysSendClientClaims": false,
"ClientClaimsPrefix": "client_",
"PairWiseSubjectSalt": null,
"Properties": {}
}
I could answer very long but in the end the quickstart code you used was the root cause of this issue.
To be exact it's this code that's causing the issue:
// validate return URL and redirect back to authorization endpoint or a local page
var returnUrl = result.Properties.Items["returnUrl"];
if (_interaction.IsValidReturnUrl(returnUrl) || Url.IsLocalUrl(returnUrl))
{
return Redirect(returnUrl);
}
return Redirect("~/");
it should become this instead:
// retrieve return URL
var returnUrl = result.Properties.Items["returnUrl"] ?? "~/";
// check if external login is in the context of an OIDC request
var context = await _interaction.GetAuthorizationContextAsync(returnUrl);
if (context != null)
{
if (await _clientStore.IsPkceClientAsync(context.ClientId))
{
// if the client is PKCE then we assume it's native, so this change in how to
// return the response is for better UX for the end user.
return View("Redirect", new RedirectViewModel { RedirectUrl = returnUrl });
}
}
return Redirect(returnUrl);
This would also mean that you need an extension class method:
public static class Extensions
{
/// <summary>
/// Determines whether the client is configured to use PKCE.
/// </summary>
/// <param name="store">The store.</param>
/// <param name="clientId">The client identifier.</param>
/// <returns></returns>
public static async Task<bool> IsPkceClientAsync(this IClientStore store, string clientId)
{
if (!string.IsNullOrWhiteSpace(clientId))
{
var client = await store.FindEnabledClientByIdAsync(clientId);
return client?.RequirePkce == true;
}
return false;
}
}
The missing viewmodel:
public class RedirectViewModel
{
public string RedirectUrl { get; set; }
}
This missing javascript file with this content located in wwwroot/js/signin-redirect.js
window.location.href = document.querySelector("meta[http-equiv=refresh]").getAttribute("data-url");
and as last a new razor page Redirect.cshtml located in the Views/Shared
#model RedirectViewModel
<h1>You are now being returned to the application.</h1>
<p>Once complete, you may close this tab</p>
<meta http-equiv="refresh" content="0;url=#Model.RedirectUrl" data-url="#Model.RedirectUrl">
<script src="~/js/signin-redirect.js"></script>
This should do the trick or you can update your quickstart code. But it's not an issue in your own code.
I think that the ResponseMode is the thing that bothers you. Why don't you remove it from the OIDC client settings. The flow can also go for now (just make sure it is properly configured on IDS side). Also - monitor the logs of Identity Server, for any errors.
I have had the same problem and what worked for me was to change the RedirectUri to a different value that does not start with http.
var options = new OidcClientOptions
{
Authority = "<path to ids>",
ClientId = "<your client id>",
Flow = OidcClientOptions.AuthenticationFlow.Hybrid,
ClientSecret = "<your super secret phrase>",
Scope = "<your scopes>",
RedirectUri = "winformsclients://callback", // <-- HERE IS THE ANSWER
Browser = new WinFormsWebView(),
PostLogoutRedirectUri = "winformsclients://callback", // <-- HERE IS THE ANSWER
};
var oidcClient = new OidcClient(options);
Important to the above solution would be that your Client configuration on your IdentityServer (in the database) need to be updated with this dummy uri.
The tables where this has to be configured are:
ClientCorsOrigins, ClientRedirectUris and ClientPostLogoutRedirectUris
you can also download a sample app from the link below and customize it.
https://github.com/IdentityModel/IdentityModel.OidcClient.Samples/tree/main/WinFormsWebView
I ran into a similar issue when trying to authenticate using Google. A possible workaround/fix is documented here:
https://github.com/IdentityModel/IdentityModel.OidcClient/issues/283
I have an SPA app built with AngularJS, the backend is WebApi2. I´m struggling with Authentication and Authorization. What I want in the long run is to enable authentication against Active Directory. But for now, I just trying to enable authorization for my APiControllers and setting a Cookie with Owin.
Here is my Owin Identity Helper class, I´m only adding 1 claim that is the serialized user info:
public void SignIn(bool rememberMe, T user)
{
var claims = new List<Claim>
{
new Claim(ClaimTypes.UserData, JsonConvert.SerializeObject(user)),
};
var claimsIdentity = new ClaimsIdentity(claims, DefaultAuthenticationTypes.ApplicationCookie);
AuthenticationManager.SignIn(new AuthenticationProperties() { IsPersistent = rememberMe }, claimsIdentity);
}
Here is authentication in controller:
[HttpGet, HttpPost]
[AllowAnonymous]
[Route("authenticate")]
public HttpResponseMessage Authenticate()
{
var authenticated = IdentityContext.Current.IsAuthenticated;
if (!authenticated)
{
var user = new User();
user.Email = "roger#moore.com";
user.Name = "Roger Moore";
user.Id = 23;
IdentityContext.Current.SignIn(true, user);
return new HttpResponseMessage()
{
Content = new StringContent(
JsonConvert.SerializeObject(user),
Encoding.UTF8,
"application/json"
)
};
}
else
{
//return the user if authenticated
return new HttpResponseMessage()
{
Content = new StringContent(
JsonConvert.SerializeObject(IdentityContext.Current.User), //from claim
Encoding.UTF8,
"application/json"
)
};
}
}
My StartUp class
public partial class Startup
{
// For more information on configuring authentication, please visit http://go.microsoft.com/fwlink/?LinkId=301864
public void ConfigureAuth(IAppBuilder app)
{
// Enable the application to use a cookie to store information for the signed in user
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString("/account/signedout")
});
}
}
When I call the authenticate user I´m setting signing in, but when calling a controller with [Authorize] attribute, im not signed in. Furthermore, when having fiddler running I get the error:
"[Fiddler] Response Header parsing failed. This can be caused by an illegal HTTP response earlier on this reused server socket-- for instance, a HTTP/304 response which illegally contains a body. Response Data:"
Does anyone have any suggestions, or alternatives with example code for using JWT Token Authentication and Authorization From Angular to WebApi2?