Using Blazor OIDC authentication with Google OAuth only for Google Drive - google-oauth

In my Blazor WASM app I am using OIDC authentication to log in to Google Drive as described here:
Secure an ASP.NET Core Blazor WebAssembly standalone app with the Authentication library
Google Auth error getting access token in Blazor
Cannot log in or get access token with Google Authorization on Blazor WASM Standalone app
But I don't want to use
<AuthorizeRouteView RouteData="#routeData" DefaultLayout="#typeof(MainLayout)">
<NotAuthorized>
#if (!context.User.Identity.IsAuthenticated)
{
<RedirectToLogin />
}
</NotAuthorized>
</AuthorizeRouteView>
because I don't want to restrict user access to my Blazor page.
Most importantly: I don't want the user to see the "Authorizing..." message for several seconds when they aren't logged in - logging in is optional.
I only need the Google OAuth login if the user decides to use Google Drive, so I can get the access token.
How can I use OIDC authentication only to get the access token for Google Drive?
If that isn't possible, can I use C# to login to Google as seen here in JavaScript?
OAuth 2.0 for Client-side Web Applications

The following suggestion may work. If not, report further issues, and I'll try to improve on it...
Make the following changes in your App.razor file:
Replace:
<AuthorizeRouteView RouteData="#routeData"
DefaultLayout="#typeof(MainLayout)">
<NotAuthorized>
#if (!context.User.Identity.IsAuthenticated)
{
<RedirectToLogin />
}
else
{
<p>You are not authorized to access this resource.</p>
}
</NotAuthorized>
</AuthorizeRouteView>
with
<RouteView RouteData="#routeData" DefaultLayout="#typeof(MainLayout)" />
You also have to remove all added Authorize attributes.
You'll no longer make use of the RedirectToLogin component...
Leave the LoginDisplay component in its place to enable authentication when requested.

Related

AccessTokenNotAvailableException

Getting a AccessTokenNotAvailableException error in my Blazor Web Assembly application.
This is happening when calling a webapi controller, it should be the first call to read application data.
I am not being prompted prior to this to login, and believe this is the root problem.
I am not sure why my app is not calling the login.
I have this in app.razor:
<AuthorizeRouteView RouteData="#routeData" DefaultLayout="#typeof(MainLayout)">
<NotAuthorized>
#if (context.User.Identity?.IsAuthenticated != true)
{
<RedirectToLogin />
}
else
{
<p role="alert">You are not authorized to access this resource.</p>
}
</NotAuthorized>
</AuthorizeRouteView>
and my RedirectToLogin contains this:
protected override void OnInitialized()
{
Navigation.NavigateToLogin("authentication/login");
}
My objective is to have the user immediately directed to the login page when first reaching the application.
Not sure what else to provide.
All help is appreciated.
I ended up having a problem with:
"The certificate chain was issued by an authority that is not trusted"
Set: "TrustServerCertificate=True" in the connection string, that seems to be taking care of it. I believe this is ok for now, in a development environment.

Authenticating a Blazor WebAssembly app with Azure Active Directory

