Azure B2C Logout in Blazor - azure-active-directory

I've build a Blazor server app and I'm using the Azure b2c which I build using the wizard.
I don't have a login page and I only use the Google as oauth provider. I just have the default blanket redirect which is fine for me.
builder.Services.AddAuthorization(options =>
{
// By default, all incoming requests will be authorized according to the default policy
options.FallbackPolicy = options.DefaultPolicy;
});
One issue is that I'm facing is that when I'm opening the app it doesn't prompt me asking which account I want to use. I know I'm already signed into my google account as whole but when opening my app I would like the app to prompt for "choosing the account". When I run the same userflow on the azure portal it does prompt me. The same just doesn't happen for my app. How can I make sure that the app always asks to select the account? I read some articles which said to add "prompt" keyword but I don't know where to add that as I'm not calling any custom url.
Another issue I'm facing is that the log-out doesn't work as expected. In my app logout sequence is same as the default which redirects user to MicrosoftIdentity/Account/SignIn link.
<AuthorizeView>
<Authorized>
Hello, #context.User.Identity?.Name!
Log out
</Authorized>
<NotAuthorized>
Log in
</NotAuthorized>
</AuthorizeView>
Once I click the logout button; I does something and then redirects me to this page.
However once I click the back button, the app opens as normal with the user still signed in. I expected the app to prompt for login at-least this time.
Can you please help me with the right approach for the implementing this. I prefer to avoid advance things like custom user flows. Perhaps some settings in appsettings.json can do the trick?
Thanks a lot.

