Im trying to learn more about adding external authentication to a .net application im writing(Using razor pages). Im following a tutorial on youtube, but i get a different result from what he is getting.
When im trying to authenticate with google, the Challenge method redirects the browser so i can authenticate with google(or single signin if im already logged on), then it for some reason redirects me right back to the page where im calling the challenge method and it goes in a loop.
If i close the application and run it again, i can see that i got a cookie created and that i got the claims from google. What i dont understand is why it redirects in a loop.
The flow is as follows: User goes to login page -> Login page has hyperlink to google authentication page(called Google.cshtml in my project) -> The OnGet method in Google.cshtml.cs return Challenge(schemename for google in program.cs).
I Created a page called Callback that the program should redirect to after google authentication is done, but when i add a break-point i see that it never gets redirected(only redirects back to google in a loop).
Any help on understanding the problem is greatly appriciated. This is my code:
Hyperlink in Login.cshtml that sends user to page for google authentication:
<a asp-page="Google" asp-route-returnurl="#Model.ReturnUrl" class="btn btn-link btn-floating mx-1">
<i class="fab fa-google"></i>
</a>
Program.cs
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddRazorPages();
builder.Services.AddAuthentication("MyCookie")
.AddCookie("MyCookie", options =>
{
options.LoginPath = "/Account/Login";
options.LogoutPath = "/Account/Logout";
options.AccessDeniedPath = "/Account/AccessDenied";
})
//.AddCookie("temp")
.AddGoogle("Google", options =>
{
options.ClientId = "Omitted in this post";
options.ClientSecret = "Omitted in this post";
//options.CallbackPath = "/signin-google";
//options.SignInScheme = "temp";
})
.AddMicrosoftAccount("Microsoft", options =>
{
options.ClientId = "omitted in this post";
options.ClientSecret = "Omitted in this post";
options.CallbackPath = "/signin-oidc";
//options.SignInScheme = "temp";
});
var app = builder.Build();
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapRazorPages().RequireAuthorization();
app.Run();
Google.cshtml.cs
[AllowAnonymous]
public class Google : PageModel
{
public IActionResult OnGet(string ReturnUrl)
{
if (ReturnUrl != null)
{
if (!Url.IsLocalUrl(ReturnUrl))
{
throw new Exception("Invalid return url. fail!");
}
}
var props = new AuthenticationProperties
{
RedirectUri = Url.Page("Callback"),
Items =
{
{"uru", "/index"},
{"scheme", "Google"}
}
};
return Challenge(props,"Google");
}
}
Found the problem for anyone wondering. I had to add a / to the parameter in Url.Page() making it Url.Page("/Callback"). Weird that the guy from the tutorial didnt get the same error from this considering he only wrote Callback without forward slash
Related
I'm making a CRM project. I need to restrict access to some pages to clients and workers. I'm using CookieAuthentication and Authorize attribute and for some reason it's not working.
After registration of claims and cookies for user I'm trying to access this page "Master/Index" or "MasterController/Index" not sure which one is right to redirect but anyway instead of page I see this:
If Master as ControllerRoute
If MasterController as ControllerRoute
I'm 100% sure that user is not only Authorized but even has it's role because debagger shows it in any case:
Step After If Statement
Step After If Statement
And my MasterController is:
public class MasterController : Controller
{
[Authorize]
public IActionResult Index()
{
return View();
}
}
That's how I register user after his form sending on HttpPost page:
private async Task RegisterNewUser(LoginModel login, string r)
{
var claims = new List<Claim>()
{
new Claim(ClaimTypes.Name, login.Login),
new Claim(ClaimTypes.Role, r)
};
ClaimsIdentity claimsIdentity = new(claims, "Cookies");
await ControllerContext.HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(claimsIdentity));
}
And just to show you that I added auth in my Program.cs:
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(options =>
{
options.LoginPath = "/Verification/Auth";
options.LogoutPath = "/Verification/Logout";
options.AccessDeniedPath = "/";
});
...
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.UseAuthentication();
Btw, if I comment [Authorize] than everything works fine but that's not what I need.
The ordering of your middleware is incorrect. You need to place UseAuthentication() before UseAuthorization().
With it the way you have it, every time it hits the authorization middleware, it realizes the user is not authenticated, so redirects.
It never gets past that, as it will only get to the authentication middleware once it successfully passes through the authorization middleware. Hence you have an infinite loop resulting in your browser deciding it has had too many redirects.
See here for details.
The goal is to implement OAuth2 against Google in a C# MVC app using .NET6. The result I get shows failed in the result.Succeeded field and all values are null.
I'm sending the correct google client id and google client secret.
The app builds and sends off the request without an error. I believe that is all correct.
Here's my Program.cs
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.Google;
var config = new ConfigurationBuilder()
.AddJsonFile("appsettings.json", optional: false)
.Build();
var googleClientId = config.GetValue<string>("GoogleClientId");
var googleClientSecret = config.GetValue<string>("GoogleClientSecret");
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllersWithViews();
builder.Services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
}).AddGoogle(options =>
{
options.ClientId = googleClientId;
options.ClientSecret = googleClientSecret;
options.CallbackPath = "/account/google-response";
}).AddCookie(options =>
{
options.LoginPath = "/account/google-login"; // Must be lowercase
});
var app = builder.Build();
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Home/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.UseAuthorization();
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
app.Run();
Here's the account controller that sends and receives the result:
[AllowAnonymous, Route("account")]
public class AccountController : Controller
{
[Route("google-login")]
public IActionResult GoogleLogin()
{
var properties = new AuthenticationProperties { RedirectUri = Url.Action("GoogleResponse") };
return new ChallengeResult(GoogleDefaults.AuthenticationScheme, properties);
}
[Route("google-response")]
public async Task<IActionResult> GoogleResponse()
{
var result = await HttpContext.AuthenticateAsync(CookieAuthenticationDefaults.AuthenticationScheme);
// This is coming back with result.Succeeded == false, and result.Principal == null
var succeeded = result.Succeeded;
if (result.Principal == null) throw new Exception("Principal is null");
if (result.Principal.Identities == null) throw new Exception("Identities is null");
var claims = result.Principal.Identities
.FirstOrDefault().Claims.Select(claim => new
{
claim.Issuer,
claim.OriginalIssuer,
claim.Type,
claim.Value
});
return Json(claims);
}
}
Not sure what is wrong.
Thanks for the help.
Added: Not sure if this is helpful, but the payload that I'm getting back from Google:
state: CfDJ8Dcp9PtNslFFr9WdoNMnaxZuUE3bX_7go4zy8_XDg2ZIar8NvdxzZlhJ9mM9c8-E3cp9TchRcjvMwbX4XaMmTC79aKO7IuI39yHgZ6nrOEqORPDU9kfHGH-bgJB5S1bXIQcJ3y3wKMD39N6IDa-ygAxqoiFkd05Lf05d9RoA8bZ0h8DcbYbqpbK73rQraTQhBAdxVJlz2CLGXkzOhIoJmhdOn38poxjNILyGPOGRqA0t
code: 4/0AX4XfWjHBoCnvig2U2JG8tuPnqIvjQeC5VN_u_pSOcqIFFOjw7UadqI04qJ3iw4QyF2ngg
scope: email profile openid https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email
authuser: 0
prompt: consent
Found the issue. I was not configuring authorization and authentication to the application. (I was doubly adding authorization.) Specifically, this line when placed in Configure method in StartUp.cs fixed the problem:
app.UseAuthentication();
I have created the default ASP.NET Core Web App project using Visual Studio 2022 and .Net 6.
As the authentication type I have chosen Microsoft identify platform.
How do I get hold of the JWT that AzureAD generates for me as part of OpenID Connect?
I have changed the authentication service in the program.cs to use the option SaveTokens as follows:
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.Identity.Web;
using Microsoft.Identity.Web.UI;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(options =>
{
builder.Configuration.Bind("AzureAd", options);
options.SaveTokens = true;
});
builder.Services.AddAuthorization(options =>
{
// By default, all incoming requests will be authorized according to the default policy.
options.FallbackPolicy = options.DefaultPolicy;
});
builder.Services.AddRazorPages()
.AddMicrosoftIdentityUI();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapRazorPages();
app.MapControllers();
app.Run();
I want access to the JWT tokens so I can pass them to a bespoke service we have. I do not want regenerate them, I want the tokens that Microsoft have signed.
To test getting hold of them I have tried GetTokenAsync from the Microsoft.AspNetCore.Authentication extensions like so (in Index.cshtml)
#page
#using Microsoft.AspNetCore.Authentication
#model IndexModel
#{
ViewData["Title"] = "Home page";
}
<div class="text-center">
<h1 class="display-4">Welcome</h1>
<p>Learn about building Web apps with ASP.NET Core.</p>
<p>Access Token: #await HttpContext.GetTokenAsync("OpenIdConnect","access_token")</p>
<p>Refresh Token: #await HttpContext.GetTokenAsync("OpenIdConnect", "refresh_token")</p>
</div>
But alas - I get nulls back. Any ideas? Result below:
Well I'm none the wiser why the original SaveTokens method doesn't work but here is a way to make it work:
It's not quite to same but gets me what I need. Hook into the event OnTokenValidated and then save it explicitly on the identity as follows (in program.cs):
// Add services to the container.
builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(options =>
{
builder.Configuration.Bind("AzureAd", options);
options.Events.OnTokenValidated = context =>
{
var accessToken = context.SecurityToken as JwtSecurityToken;
if (accessToken != null)
{
var identity = context.Principal.Identity as ClaimsIdentity;
if (identity != null)
{
identity.AddClaim(new Claim("access_token", accessToken.RawData));
}
}
return Task.FromResult(0);
};
});
And then access it from the user.identity - following just for testing (in Index.cshtml):
<p>Access Token: #((User.Identity as ClaimsIdentity).Claims.Where( c => c.Type == "access_token").FirstOrDefault().Value)</p>
I installed AWS Toolkit for Visual Studio 2019 & created new "AWS Serverless Application(.NET Core C#)" project.
Following this video tutorial I configured new Cognito User Pool & edited the default project's Startup.cs & added secure page with [Authorize] attribute (at the 12min mark you can find the exact steps of how Cognito was configured).
After publishing to AWS (or even when debugging locally) when I navigate to secure page I correctly get redirected and prompted to login, but after successful login it enters a loop in the background that keeps sending back to OIDC authorization link & back to my site... after a few of those it throws an error of "ERR_TOO_MANY_REDIRECTS".
Any idea what is missing from this tutorial in order to make it work properly? The only step I skipped was that he registered a domain name for his site, while I'm just using the default site URL I get after right-clicking project & choosing "Publish to AWS Lambda".
Here's edited code of my Startup.cs:
public void ConfigureServices(IServiceCollection services)
{
string clientId = "<cognito_app_client_id>";
string clientSecret = "<cognito_app_client_secret>";
string logoutUrl = "https://<published_app_aws_url>/logout";
string baseUrl = "https://<published_app_aws_url>";
services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie()
.AddOpenIdConnect(options =>
{
options.ResponseType = OpenIdConnectResponseType.Code;
options.MetadataAddress = $"https://cognito-idp.<aws_region>.amazonaws.com/<cognito_pool_id>/.well-known/openid-configuration";
options.ClientId = clientId;
options.ClientSecret = clientSecret;
options.GetClaimsFromUserInfoEndpoint = true;
options.Scope.Add("email");
options.Events = new OpenIdConnectEvents
{
OnRedirectToIdentityProviderForSignOut = (context) =>
{
var logoutUri = logoutUrl;
logoutUri += $"client_id={clientId}&logout_uri={baseUrl}";
context.Response.Redirect(logoutUri);
context.HandleResponse();
return Task.CompletedTask;
}
};
});
services.AddRazorPages();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.UseAuthentication();
app.UseEndpoints(endpoints =>
{
endpoints.MapRazorPages();
});
}
Thanks to comment by Philip Pittle I found the UseAuthentication must come before UseAuthorization.
I am using ASP.NET Core 3.1, and Azure AD B2C. My goal is to use the Authorization Code Flow for my whole web app, but the documentation is not straight forward.
I followed the instructions given here: Web app that signs in users: Code configuration
However when I try to access my website, I get the following error:
[ERR] Message contains error: '"unauthorized_client"', error_description: '"AADB2C90057: The provided application is not configured to allow the 'OAuth' Implicit flow. uri: '"error_uri is null"'. (95c3107f)
In my Application Registration, I did NOT enable any of the two options for the Implicit Grant (Access tokens, and ID tokens).
Again, my goal is to use the Authorization Code Flow everywhere.
Any idea why I am getting the message saying that my app should be configured to allow the Implicit flow? How should I configure it to use the Authorization Code Flow?
after
services.AddAuthentication(AzureADB2CDefaults.AuthenticationScheme).AddAzureADB2C( ....
just add
services.Configure<OpenIdConnectOptions>(AzureADB2CDefaults.OpenIdScheme, options =>
{
options.ResponseType = OpenIdConnectResponseType.Code;
options.Scope.Add(options.ClientId);
});
If you want to securely sign users into web applications, we should use the OpenId Connect protocol. The response_type needs to contain id_token. So we need to enable Implicit Grant. For more details, please refer to the document and the document
Regarding how to implement OpenId connect in .net core web app, we can use the sdk Microsoft.AspNetCore.Authentication.AzureADB2C.UI. The detailed steps are as below
Register a web application in Azure AD B2C tenant
Implement Azure AD B2C auth in web application
a. add the following settings in the appsettings.json
{
"AzureAdB2C": {
"Instance": "https://<your-tenant-name>.b2clogin.com",
"ClientId": "<web-app-application-id>",
"Domain": "<your-b2c-domain>"
"CallbackPath": "/signin-oidc",
"SignUpSignInPolicyId": "B2C_1_test",
"ResetPasswordPolicyId": "B2C_1_test2",
"EditProfilePolicyId": "B2C_1_test1"
},
...
}
b. add the following code in the Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(AzureADB2CDefaults.AuthenticationScheme)
.AddAzureADB2C(options => Configuration.Bind("AzureAdB2C", options));
services.AddRazorPages();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapRazorPages();
endpoints.MapControllers();
});
}
c. Implement sign in and sign out. The sdk has helped us implement sign-in and sign-out method. So we can directly use it. For example
my login.cshtml
#using System.Security.Principal
#using Microsoft.AspNetCore.Authentication.AzureADB2C.UI
#using Microsoft.Extensions.Options
#inject IOptionsMonitor<AzureADB2COptions> AzureADB2COptions
#{
var options = AzureADB2COptions.Get(AzureADB2CDefaults.AuthenticationScheme);
}
<ul class="navbar-nav">
#if (User.Identity.IsAuthenticated)
{
<li class="nav-item">
<span class="nav-text text-dark">Hello #User.Identity.Name!</span>
</li>
<li class="nav-item">
<a class="nav-link text-dark" asp-area="AzureADB2C" asp-controller="Account" asp-action="SignOut">Sign out</a>
</li>
}
else
{
<li class="nav-item">
<a class="nav-link text-dark" asp-area="AzureADB2C" asp-controller="Account" asp-action="SignIn">Sign in</a>
</li>
}
</ul>
Test
Update
If you want to use Authorization Code Flow, please change startup.cs code as below.
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(AzureADB2CDefaults.AuthenticationScheme)
.AddAzureADB2C(options => Configuration.Bind("AzureAdB2C", options));
services.Configure<OpenIdConnectOptions>(AzureADB2CDefaults.OpenIdScheme, options =>
{
options.UsePkce = false;
options.ResponseType = "code";
options.Scope.Add("offline_access");
options.Events = new OpenIdConnectEvents()
{
OnAuthorizationCodeReceived = async context => {
var code = context.ProtocolMessage.Code;
var request = context.HttpContext.Request;
string currentUri = UriHelper.BuildAbsolute(
request.Scheme,
request.Host,
request.PathBase,
options.CallbackPath);
IConfidentialClientApplication cca = ConfidentialClientApplicationBuilder.Create(options.ClientId)
.WithB2CAuthority(options.Authority)
.WithRedirectUri(currentUri)
.WithClientSecret(options.ClientSecret)
.Build();
try
{
AuthenticationResult result = await cca.AcquireTokenByAuthorizationCode(options.Scope, code)
.ExecuteAsync();
context.HandleCodeRedemption(result.AccessToken, result.IdToken);
}
catch (Exception ex)
{
//TODO: Handle
throw;
}
}
};
});
services.AddRazorPages();
}