I've already got an existing Blazor WebAssembly app and I'm attempting to add authentication with Azure Active Directory.
I've added the Microsoft.Authentication.WebAssembly.Msal nuget.
In my server's Program.cs, I've added the following code:
builder.Services.AddMsalAuthentication(options =>
{
builder.Configuration.Bind("AzureAd", options.ProviderOptions.Authentication);
options.ProviderOptions.LoginMode = "redirect";
});
And I've added the following to my appsettings.json
"AzureAd": {
"Instance": "xxxxxxxxxxx",
"Domain": "xxxxxxxxxxx",
"TenantId": "xxxxxxxxxxx",
"ClientId": "xxxxxxxxxxx",
"CallbackPath": "xxxxxxxxxxx"
},
I'm struggling to understand what else I need to add so that when I run the app, I get the Microsoft sign in screen.
You need to require authorization for parts of the application, or for the entire app. By default, all routes inside the application are open to the public. If you want to shield any of these routes, you need to explicitly ask for authorization.
How to do this is documented here: Secure ASP.NET Core Blazor WebAssembly.
In Blazor WebAssembly apps, authorization checks can be bypassed because all client-side code can be modified by users. The same is true for all client-side app technologies, including JavaScript SPA frameworks or native apps for any operating system.
Always perform authorization checks on the server within any API endpoints accessed by your client-side app.
If you host the app on a hosting option like a Static Web App, you can have the Azure platform enforce authorization without having to implement anything manually.
Authenticate and authorize Static Web Apps.
You can create a new blazor web assemebly application and choose Microsoft identity platform and then modify the appsettings.json file.
You need to add 3 parts, authentication injection in Program.cs, configurations in appsettings.json, and the module for sign view, 2 of them you already had, and the left is the sign in/out button in the top navigation bar, so that you can click it to redirect to Microsoft sign in page. You can create the template project and copy the razor components into your project.
LoginDisplay.razor:
#using Microsoft.AspNetCore.Components.Authorization
#using Microsoft.AspNetCore.Components.WebAssembly.Authentication
#inject NavigationManager Navigation
#inject SignOutSessionStateManager SignOutManager
<AuthorizeView>
<Authorized>
Hello, #context.User.Identity?.Name!
<button class="nav-link btn btn-link" #onclick="BeginLogout">Log out</button>
</Authorized>
<NotAuthorized>
Log in
</NotAuthorized>
</AuthorizeView>
#code{
private async Task BeginLogout(MouseEventArgs args)
{
await SignOutManager.SetSignOutState();
Navigation.NavigateTo("authentication/logout");
}
}

Asp.Netcore Web app with Azure AD b2c redirect url

Azure AD B2C is setup for authentication in Asp.netCore web app. The authentication process works perfectly. After authentication, the user needs to be redirected back the page where the login was initiated at.
the way the current flow happens:
This is the button <a class="btn btn-primary" asp-area="AzureADB2C" asp-controller="Account" asp-action="SignIn">Sign in</a>
PublicPage (has an sign-in/register button) on part of page that user need be authenticated to interact with -> signin button clicked -> redirected to Azure AD B2C -> user returned to IndexPage.
the way the needs to be:
PublicPage (has an sign-in/register button) on part of page that user need be authenticated to interact with -> signin button clicked -> redirected to Azure AD B2C -> user returned to PublicPage.
EDIT
#Jit_MSFT thanks for the suggestion, but I'm not exactly sure where to add those configurations.
services.AddAuthentication(AzureADB2CDefaults.AuthenticationScheme)
.AddAzureADB2C(options => {
Configuration.Bind("AzureAdB2C", options);
});
The settings above don't have those options. Also there are several page need to have that dynamic ability.
context.Properties.RedirectUri = "/xxxx;
this seems like i wold be locked into one page on the returnUrl
In your AccountController, please define the SignIn method as something like:
public async Task SignIn()
{
var redirectUri = ... // your redirect URI
await HttpContext.ChallengeAsync(AzureADB2CDefaults.AuthenticationScheme,
new AuthenticationProperties { RedirectUri = redirectUri });
}
You may also check other details and options in this answer
In addition, please check if, in Azure AD, you have to register your client app with a matching redirect URI (more details here) :)

OIDC authentication in server-side Blazor

