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>()
Related
So I'm fairly new with using Keycloak and I'm using this tutorial to install it with my React & TS app.
https://blog.devgenius.io/security-in-react-and-webapi-in-asp-net-core-c-with-authentification-and-authorization-by-keycloak-89ba14be7e5a
That author says we should set the Access Type to confidential.
I've done the settings he says there (literally the same) and I get
{"error":"unauthorized_client","error_description":"Client secret not provided in request"}
my keycloak.json (which is in the public/ folder)
{
"realm": "best-realm",
"auth-server-url": "http://localhost:28080/auth/",
"ssl-required": "external",
"resource": "best-react",
"verify-token-audience": true,
"credentials": {
"secret": "secret"
},
"use-resource-role-mappings": true,
"confidential-port": 0
}
KeycloakService.tsx
import Keycloak from "keycloak-js";
const keycloakInstance = new Keycloak();
/**
* Initializes Keycloak instance and calls the provided callback function if successfully authenticated.
*
* #param onAuthenticatedCallback
*/
const Login = (onAuthenticatedCallback: Function) => {
keycloakInstance
.init({ onLoad: "login-required" })
.then(function (authenticated) {
authenticated ? onAuthenticatedCallback() : alert("non authenticated");
})
.catch((e) => {
console.dir(e);
console.log(`keycloak init exception: ${e}`);
});
};
const KeyCloakService = {
CallLogin: Login,
};
export default KeyCloakService;
Why am I getting this error? I've read some posts that access type confidential doesn't work anymore with a JS adapter. But those posts were older than the posting date of that tutorial (it is written in may 2022). So I don't know what to believe.
Can anybody help me understand this error and teach me how to fix it?
Thanks.
In keycloak.js removed "credential" access type option.
Official comment about this since Keycloak 8.0.0
https://www.keycloak.org/docs/latest/release_notes/#credentials-support-removed-from-the-javascript-adapter
You should be use public option in front-end side.
The public option with PCKE(Proof Key for Code Exchange) is protect to steal token that is intended for another app.
Understanding benefits of PKCE vs. Authorization Code Grant
This web site shows how to use PCKE from Keycloak
https://www.appsdeveloperblog.com/pkce-verification-in-authorization-code-grant/
I am trying to set up a web app and web API using Azure B2C as the auth mechanism, but apparently failing miserably. My web app and API are both registered in Azure, the return URIs set properly, a scope created for the API which is then granted to the app, an app client secret created, and I have tested this locally WITH IT WORKING, but now that I've published my API to my live Azure App Service, it goes wrong for some reason.
Locally, I have run my web API and web app separately, signed into the web app and then obtained an access token, called the API (running locally) and all works well. However, now the API is published and running live (i.e. https://api.myapp.net), I am running the web app locally, calling the live API and every single time I request an access token then send it to the API I get a 401 Unauthorized because apparently the signature is invalid. WHY?!?!?!?
Below are slightly redacted copies of my startup and app settings files. Note I also have Azure Front Door set up to redirect "login.myapp.net" to "myapp.b2clogin.com", this has been tested with both the user flows on the Azure dashboard and with my own app running locally and all is fine.
Here is my web app's Startup.cs file:
services.AddRazorPages();
services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(Configuration.GetSection(Constants.AzureAdB2C))
.EnableTokenAcquisitionToCallDownstreamApi(new string[] { "https://myapp.net/api/query" })
.AddInMemoryTokenCaches();
services
.AddControllersWithViews()
.AddMicrosoftIdentityUI();
services
.AddServerSideBlazor()
.AddMicrosoftIdentityConsentHandler();
services.AddAuthorization(authorizationOptions =>
{
authorizationOptions.AddPolicy(
App.Policies.CanManageUsers,
App.Policies.CanManageUsersPolicy());
});
services.Configure<OpenIdConnectOptions>(OpenIdConnectDefaults.AuthenticationScheme, options =>
{
options.ResponseType = "code";
options.SaveTokens = true;
}).AddInMemoryTokenCaches();
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddSingleton<AppData>();
services.AddScoped<IAuthTokensService, AuthTokensService>();
services.AddOptions();
My web app's appsettings.json file:
"AzureAdB2C": {
"Instance": "https://login.myapp.net",
"ClientId": "my-web-app-client-id",
"CallbackPath": "/signin-oidc",
"Domain": "my-b2c-tenant-id",
"SignedOutCallbackPath": "/signout/B2C_1_SignUpIn",
"SignUpSignInPolicyId": "B2C_1_SignUpIn",
"ResetPasswordPolicyId": "B2C_1_PasswordReset",
"EditProfilePolicyId": "B2C_1_ProfileEdit",
"ClientSecret": "my-client-secret"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*"
Calling for an access token in my web app:
private readonly ITokenAcquisition _tokenAcquisition;
public AuthTokensService(ITokenAcquisition tokenAcquisition)
{
_tokenAcquisition = tokenAcquisition;
}
/// <inheritdoc />
public async Task<string> GetToken()
{
return await _tokenAcquisition.GetAccessTokenForUserAsync(new[] { "https://myapp.net/api/query" });
}
My web API's Startup.cs file:
if (Configuration.GetConnectionString("SQLConnection") == null)
{
throw new InvalidOperationException("ConfigureServices: Connection string 'SQLConnection' returned null.");
}
services.AddDbContext<MyAppDbContext>(
option => option.UseSqlServer(Configuration.GetConnectionString("SQLConnection")));
services.AddMicrosoftIdentityWebApiAuthentication(Configuration, Constants.AzureAdB2C);
services.AddControllers();
services.AddScoped<IMyRepository, MyRepository>();
And my web API's appsettings.json file:
"AzureAdB2C": {
"Instance": "https://login.myapp.net",
"ClientId": "my-web-api-client-id",
"Domain": "my-b2c-tenant-id",
"SignUpSignInPolicyId": "B2C_1_SignUpIn",
"SignedOutCallbackPath": "/signout/B2C_1_SignUpIn"
},
"ConnectionStrings": {
"SQLConnection": "Server=(localdb)\\mssqllocaldb;Database=MyAppDebug;Trusted_Connection=True;MultipleActiveResultSets=true"
},
"Logging": {
"LogLevel": {
"Default": "Debug",
"System": "Information",
"Microsoft": "Information"
}
}
Can anybody advise on why this is? The only sensible answer I've seen anywhere online is that Blazor uses a v1.0 endpoint to obtain public keys whereas v2.0 is required for the API (see here) and I can see this happening with my tokens, but when I followed the fix given on the Microsoft documentation, I then get an exception thrown on startup of my web app IDX20807: Unable to retrieve document from: 'System.String'.
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
I am trying to set up an app with a react front end + a .NET Core back end in Azure with Azure AD Auth. The back end will call other APIs and hold some logic. I set up the .NET Core app and hosted it in an Azure app service, then added authentication using the connected services wizard in visual studio, which generated code similar to what is on this tutorial (back end section):
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(sharedOptions =>
{
sharedOptions.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddAzureAdBearer(options => Configuration.Bind("AzureAd", options));
...
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseAuthentication();
...
}
appsettings.json (fake IDs):
"AzureAd": {
"ClientId": "1fff1098-3bc0-40d9-8cd0-e47b065085b6",
"Domain": "mytenant.onmicrosoft.com",
"Instance": "https://login.microsoftonline.com/",
"TenantId": "mytenantid",
"AppIDURL": "https://my-api.azurewebsites.net/",
"ConfigView": "API"
}
Then I set up react-adal on my front end with:
{
tenant: "mytenant.onmicrosoft.com",
clientId: "1fff1098-3bc0-40d9-8cd0-e47b065085b6",
endpoints: {
api: "1fff1098-3bc0-40d9-8cd0-e47b065085b6"
},
cacheLocation: "localStorage"
};
Which I set up according to the github instructions to set up react-adal. The sign in works as expected but when I run adalApiFetch against my back end, I get a 401 error with description = the signature is invalid. I can see on the debugger that the authorization header (Bearer + token) is sent. Any ideas on what I might be doing wrong here? Thanks in advance!
The endpoint I'm testing with is a simple test controller (with [Authorize]) that simply returns "Authentication Tested".
I was able to find my mistake after a while and came back to post the solution. The problem was that I was using the incorrect method/settings (not matching). From the question's code: If using sharedOptions.DefaultScheme = JwtBearerDefaults.AuthenticationScheme; then you should also use AddJwtBearer with the appropriate configuration options (found here: JwtBearerOptions) and not AddAzureAdBearer. In my case, the final corrected startup code was
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(AzureADDefaults.BearerAuthenticationScheme)
.AddAzureADBearer(options => Configuration.Bind("AzureAd",options));
....
With corresponding settings (found here: AzureADOptions)
Recently we have created a React front-end which communicates with our API back-end following this tutorial: https://itnext.io/a-memo-on-how-to-implement-azure-ad-authentication-using-react-and-net-core-2-0-3fe9bfdf9f36
Just as in the tutorial we have set-up the authentication in the front-end with the adal-react library. We added/registered the front-end in azure.
Next we created our API (.Net Core 2) and also registered this in the azure environment, the config is setup in the appsettings:
"AzureAd": {
"Instance": "https://login.microsoftonline.com/",
"TenantDomain": "our_azure_environment.onmicrosoft.com",
"TenantId": "our_azure_environment.onmicrosoft.com",
"ClientId": "our_front-end_azure_id_1234"
}
In the API we also added the JWT middleware in the ConfigureServices as follow:
services.AddAuthentication(sharedOptions =>
{
sharedOptions.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.Audience = Configuration["AzureAd:ClientId"];
options.Authority = $"{Configuration["AzureAd:Instance"]}{Configuration["AzureAd:TenantId"]}";
});
When testing (calling an endpoint from the front-end) after logging in the front-end works, the data is being returned and the user is authenticated (api endpoint has the Authorize attribute), when not logged in the api endpoint returns 401 (as it should).
The problem is as follows:
When I add the following piece of code to the API ConfigureServices (which I want to use to do some additional stuff after authenticating) :
options.Events = new JwtBearerEvents()
{
OnTokenValidated = context =>
{
//Check if user has a oid claim
if (!context.Principal.HasClaim(c => c.Type == "oid"))
{
context.Fail($"The claim 'oid' is not present in the token.");
}
return Task.CompletedTask;
}
};
suddenly, the calls to the API endpoint return a 401 (Unauthorized) error when logged in.. Though, if I remove the OnTokenValidated part it works fine.
When reaching the OnTokenValidated, the token should already be validated / authenticated or am I wrong?
IntelliSense also says; Invoked after the security token has passed validation and a ClaimsIdentity has been generated.
Did I forgot to add some setting? My feeling tells me that it is propably a wrong setup in azure itself but I have actually no clue.
The same token which is send from the front-end to the API is also being send to the graph API, when doing this, graph asks to give consent and after agreeing it works. With this in mind I believe I should add some permission to the API or something but I am not sure.
UPDATE
juunas pointed out in his comment below that I was using the wrong ClaimsPrincipal value this fixed the initial problem but now the following gave me the 401 error:
In my ConfigureServices (before the AddAuthentication part) I have added the following to manage / add users to my AspNetUsers table (in my azure database):
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<TRSContext>()
.AddDefaultTokenProviders();
When adding this code to the pipeline, I once more get the 401 error in the front-end. Any clue why this is?
UPDATE2
I found the solution for above (update). This was caused due to AddIdentity taken over the Authentication from JWT. This can be avoided by adding:
Options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
Options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
to .AddAuthentication options:
services.AddAuthentication(Options =>
{
Options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
Options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
Options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
More information about the above can be found here:
https://github.com/aspnet/Identity/issues/1376
The error appears in the first case due to the fact that .NET ClaimsPrincipal objects translate the oid claim type to: http://schemas.microsoft.com/identity/claims/objectidentifier.
So it needs to be like:
options.Events = new JwtBearerEvents()
{
OnTokenValidated = context =>
{
//Check if user has a oid claim
if (!context.Principal.HasClaim(c => c.Type == "http://schemas.microsoft.com/identity/claims/objectidentifier"))
{
context.Fail($"The claim 'oid' is not present in the token.");
}
return Task.CompletedTask;
}
};