For the logout to work properly ,In the Redirect URIs section in portal, setredirect URIs.
Example:
redirectUri : https://localhost:44365/signin-oidc
In the Logout URL section, https://localhost:44365/signout-oidc or : https://localhost:44365/signin-oidc
Please check Configure session behavior - Azure Active Directory B2C | Microsoft Learn
In appsetting.json set "CallbackPath": "/signin-oidc" and set a userflow SignedOutCallbackPath
appsettings.json:
"AzureAd": {
"Authority": "https://xx.b2clogin.com/XXXXXX.onmicrosoft.com/B2C_1_SignUpSignIn",
"Instance": "https://XXXXXX.b2clogin.com",
"TenantId": "XXXXXX-XXXXXX-XXXXXX-XXXXXX-XXXXXX",
"ClientId": "XXXXXX-XXXXXX-XXXXXX-XXXXXX-XXXXXX",
"ClientSecret": "XXXXXXXXXXXXXXXXXX"
"CallbackPath": "/signin-oidc",
"Domain": "XXXXXX.onmicrosoft.com",
"SignUpSignInPolicyId": "B2C_1_SignUpSignIn",
"SignedOutCallbackPath": "/signout/B2C_1_susi",
"ResetPasswordPolicyId": "B2C_1_PasswordReset",
"EditProfilePolicyId": "B2C_1_EditProfile",
},
"API": {
"BaseUrl": "",
"Scopes": "https://XXXXXX.onmicrosoft.com/ xxx/<scope>"
},
In startup.cs:
builder.Services.Configure<OpenIdConnectOptions>(OpenIdConnectDefaults.AuthenticationScheme, options =>
{
options.Events.OnSignedOutCallbackRedirect = context =>
{
context.HttpContext.Response.Redirect(context.Options.SignedOutRedirectUri);
context.HandleResponse();
return Task.CompletedTask;
};
});
Or
set the prompt value to login or select_account using OnRedirectToIdentityProvider when OIDC authentication handler is registered
public void ConfigureServices(IServiceCollection services)
{
.....
services.Configure<OpenIdConnectOptions>(AzureADB2CDefaults.OpenIdScheme, options =>
{
options.Events = new OpenIdConnectEvents
{
OnRedirectToIdentityProvider = context =>
{
if (context.Properties.Items.TryGetValue("prompt", out string prompt))
{
context.ProtocolMessage.Prompt = prompt;
}
return Task.CompletedTask;
}
};
...
}
When I logged in
Selected logout which redirects to signout path which redirects to post_logout_redirect_uri=https%3A%2F%2Flocalhost%3A44365%2Fsignout-callback-oidc&state=
Then if I clicked back , then I cant access other areas without login
Reference : https://github.com/Azure-Samples/ms-identity-blazor-server/blob/main/WebApp-OIDC/B2C/blazorserver-B2C/appsettings.json

Related

Admin and user login from same login page in react and back end i am using firebase

I want to use one login form for admin and a user in my project for react js and firebase,only the admin to be able to login and be redirected to the admin panel and the user to the user profile with,
Admin panel and user login in firebase and reactjs. Anybody knows how to do this?
There are some hints and keywords:
1> For firebase auth user, you can set custom claims to set role to user, ex. admin or user. Please refer to the link below
https://firebase.google.com/docs/auth/admin/custom-claims
Firebase - how to set custom claim from the console
2> When log in, you have to listen to auth state changed to get user role thanks to the getIdTokenResult function
onAuthStateChanged(auth, async (user) => {
if (user) {
user.getIdTokenResult(true).then((result) => {
// Confirm the user is an Admin.
if (!!result.claims.admin) {
// Show admin UI.
showAdminUI();
} else {
// Show regular user UI.
showRegularUI();
}
})
}
});
or
Check for firebase's auth user's role when or after logging in
https://firebase.google.com/docs/auth/admin/custom-claims#access_custom_claims_on_the_client

IdentityServer4 - How to Logout Blazor Webassembly Client from IdentityServer

i am playing around with Blazor WASM and IdentityServer4. Login/Logut flows invoked from the client are all working well. Used Microsofts documentation found here Microsofts Docs
IdentityServer4 is hosted as a seperate Microservice as well as the Blazor WASM App - two indepented projects.
Now i am facing the problem of signing out from the IdentiyServer4. Invoking the logout from the IdentityServer4 UI doesnt logout the user from the Blazor WASM App. I already read this explenation signout IdentityServer4
"oidc": {
"Authority": "http://localhost:8010/",
"ClientId": "demoportal.blazor",
"DefaultScopes": [
"openid",
"profile"
],
"PostLogoutRedirectUri": "http://localhost:8070/authentication/logout-callback",
"RedirectUri": "http://localhost:8070/authentication/login-callback",
"ResponseType": "code"
}
I haven´t found anything so far to achieve the goal. From my unterstanding it has to be used as oidc connect session managements not front or backend channel policy. But i cant find any useful docs on microsofts site.
After lots of reading ive found the answer.
Microsoft descripes the SPA difficulties right here: Microsoft Handle-Token-Request-Errors
These pointed me to implement on my base component something like this:
protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync();
var user = (await authenticationStateTask).User;
if (user.Identity.IsAuthenticated)
{
var tokenResult = await AccessTokenProvider.RequestAccessToken();
if(tokenResult.Status == AccessTokenResultStatus.RequiresRedirect)
{
NavigationManager.NavigateTo(tokenResult.RedirectUrl);
}
}
}
It works like a charm.
Btw dont forget to include the token when configuring HttpClient.
services.AddHttpClient<YOURSERVICEHERE>()
.AddHttpMessageHandler(sp =>
{
var handler = sp.GetService<AuthorizationMessageHandler>()
.ConfigureHandler(
authorizedUrls: new[] { "URI here" },
scopes: new[] { "your scope here" });
return handler;
})
.AddHttpMessageHandler<BaseAddressAuthorizationMessageHandler>()

Setting up Azure B2C with React

I am trying to configure my react/.NET 5.0 application to work with Azure B2C. I have everything set up , I have tried to run this against an MVC application and I get the login screen. But for some reason, when I try to redirect from a react page, I keep getting the same error. There appears to be almost no real good documentation for this as well. This is my authConfig file.
export const msalConfig = {
auth: {
clientId: process.env.REACT_APP_ADB2C_CLIENT_ID
, authority: process.env.REACT_APP_ADB2C_AUTHORITY
, knownAuthorities: [process.env.REACT_APP_KNOWN_AUTH]
, clientSecret: process.env.REACT_APP_CLIENT_SECRET
, reponseType: 'code'
},
cache: {
cacheLocation: 'sessionStorage'
,storeAuthStateInCoolie: false
}
};
const b2cPolicies = {
name: {
signUpSignIn: "B2C_1_cp_signin_up"
, forgotPassword: "B2C_1_cp_forgot_pwd"
, editProfile: "B2C_1_cp_edit_profile"
},
authorities: {
signUpSignIn: {
authority: `https://${process.env.REACT_APP_TENANT_LOGIN}/${process.env.REACT_APP_TENANT}/${process.env.REACT_APP_SIGNUP_POLICY}`,
},
forgotPassword: {
authority: `https://${process.env.REACT_APP_TENANT_LOGIN}/${process.env.REACT_APP_TENANT}/${process.env.REACT_APP_FORGOT_POLICY}`,
},
editProfile: {
authority: `https://${process.env.REACT_APP_TENANT_LOGIN}/${process.env.REACT_APP_TENANT}/${process.env.REACT_APP_EDIT_POLICY}`
}
},
authorityDomain: process.env.REACT_APP_TENANT_LOGIN
}
export const loginRequest = {
scopes: ["openid", "profile"],
};
I keep running into this error when I click on the link to redirect.
Any help with this would be great.
The reply URL must begin with the scheme https. Please check if reply urls are configured correctly which must be same in azure portal and in code .
Check if the callback path is set to identity provider something like /signin-oidc for redirect url .(And make sure you have unique callback if multiple urls are used.
such as https://contoso.com/signin-oidc.
The CallbackPath is the path where server will redirect during authentication. It's automatically handled by the OIDC middleware itself, that means we can't control the logic by creating a new controller/action and set CallbackPath to it
If you have check marked id_token in portal, try redirecting to home page ,instead of api actions directly.
Change the cookies to be set as secure using this in start up class
services.Configure(options =>
{
options.CheckConsentNeeded = context => true;//add if consent needed
options.MinimumSameSitePolicy = SameSiteMode.None; // else try SameSiteMode.Lax;
options.Secure = CookieSecurePolicy.Always;
});
use Microsoft.AspNetCore.HttpOverrides; reference in startup.cs class, by including the nuget package.
Also check and Add > app.UseHttpsRedirection(); above app.authentication(); in startup configure method.
then try again.
If not try to set ProtocolMessage.RedirectUri to the HTTPS url
services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(options =>
{
Configuration.Bind("AzureAdB2C", options);
options.Events = new OpenIdConnectEvents
{
OnRedirectToIdentityProvider = async ctx =>
{
/* Redirect Uri modified for https protocol */
ctx.ProtocolMessage.RedirectUri = urlWithHttps
}
}
});
Or you can pass login hint :Please refer this doc.
References:
Tutorial: Register an application - Azure AD B2C | Microsoft Docs
Configure authentication in a sample web application by using Azure Active Directory B2C | Microsoft Docs

Graph API with ASP.NET Core Blazor WebAssembly

I would like to get information from Microsoft graph web API. I followed these instructions:
https://learn.microsoft.com/en-us/aspnet/core/blazor/security/webassembly/graph-api?view=aspnetcore-5.0
The problem is that the variable "token" in the AuthenticateRequestAsync method is always null. It means that the Blazor app does not get the token.
public async Task AuthenticateRequestAsync(HttpRequestMessage request)
{
var result = await TokenProvider.RequestAccessToken(
new AccessTokenRequestOptions()
{
Scopes = new[] { "https://graph.microsoft.com/User.Read" }
});
if (result.TryGetToken(out var token))
{
request.Headers.Authorization ??= new AuthenticationHeaderValue(
"Bearer", token.Value);
}
}
The Program.cs has the following code:
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
builder.Services.AddMsalAuthentication<RemoteAuthenticationState, RemoteUserAccount>(options =>
{
options.ProviderOptions.DefaultAccessTokenScopes.Add("https://graph.microsoft.com/User.Read");
builder.Configuration.Bind("AzureAd", options.ProviderOptions.Authentication);
});
builder.Services.AddGraphClient("https://graph.microsoft.com/User.Read");
In Index.razor I just add two lines of code I OnInitializedAsync method
var request = GraphClient.Me.Request();
user = await request.GetAsync();
I spent a lot of time to figure out what is the main issue but without success. I will appreciate any help.
Please imagine the single-page website. Usually, this kind of page has a "contact us" tab where is the contact form. If the user fills up the contact form then data have to be somehow sent to us. For this purpose, I tried to use MS graph API. When the user clicks the submit button, in the background the registration to my account will be created and an email will be sent to me. It means that the user is not aware of any registration procedure. – Samo Simoncic
For your app to be able to create users in a tenant, it needs to use an app only flow which requires a secret. We do not advise exposing app only flows of this nature, which can easily be exploited to create bogus users or overwhelm your tenant, open to the general public.
The best approach would be to take this registrations in a local DB, and then have a daemon app process them behind the scenes. Here is the sample where daemon console application is calling Microsoft Graph.
Not sure about the cause of the issue.
But I can make it work with the following code and configuration:
Program.cs
public static async Task Main(string[] args)
{
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("app");
// Adds the Microsoft graph client (Graph SDK) support for this app.
builder.Services.AddMicrosoftGraphClient("https://graph.microsoft.com/User.Read");
// Integrates authentication with the MSAL library
builder.Services.AddMsalAuthentication(options =>
{
builder.Configuration.Bind("AzureAd", options.ProviderOptions.Authentication);
options.ProviderOptions.DefaultAccessTokenScopes.Add("https://graph.microsoft.com/User.Read");
});
await builder.Build().RunAsync();
}
appsettings.json
{
"AzureAd": {
"Authority": "https://login.microsoftonline.com/exxxxx4e-bd27-40d5-8459-230ba2xxxxxb",
"ClientId": "7xxxxxx8-88b3-4c02-a2f8-0a890xxxxxx5",
"CallbackPath": "/signin-oidc",
"ValidateAuthority": "true",
"DefaultScopes": [
"openid",
"profile"
]
}
}
You can refer to the configuration and sample code here.
I have cloned your repo from the GitHub URL you posted in the comments.
There is no issue with the code to fetch the data from the Microsoft Graph API, the problem is that you have written the code of calling the API when the apps shows the index component before even the user logs in, you have to check if the user is logged in first and add a login button to the UI or you can add [Authorize] to the index page so it will redirect the user to Login before it shows the component and make the API and to implement that make sure to add the CascadingAuthenticationState and AuthorizeView to your App.razor as following
<CascadingAuthenticationState>
<Router AppAssembly="#typeof(Program).Assembly">
<Found Context="routeData">
<AuthorizeRouteView RouteData="#routeData" DefaultLayout="#typeof(MainLayout)">
<NotAuthorized>
#if (!context.User.Identity.IsAuthenticated)
{
<a class="btn btn-success" href="/authentication/login">Login with Microsoft</a>
}
else
{
<p>You are not authorized to access this resource.</p>
}
</NotAuthorized>
</AuthorizeRouteView>
</Found>
<NotFound>
<LayoutView Layout="#typeof(MainLayout)">
<p>Sorry, there's nothing at this address.</p>
</LayoutView>
</NotFound>
</Router>
</CascadingAuthenticationState>
And then in your Index.razor add at the top the following line
#attribute [Authorize]
Then you launch the app if the user is not logged in, he/she will be asked to do so and then go to the Index component and make the API call which will succed then

Authentication with oidc-client.js and Identityserver4 in a React frontend

Lately I'm trying to set-up authentication using IdentityServer4 with a React client. I followed the Adding a JavaScript client tutorial (partly) of the IdentityServer documentation: https://media.readthedocs.org/pdf/identityserver4/release/identityserver4.pdf also using the Quickstart7_JavaScriptClient file.
The downside is that I'm using React as my front-end and my knowledge of React is not good enough to implement the same functionality used in the tutorial using React.
Nevertheless, I start reading up and tried to get started with it anyway. My IdentityServer project and API are set-up and seem to be working correctly (also tested with other clients).
I started by adding the oidc-client.js to my Visual Code project. Next I created a page which get's rendered at the start (named it Authentication.js) and this is the place where the Login, Call API and Logout buttons are included. This page (Authentication.js) looks as follows:
import React, { Component } from 'react';
import {login, logout, api, log} from '../../testoidc'
import {Route, Link} from 'react-router';
export default class Authentication extends Component {
constructor(props) {
super(props);
}
render() {
return (
<div>
<div>
<button id="login" onClick={() => {login()}}>Login</button>
<button id="api" onClick={() => {api()}}>Call API</button>
<button id="logout" onClick={() => {logout()}}>Logout</button>
<pre id="results"></pre>
</div>
<div>
<Route exact path="/callback" render={() => {window.location.href="callback.html"}} />
{/* {<Route path='/callback' component={callback}>callback</Route>} */}
</div>
</div>
);
}
}
In the testoidc.js file (which get's imported above) I added all the oidc functions which are used (app.js in the example projects). The route part should make the callback.html available, I have left that file as is (which is probably wrong).
The testoidc.js file contains the functions as follow:
import Oidc from 'oidc-client'
export function log() {
document.getElementById('results').innerText = '';
Array.prototype.forEach.call(arguments, function (msg) {
if (msg instanceof Error) {
msg = "Error: " + msg.message;
}
else if (typeof msg !== 'string') {
msg = JSON.stringify(msg, null, 2);
}
document.getElementById('results').innerHTML += msg + '\r\n';
});
}
var config = {
authority: "http://localhost:5000",
client_id: "js",
redirect_uri: "http://localhost:3000/callback.html",
response_type: "id_token token",
scope:"openid profile api1",
post_logout_redirect_uri : "http://localhost:3000/index.html",
};
var mgr = new Oidc.UserManager(config);
mgr.getUser().then(function (user) {
if (user) {
log("User logged in", user.profile);
}
else {
log("User not logged in");
}
});
export function login() {
mgr.signinRedirect();
}
export function api() {
mgr.getUser().then(function (user) {
var url = "http://localhost:5001/identity";
var xhr = new XMLHttpRequest();
xhr.open("GET", url);
xhr.onload = function () {
log(xhr.status, JSON.parse(xhr.responseText));
}
xhr.setRequestHeader("Authorization", "Bearer " + user.access_token);
xhr.send();
});
}
export function logout() {
mgr.signoutRedirect();
}
There are multiple things going wrong. When I click the login button, I get redirected to the login page of the identityServer (which is good). When I log in with valid credentials I'm getting redirected to my React app: http://localhost:3000/callback.html#id_token=Token
This client in the Identity project is defined as follows:
new Client
{
ClientId = "js",
ClientName = "JavaScript Client",
AllowedGrantTypes = GrantTypes.Implicit,
AllowAccessTokensViaBrowser = true,
// where to redirect to after login
RedirectUris = { "http://localhost:3000/callback.html" },
// where to redirect to after logout
PostLogoutRedirectUris = { "http://localhost:3000/index.html" },
AllowedCorsOrigins = { "http://localhost:3000" },
AllowedScopes =
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
"api1"
}
}
Though, it seems the callback function is never called, it just stays on the callback url with a very long token behind it..
Also the getUser function keeps displaying 'User not logged in' after logging in and the Call API button keeps saying that there is no token. So obviously things are not working correctly. I just don't know on which points it goes wrong.
When inspecting I can see there is a token generated in the local storage:
Also when I click the logout button, I get redirected to the logout page of the Identity Host, but when I click logout there I don't get redirected to my client.
My questions are:
Am I on the right track implementing the oidc-client in combination with IdentityServer4?
Am I using the correct libraries or does react require different libraries than the oidc-client.js one.
Is there any tutorial where a react front-end is used in combination with IdentityServer4 and the oidc-client (without redux), I couldn't find any.
How / where to add the callback.html, should it be rewritten?
Could someone point me in the right direction, there are most likely more things going wrong here but at the moment I am just stuck in where to even begin.
IdentityServer4 is just a backend implementation of OIDC; so, all you need to do is implement the flow in the client using the given APIs. I don't know what oidc-client.js file is but it is most likely doing the same thing that you could have implemented yourself. The flow itself is very simple:
React app prepares the request and redirects the user to the Auth server with client_id and redirect_uri (and state, nonce)
IdentityServer checks if the client_id and redirect_uri match.
If the user is not logged in, show a login box
If a consent form is necessary (similar to when you login via Facebook/Google in some apps), show the necessary interactions
If user is authenticated and authorized, redirect the page to the redirect_uri with new parameters. In your case, you the URL will look like this: https://example.com/cb#access_token=...&id_token=...&stuff-like-nonce-and-state
Now, the React app needs to parse the URL, access the values, and store the token somewhere to be used in future requests:
Easiest way to achieve the logic is to first set a route in the router that resolves into a component that will do the logic. This component can be "invisible." It doesn't even need to render anything. You can set the route like this:
<Route path="/cb" component={AuthorizeCallback} />
Then, implement OIDC client logic in AuthorizeCallback component. In the component, you just need to parse the URL. You can use location.hash to access #access_token=...&id_token=...&stuff-like-nonce-and-state part of the URL. You can use URLSearchParams or a 3rd party library like qs. Then, just store the value in somewhere (sessionStorage, localStorage, and if possible, cookies). Anything else you do is just implementation details. For example, in one of my apps, in order to remember the active page that user was on in the app, I store the value in sessionStorage and then use the value from that storage in AuthorizeCallback to redirect the user to the proper page. So, Auth server redirects to "/cb" that resolves to AuthorizeCallback and this component redirects to the desired location (or "/" if no location was set) based on where the user is.
Also, remember that if the Authorization server's session cookie is not expired, you will not need to relogin if the token is expired or deleted. This is useful if the token is expired but it can be problematic when you log out. That's why when you log out, you need to send a request to Authorization server to delete / expire the token immediately before deleting the token from your storage.

Resources