I used this method but somehow it's not right because #attribute [AllowAnonymous] doesn't really worked so I use [Authorized] attribute instead of [AllowAnonymous] and then remove RequireAuthenticatedUser but OIDC does not redirect client to server login page.
I checked SteveSanderson github article about authentication and authorization in blazor but he didn't talk about OIDC.
So how can I handle this?
Startup class:
services.AddAuthentication(config =>
{
config.DefaultScheme = "Cookie";
config.DefaultChallengeScheme = "oidc";
})
.AddCookie("Cookie")
.AddOpenIdConnect("oidc", config =>
{
config.Authority = "https://localhost:44313/";
config.ClientId = "client";
config.ClientSecret = "secret";
config.SaveTokens = true;
config.ResponseType = "code";
config.SignedOutCallbackPath = "/";
config.Scope.Add("openid");
config.Scope.Add("api1");
config.Scope.Add("offline_access");
});
services.AddMvcCore(options =>
{
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser() // site-wide auth
.Build();
options.Filters.Add(new AuthorizeFilter(policy));
});
The following is a complete and working solution to the question:
First off, you'll need to provide an authentication challenge request mechanism that enables redirection to an authenticating agent such as IdentityServer. This is only possible with HttpContext, which is not available in SignalR (Blazor Server App). To solve this issue we'll add a couple of Razor pages where the HttpContext is available. More in the answer...
Create a Blazor Server App.
Install-Package Microsoft.AspNetCore.Authentication.OpenIdConnect -Version 3.1.0 or later.
Create a component named LoginDisplay (LoginDisplay.razor), and place it in the
Shared folder. This component is used in the MainLayout component:
<AuthorizeView>
<Authorized>
Hello, #context.User.Identity.Name !
<form method="get" action="logout">
<button type="submit" class="nav-link btn btn-link">Log
out</button>
</form>
</Authorized>
<NotAuthorized>
Log in
</NotAuthorized>
</AuthorizeView>
Add the LoginDisplay component to the MainLayout component, just above the About
anchor element, like this
<div class="top-row px-4">
<LoginDisplay />
About
</div>
Note: In order to redirect requests for login and for logout to IdentityServer, we have to create two Razor pages as follows:
Create a Login Razor page Login.cshtml (Login.cshtml.cs) and place them in the Pages folder as follow:
Login.cshtml.cs
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.IdentityModel.Tokens;
public class LoginModel : PageModel
{
public async Task OnGet(string redirectUri)
{
await HttpContext.ChallengeAsync("oidc", new
AuthenticationProperties { RedirectUri = redirectUri } );
}
}
This code starts the challenge for the Open Id Connect authentication scheme you defined in the Startup class.
Create a Logout Razor page Logout.cshtml (Logout.cshtml.cs) and place them in the Pages folder as well:
Logout.cshtml.cs
using Microsoft.AspNetCore.Authentication;
public class LogoutModel : PageModel
{
public async Task<IActionResult> OnGetAsync()
{
await HttpContext.SignOutAsync();
return Redirect("/");
}
}
This code signs you out, redirecting you to the Home page of your Blazor app.
Replace the code in App.razor with the following code:
#inject NavigationManager NavigationManager
<CascadingAuthenticationState>
<Router AppAssembly="#typeof(Program).Assembly">
<Found Context="routeData">
<AuthorizeRouteView RouteData="#routeData" DefaultLayout="#typeof(MainLayout)">
<NotAuthorized>
#{
var returnUrl = NavigationManager.ToBaseRelativePath(NavigationManager.Uri);
NavigationManager.NavigateTo($"login?redirectUri={returnUrl}", forceLoad: true);
}
</NotAuthorized>
<Authorizing>
Wait...
</Authorizing>
</AuthorizeRouteView>
</Found>
<NotFound>
<LayoutView Layout="#typeof(MainLayout)">
<p>Sorry, there's nothing at this address.</p>
</LayoutView>
</NotFound>
</Router>
</CascadingAuthenticationState>
Replace the code in the Startup class with the following:
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.IdentityModel.Tokens;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
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.
// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
public void ConfigureServices(IServiceCollection services)
{
services.AddRazorPages();
services.AddServerSideBlazor();
services.AddAuthorizationCore();
services.AddSingleton<WeatherForecastService>();
services.AddAuthentication(sharedOptions =>
{
sharedOptions.DefaultAuthenticateScheme =
CookieAuthenticationDefaults.AuthenticationScheme;
sharedOptions.DefaultSignInScheme =
CookieAuthenticationDefaults.AuthenticationScheme;
sharedOptions.DefaultChallengeScheme =
OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie()
.AddOpenIdConnect("oidc", options =>
{
options.Authority = "https://demo.identityserver.io/";
options.ClientId = "interactive.confidential.short";
options.ClientSecret = "secret";
options.ResponseType = "code";
options.SaveTokens = true;
options.GetClaimsFromUserInfoEndpoint = true;
options.UseTokenLifetime = false;
options.Scope.Add("openid");
options.Scope.Add("profile");
options.TokenValidationParameters = new
TokenValidationParameters
{
NameClaimType = "name"
};
options.Events = new OpenIdConnectEvents
{
OnAccessDenied = context =>
{
context.HandleResponse();
context.Response.Redirect("/");
return Task.CompletedTask;
}
};
});
}
// 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.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapBlazorHub();
endpoints.MapFallbackToPage("/_Host");
});
}
}
IMPORTANT: in all the code sample above you'll have to add using statements as necessary. Most of them are provided by default. The using provided here are those necessary to enable the authentication and authorization flow.
Run your app, click on the log in button to authenticate. You are being redirected to IdentityServer test server which allows you to perform an OIDC login. You may enter a user name: bob and password bob, and after click the OK button, you'll be redirected to your home page. Note also that you can use the external login provider Google (try it). Note that after you've logged in with identity server, the LoginDisplay component displays the string "Hello, <your user name>".
Note: While you're experimenting with your app, you should clear the browsing data, if you want to be redirected to the identity server's login page, otherwise, your browser may use the cached data. Remember, this is a cookie-based authorization mechanism...
Note that creating a login mechanism as is done here does not make your app more secured than before. Any user can access your web resources without needing to log in at all. In order to secure parts of your web site, you have to implement authorization as well, conventionally, an authenticated user is authorized to access secured resource, unless other measures are implemented, such as roles, policies, etc. The following is a demonstration how you can secure your Fetchdata page from unauthorized users (again, authenticated user is considered authorized to access the Fetchdata page).
At the top of the Fetchdata component page add the #attribute directive for the Authorize attribute, like this: #attribute [Authorize]
When an unauthenticated user tries to access the Fetchdata page, the AuthorizeRouteView.NotAuthorized delegate property is executed, so we can add some code to redirect the user to the same identity server's login page to authenticate.
The code within the NotAuthorized element looks like this:
<NotAuthorized>
#{
var returnUrl =
NavigationManager.ToBaseRelativePath(NavigationManager.Uri);
NavigationManager.NavigateTo($"login?redirectUri=
{returnUrl}", forceLoad: true);
}
</NotAuthorized>
This retrieves the url of the last page you were trying to access, the FetchData page, and then navigates to the Login Razor page from which a password challenge is performed, that is, the user is redirected to the identity server's login page to authenticate.
After the user has authenticated they are redirected to the FetchData page.
For server-side Blazor, authentication happens on the Razor page on which the Blazor application is hosted. For the default template, this is the _Host.cshtml Razor page which is configured to be the fallback page for server-side routing. Since the page is like a normal Razor page, you can use the [Authorize] or [AllowAnonymous] attributes there.
Any authorization you apply to the _Host.cshtml impacts how the general access to the Blazor app itself is authorized. If you want only authenticated users to access the app, you should require authorization; if you want any non-authenticated users to access the app, you cannot protect the app access itself.
The authorization of the page does not mean that you cannot have a more fine-grained authorization within your app. You can still use different rules and policies for particular components within your application. For that, you can use the <AuthorizeView> component.
There are two common scenarios that are likely for server-side Blazor:
Access to the whole Blazor application is limited to authenticated users. Users that are not authenticated should immediately authenticate (e.g. using OIDC) so that no anonymous user hits the app.
In that case, it should be enough to protect the _Host.cshtml by requiring authenticated users, either through the [Authorize] attribute, or using a convention in the AddRazorPages() call.
When accessing the Blazor application without being authenticated, the default authorization middleware will cause an authentication challenge and redirect to the OIDC sign-in.
Non-authenticated users should be able to access the Blazor application but the Blazor application will use a more detailed authorization using the <AuthorizeView> or IAuthorizationService.
In this situation, the _Host.cshtml must not be protected since anonymous users need to access it. This also means that the default authorization middleware, which runs as part of the Razor page, will not do anything. So you will have to handle the challenge yourself.
The “simple” way to do this would be to provide a login link to a different server-side route which will then trigger the authentication challenge and redirect to the OIDC sign-in. For example, you could have a MVC action like this:
[HttpGet("/login")]
public IActionResult Login()
=> Challenge();
Within your Blazor app, you could now add a link to this route and allow users to sign in that way:
<AuthorizeView>
<Authorized>
Signed in as #context.User.Identity.Name.
</Authorized>
<NotAuthorized>
Sign in here
</NotAuthorized>
</AuthorizeView>

Supporting Individual User Accounts AND Organizational Accounts in MVC5 / ASP.Net Identity 2

I've created an ASP.Net MVC5 application, in which I have configured (and have working fine) Individual User Accounts via Google, Facebook, etc.
What I'd like to do is also support authentication against Azure Active Directory (Organizational Accounts). This would be for internal staff to be able to logon to the app as administrators.
All existing information/guides/documentation I've found typically deals with using one or the other. How would I enable them both together?
If there needs to be a separate logon form for each type of user, that would not be an issue.
EDIT:
I was looking at the Application configuration within Azure Active Directory portal, and notice that they define an "OAUTH 2.0 AUTHORIZATION ENDPOINT". Can MVC5 be configured within Startup.Auth.cs to use this?
I managed to implement this by doing the following:
First, adding a reference to the Microsoft.Owin.Security.OpenIdConnect Nuget package.
Second, configuring it in my Startup.Auth.cs:
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
ClientId = "From the Azure Portal (see below)",
Authority = "https://login.windows.net/<domain>.onmicrosoft.com",
Notifications = new OpenIdConnectAuthenticationNotifications
{
RedirectToIdentityProvider = (ctx) =>
{
if (ctx.Request.Path.Value.EndsWith("ExternalLogin"))
{
string appBasePathUrl = ctx.Request.Scheme + "://" + ctx.Request.Host + ctx.Request.PathBase;
ctx.ProtocolMessage.RedirectUri = appBasePathUrl + "/";
ctx.ProtocolMessage.PostLogoutRedirectUri = appBasePathUrl;
}
else
{
ctx.State = NotificationResultState.Skipped;
ctx.HandleResponse();
}
return Task.FromResult(0);
}
},
Description = new AuthenticationDescription
{
AuthenticationType = "OpenIdConnect",
Caption = "SomeNameHere"
}
});
Third, I setup the application in the Azure Portal (classic):
Fourth, I added a separate logon page for admin users:
#using (Html.BeginForm("ExternalLogin", "Home"))
{
#Html.AntiForgeryToken()
<div class="ui basic segment">
<div class="ui list">
<div class="item">
<button type="submit" name="provider" value="OpenIdConnect" class="left floated huge ui button social">
<i class="windows icon"></i>
<span>My Org Name</span>
</button>
</div>
</div>
</div>
}
Fifth, the ExternalLogin action doesn't need to change - we just let OWIN middleware redirect us to the external login page. The flow would then direct the user back to the ExternalLoginCallback action.
Finally, in the ExternalLoginCallback action, I check the incoming claims to determine that the login was via Azure AD, and instead of calling into ASP.NET Identity, I construct my own ClaimsIdentity, which has all my (application specific) claim information which my application recognises as an admin user.
Now, admin users navigate to https://example.com/admin, click the login button, are redirected to the Azure AD login, and windup back at the application as an admin user.
Your best bet would be to leverage Azue AD Access Control Services (ACS) and setup the Identity Providers to include Azure AD, Facebook, et al. See the documentation here: http://azure.microsoft.com/en-us/documentation/articles/active-directory-dotnet-how-to-use-access-control/
ACS can indeed be used, however as you have already implemented Google/Facebook signin I recommend that you directly integrate with Azure AD instead of going through an intermediate STS like ACS/thinktecture.
If your app' signin experience involves the user clicking on "Signin with Google/Signin with Facebook" stickers - you can add "Signin with your company' account." (there's even a recommended branding style: http://msdn.microsoft.com/en-us/library/azure/dn132598.aspx
If you app performs realm discovery and forwards the user to the appropriate IdP (employing a text box saying something like "Enter your email address to sign in") - then you could add matching for your company name email addresses and forward those users to AAD.
In both cases, your application will issue an SSO request to SSO endpoint of AAD: https://login.windows.net/{company domain name/id}/{wsfed/saml/oauth2}. If you're using .Net, WSFed, this should see you through: http://msdn.microsoft.com/en-us/library/azure/dn151789.aspx. Look for the code:
SignInRequestMessage sirm = FederatedAuthentication.WSFederationAuthenticationModule.CreateSignInRequest("", HttpContext.Request.RawUrl, false);
result = Redirect(sirm.RequestUrl.ToString());
There's also an OpenIdConnect sample here: http://msdn.microsoft.com/en-us/library/azure/dn151789.aspx