Automatically Attaching Identity Cookie to HTTP Client in Blazor wasm - asp.net

I am working on a blazor application where I used my API project as Identity
Provider. Everything is working fine but the issue is that the access token
issued by my API is not validated by the API. It turns out the API is expecting a
cookie header. I took a closer look at blazor hosted application and found out
the cookie is being sent along with each request but it's same-origin.
My Blazor WASM project does not automatically attach this cookie in the request
header, just the access token.
Is there a way I can make the Http handler attach this cookie on each request?
or make the API validate the access token instead of the identity cookie.
This is my startup class in the API Project
public static void AddIdentityServer(IServiceCollection services,IConfiguration configuration)
{
services.AddIdentityServer(options =>
{
options.UserInteraction.LoginUrl = "/Identity/Account/Login";
options.UserInteraction.LogoutUrl = "/Identity/Account/Logout";
}).AddProfileService<LocalProfileService>()
.AddApiAuthorization<ApplicationUser, ApplicationDbContext>(option =>
{
option.Clients.Add(new Client
{
ClientId = "blazor",
AllowedGrantTypes = GrantTypes.Code,
RequirePkce = true,
RequireClientSecret = false,
AllowedCorsOrigins = { "https://localhost:5001" },
AllowedScopes = { "openid", "profile", "email","id" },
RedirectUris = { "https://localhost:5001/authentication/login-callback" },
PostLogoutRedirectUris = { "https://localhost:5001/" },
Enabled = true,
RequireConsent = false,
});
option.IdentityResources.AddEmail();
option.IdentityResources["openid"].UserClaims.Add("name");
option.ApiResources.Single().UserClaims.Add("name");
option.IdentityResources["openid"].UserClaims.Add("role");
option.ApiResources.Single().UserClaims.Add("role");
option.IdentityResources.Add(new IdentityResource("id",new string[] {"id" }));
option.ApiResources.Single().UserClaims.Add("id");
});
services.AddAuthentication()
.AddGoogle("Google", options =>
{
options.ClientId = configuration["ExternalLoginApiKey:GoogleClientId"];
options.ClientSecret = configuration["ExternalLoginApiKey:GoogleClientSecret"];
})
.AddFacebook("Facebook", options =>
{
options.AppId = configuration["ExternalLoginApiKey:FacebookAppId"];
options.AppSecret = configuration["ExternalLoginApiKey:FacebookAppSecret"];
})
.AddIdentityServerJwt();
}
Program class in the Blazor Project
public static async Task Main(string[] args)
{
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("app");
builder.Services.AddOidcAuthentication(options =>
{
builder.Configuration.Bind("oidc", options.ProviderOptions);
options.UserOptions.RoleClaim = "role";
}).AddAccountClaimsPrincipalFactory<CustomUserFactory>();
builder.Services.AddHttpClient<IAuthorizedRestService, AuthorizedRestService>(
client => client.BaseAddress = new Uri("https://localhost:5002/api/mart/v1/"))
.AddHttpMessageHandler(sp => sp.GetRequiredService<AuthorizationMessageHandler>()
.ConfigureHandler(authorizedUrls: new[] { "https://localhost:5002" }));
builder.Services.AddHttpClient("noauth", option => option.BaseAddress = new
Uri("https://localhost:5002/api/mart/v1/"));
builder.Services.AddScoped<IRestService, RestService>();
await builder.Build().RunAsync();
}

I have found the Solution.
It happens that there is already a JWT handler provided by IdentityServer4 for APIs that double as Authorization Server
.AddIdentityServerJwt();
So what I did was to configure it
services.Configure<JwtBearerOptions>
(IdentityServerJwtConstants.IdentityServerJwtBearerScheme,
options =>
{
options.Authority = "https://localhost:5002";
options.Audience = "mart";
options.SaveToken = true;
});
Then specify the Authentication scheme to use
[Authorize(AuthenticationSchemes = IdentityServerJwtConstants.IdentityServerJwtBearerScheme)]
You can also add it globally in the start up class
var authorizationPolicy = new AuthorizationPolicyBuilder(IdentityServerJwtConstants.IdentityServerJwtBearerScheme)
.RequireAuthenticatedUser().Build();
options.Filters.Add(new AuthorizeFilter(authorizationPolicy));
You can read more using these links
https://learn.microsoft.com/en-us/aspnet/core/security/authorization/limitingidentitybyscheme?view=aspnetcore-3.1
https://learn.microsoft.com/en-us/aspnet/core/security/authentication/identity-api-authorization?view=aspnetcore-3.1

Related

Firebase google auth in Swagger (Swashbuckle.AspNetCore)

Trying to implement firebase google authentication in the swagger
onboard: asp.net core 5, Swashbuckle.AspNetCore 6.3.1,
In fairbase console > authentication > Sign-in method > authentication via google is enabled
On ServiceConfigure method:
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "MyApi", Version = "v1" });
c.UseInlineDefinitionsForEnums();
c.AddSecurityDefinition("oauth2", new OpenApiSecurityScheme
{
Type = SecuritySchemeType.OAuth2,
Flows = new OpenApiOAuthFlows
{
//email and password authentication - works fine
Password = new OpenApiOAuthFlow
{
TokenUrl = new Uri("/api/v1/auth/password", UriKind.Relative), //here my backend endpoint
Extensions = new Dictionary<string, IOpenApiExtension>
{
{ "returnSecureToken", new OpenApiBoolean(true) },
},
},
//try add google auth - troble here
Implicit = new OpenApiOAuthFlow()
{
//Not sure about the endpoints. Its not work with 404 err
AuthorizationUrl = new Uri("https://securetoken.google.com/MY-PROJECT-FIREBASE-NAME"),
TokenUrl = new Uri("https://securetoken.google.com/MY-PROJECT-FIREBASE-NAME"),
Scopes = new Dictionary<string, string>
{
{ "profile", "profile" },
}
}
}
});
c.OperationFilter<AuthorizeCheckOperationFilter>();
});
class filter:
public class AuthorizeCheckOperationFilter : IOperationFilter
{
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
var requiredScopes = context.MethodInfo.DeclaringType.GetCustomAttributes(true)
.OfType<AuthorizeAttribute>()
.Select(attr => attr.Policy)
.Distinct();
if (requiredScopes.Any())
{
var oAuthScheme = new OpenApiSecurityScheme
{
Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "oauth2" }
};
operation.Security = new List<OpenApiSecurityRequirement>
{
new OpenApiSecurityRequirement
{
[ oAuthScheme ] = requiredScopes.ToList()
}
};
}
}
}
in Configure method:
app.UseSwagger();
app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "MyApi v1");
What endpoints for AuthorizationUrl / TokenUrl should call?
Any additinal options for swagger?
I`m new in Firebase. M.b. need aditional adjustments in firebase console?
I would be very appreciate for a code sample.
I found a good repo for you. This simple is .NET 5 WebApi project base with the following features implemented:
Swagger with Firebase login
Serilog Logging to Amazon CloudWatch
Encrypted fields in AppSettings
Soft Delete and Audit Columns
Multitenancy support
Data Seeding
Localization
code sample:clean-base-api

How to make jwt bearer token not required in .NET Core 6?

I have a configuration of JWT Bearer authentication, but sometimes instead of using JWT token, I want to use an API KEY in the request header and check this key in a middleware.
But in that case, when I don't put the bearer token in the header, I always respond with an Unauthorized response code.
How can I disable the bearer token check?
My configuration:
services.AddAuthentication(x =>
{
x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(options =>
{
// options.RequireHttpsMetadata = false;
// options.SaveToken = true;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
ValidateIssuer = true,
ValidateAudience = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(jwtSettings.Secret)),
ValidIssuer = jwtSettings.Issuer,
ValidAudiences = jwtSettings.Audiences,
ClockSkew = TimeSpan.Zero // remove delay of token when expire
};
});
Rather than checking in a middleware a more idiomatic way you can achieve this by using multipe AuthenticationSchemes. See the MSDN link for more details but at a very high level you can assign add multiple authentication schemes, each with a different scheme. You then refer to this scheme name when using the autorize attribute (e.g. [Authorize(AuthenticationSchemes = "Api-Key-Scheme")]).
services
.AddAuthentication()
.AddJwtBearer(options => { .. })
.AddApiKey(options => { .. }); // custom code
The .AddApiKey() method above will require a custom AuthenticationHandler<T> implementation, an example of how to do that can be found here - https://josef.codes/asp-net-core-protect-your-api-with-api-keys/
You can use the [AllowAnonymous] attribute on your method to disable the authentication check.
Then, create an ActionFilterAttribute and apply it to the same method:
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
namespace YourNameSpace
{
public class RequireYourKeyHeader : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
if(!filterContext.HttpContext.Request.Headers.TryGetValue("YourKey", out string headerValue))
{
filterContext.Result = new BadRequestObjectResult("missing header value");
}
// TODO: check if value passed in the header is actually valid
}
}
}
Apply with [RequireYourKeyHeader]

Blazor IdentityServer Authenthentication

Currently I have three separate servers. Client on :5001, API on :5002 and IdentityServer on :5003. I can athenticate my Blazor pages using #attribute [Authorize] but when I call the API I get a 401 error. If I past the token_id into postman and make a request to the API server it authenticates. If I make a request from my Blazor client it fails. I have whitelisted CORS to rule out that being the issue. If I remove the Audience check on the api with:
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateAudience = false
};
It works
Client program.cs
builder.Services.AddHttpClient("api")
.AddHttpMessageHandler(sp =>
{
var handler = sp.GetService<AuthorizationMessageHandler>()
.ConfigureHandler(
authorizedUrls: new[] { "https://localhost:5002" },
scopes: new[] { "coredesk" });
return handler;
});
builder.Services.AddScoped(
sp => sp.GetService<IHttpClientFactory>().CreateClient("api"));
builder.Services.AddOidcAuthentication(options =>
{
builder.Configuration.Bind("oidc", options.ProviderOptions);
});
Client appsettings.json
{
"oidc": {
"Authority": "https://localhost:5003/",
"ClientId": "coredesk",
"DefaultScopes": [
"openid",
"profile",
"coredesk"
],
"PostLogoutRedirectUri": "/",
"ResponseType": "code"
}
}
API Startup.cs
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.Authority = "https://localhost:5003";
options.Audience = "coredesk";
});
IdentityServer Config.cs
public static IEnumerable<IdentityResource> IdentityResources =>
new IdentityResource[]
{
new IdentityResources.OpenId(),
new IdentityResources.Profile(),
};
public static IEnumerable<ApiResource> Apis =>
new ApiResource[]
{
new ApiResource("coredesk", "CoreDesk API")
};
public static IEnumerable<ApiScope> ApiScopes =>
new ApiScope[]
{
new ApiScope("coredesk"),
};
public static IEnumerable<Client> Clients =>
new Client[]
{
new Client
{
ClientId = "coredesk",
AllowedGrantTypes = GrantTypes.Code,
RequirePkce = true,
RequireClientSecret = false,
AllowedCorsOrigins = { "https://localhost:5001", "https://localhost:5002" },
AllowedScopes = { "openid", "profile", "coredesk" },
RedirectUris = { "https://localhost:5001/authentication/login-callback" },
PostLogoutRedirectUris = { "https://localhost:5001/" },
Enabled = true
},
};
if you look at the source code for AddJwtBearer you find this part:
// If no authorization header found, nothing to process further
if (string.IsNullOrEmpty(authorization))
{
return AuthenticateResult.NoResult();
}
if (authorization.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase))
{
token = authorization.Substring("Bearer ".Length).Trim();
}
// If no token found, no further work possible
if (string.IsNullOrEmpty(token))
{
return AuthenticateResult.NoResult();
}
this shows that by default (without customization) it requires the token to be provided in the Authorization header, like this:
GET /api/payments HTTP/1.1
Host: localhost:7001
Authorization: Bearer eyJhbGciOiJSUzI1NiIsIYwA...
to further debug, I would remove the authorize attribute and then put a breakpoint in one of the action method and examine what the ClaimsPrincipal user object contains.

Authentication with MVC Client 4.7.1 and IdentityServer4

I am trying to integrate user authentication between an MVC 4.7.1 client and an ASP.NET Core 3.1 IdentityServer4 & ASP.NET Identity service.
I have been following this tutorial for cookie issued authentication: Refreshing your Legacy ASP.NET IdentityServer Client Applications (with PKCE)
So far, the MVC client is able to redirect to the Login page. Upon logging in, I have a null error in the following function:
private string RetrieveCodeVerifier(AuthorizationCodeReceivedNotification n)
{
string key = GetCodeVerifierKey(n.ProtocolMessage.State);
string codeVerifierCookie = n.Options.CookieManager.GetRequestCookie(n.OwinContext, key);
if (codeVerifierCookie != null)
{
var cookieOptions = new CookieOptions
{
SameSite = SameSiteMode.None,
HttpOnly = true,
Secure = n.Request.IsSecure
};
n.Options.CookieManager.DeleteCookie(n.OwinContext, key, cookieOptions);
}
string codeVerifier;
var cookieProperties = n.Options.StateDataFormat.Unprotect(Encoding.UTF8.GetString(Convert.FromBase64String(codeVerifierCookie)));
cookieProperties.Dictionary.TryGetValue("cv", out codeVerifier);
return codeVerifier;
}
Apparently the codeVerifierCookie is null.
The rest of the configuration is as follows.
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = "cookie"
});
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
ClientId = "mvc.owin",
Authority = "https://localhost:44355",
RedirectUri = "http://localhost:5001/Auth/",
Scope = "openid profile scope1",
SignInAsAuthenticationType = "cookie",
RequireHttpsMetadata = false,
UseTokenLifetime = false,
RedeemCode = true,
SaveTokens = true,
ClientSecret = "secret",
ResponseType = "code",
ResponseMode = "query",
Notifications = new OpenIdConnectAuthenticationNotifications
{
RedirectToIdentityProvider = n =>
{
if (n.ProtocolMessage.RequestType == OpenIdConnectRequestType.Authentication)
{
// set PKCE parameters
var codeVerifier = CryptoRandom.CreateUniqueId(32);
string codeChallenge;
using (var sha256 = SHA256.Create())
{
var challengeBytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(codeVerifier));
codeChallenge = Base64Url.Encode(challengeBytes);
}
n.ProtocolMessage.SetParameter("code_challenge", codeChallenge);
n.ProtocolMessage.SetParameter("code_challenge_method", "S256");
// remember code_verifier (adapted from OWIN nonce cookie)
RememberCodeVerifier(n, codeVerifier);
}
return Task.CompletedTask;
},
AuthorizationCodeReceived = n =>
{
// get code_verifier
var codeVerifier = RetrieveCodeVerifier(n);
// attach code_verifier
n.TokenEndpointRequest.SetParameter("code_verifier", codeVerifier);
return Task.CompletedTask;
}
}
});
And on IdentityServer4 ConfigureServices:
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
var builder = services.AddIdentityServer(options =>
{
options.Events.RaiseErrorEvents = true;
options.Events.RaiseInformationEvents = true;
options.Events.RaiseFailureEvents = true;
options.Events.RaiseSuccessEvents = true;
// see https://identityserver4.readthedocs.io/en/latest/topics/resources.html
options.EmitStaticAudienceClaim = true;
})
.AddInMemoryIdentityResources(Config.IdentityResources)
.AddInMemoryApiScopes(Config.ApiScopes)
.AddInMemoryClients(Config.Clients)
.AddAspNetIdentity<ApplicationUser>();
Finally, Client.cs configuration on IdentityServer4 side:
new Client
{
ClientId = "mvc.owin",
ClientName = "MVC Client",
AllowedGrantTypes = GrantTypes.Code,
ClientSecrets = {new Secret("secret".Sha256())},
RedirectUris = {"http://localhost:5001/Auth/"},
AllowedScopes = {"openid", "profile", "scope1"},
AllowPlainTextPkce = false,
RequirePkce = true,
RequireConsent = false,
// Token lifetimes
AuthorizationCodeLifetime = 60,
AccessTokenLifetime = 60,
IdentityTokenLifetime = 60
}
AccountController and the rest is pretty much the basic IdentityServer4.Template, specifically is4aspid.
Anyone tried the same and knows what fails?
Is there a way to do it with JWT instead of Cookies? And, what are the drawbacks?
Edit: Apparently, this configuration works with Firefox, and I am suspecting this is a problem with Chrome's Same-Site cookie policy, hence the null in GetRequestCookie. The thing is, IdentityServer4 is running on HTTPS (otherwise, there are others) while the MVC client app is running on HTTP (note: both on localhost). I have tried using SameSite policy None, Lax, Strict and vise-versa with no success. I am not sure what else to try.
Best,
mkanakis.
Cookies that assert SameSite=None must also be marked as Secure, this means you need to use https
Read more here

'Invalid token' when using webapi thru Swagger authenticated by Azure AD

I try to use AAD authentification on my WebApi (dotnet core 3.1) from swagger (Swashbuckle) client.
In my Startup class, I've configured like follow:
// Configure authentication
services.AddAuthentication(AzureADDefaults.JwtBearerAuthenticationScheme)
.AddAzureADBearer(options => Configuration.Bind("AzureAd", options));
my settings for AzureAd:
"AzureAd": {
"Instance": "https://login.microsoftonline.com/",
"ClientId": "xxxx-xxxxx1b",
"Domain": "myoffice.onmicrosoft.com",
"TenantId": "xxxxx-xxxxa5",
"Scope": "api://xxxxxxxx-abc3cff48f1b/Full.Access",
"ScopeDescription": "Full Access"
},
...
services.AddSwaggerGen(c =>
{
c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme()
{
Type = SecuritySchemeType.OAuth2,
In = ParameterLocation.Header,
Flows = new OpenApiOAuthFlows()
{
Implicit = new OpenApiOAuthFlow
{
TokenUrl = new Uri($"Configuration["AzureAd:Instance"]}/{Configuration["AzureAd:TenantId"]}/oauth2/v2.0/token"),
AuthorizationUrl = new Uri($"{Configuration["AzureAd:Instance"]}/{Configuration["AzureAd:TenantId"]}/oauth2/v2.0/authorize"),
Scopes =
{
{
Configuration["AzureAd:Scope"],Configuration["AzureAd:ScopeDescription"]
}
}
}
}
});
c.AddSecurityRequirement(new OpenApiSecurityRequirement
{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme,
Id = "Bearer"
}
},
Array.Empty<string>()
}
});
});
And in my Configure method:
app.UseSwaggerUI(c =>
{
c.RoutePrefix = string.Empty;
c.SwaggerEndpoint($"/swagger/{ApiVersion}/swagger.json", ApiName);
c.OAuthClientId(Configuration["AzureAd:ClientId"]);
c.OAuthScopeSeparator(" ");
});
Swagger corectly log to AAD using my credential and when I use a route protected by [Authorize] the token is correcly sent to the API by I receive a 401 error with the following message:
www-authenticate: Bearer error="invalid_token"error_description="The issuer 'https://login.microsoftonline.com/{tenantid}/v2.0' is invalid"
The url https://login.microsoftonline.com/{tenantid}/v2.0 is in the token in the iss section.
What is wrong?
According to your error and your code, you do not tell your application the ValidIssuer. So you get the error. Please add the following code in the method ConfigureServices of startup.cs file
services.Configure<JwtBearerOptions>(AzureADDefaults.JwtBearerAuthenticationScheme, options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidIssuers = new[] {
},
});
For example
Configure Azure AD for your web API. For more details, please refer to the document
a. Create Azure AD web api application
b. Expose API
c. Configure code
config file
"AzureAd": {
"Instance": "https://login.microsoftonline.com/",
"ClientId": "[Client_id-of-web-api-eg-2ec40e65-ba09-4853-bcde-bcb60029e596]",
"TenantId": "<your tenant id>"
},
Add following code in the Stratup.cs
services.AddAuthentication(AzureADDefaults.BearerAuthenticationScheme)
.AddAzureADBearer(options => Configuration.Bind("AzureAd", options));
services.Configure<JwtBearerOptions>(AzureADDefaults.JwtBearerAuthenticationScheme, options =>
{
options.Authority += "/v2.0";
options.TokenValidationParameters = new TokenValidationParameters
{
/**
* with the single-tenant application, you can configure your issuers
* with the multiple-tenant application, please set ValidateIssuer as false to disable issuer validation
*/
ValidIssuers = new[] {
$"https://sts.windows.net/{Configuration["AzureAD:TenantId"]}/",
$"https://login.microsoftonline.com/{Configuration["AzureAD:TenantId"]}/v2.0"
},
ValidAudiences = new[]
{
options.Audience,
$"api://{options.Audience}"
}
};
});
Configure swagger. For more details, please refer to the blog.
a. Create Azure Web application
b. Configure API permissions. Regarding how to configure, you can refer to the document
c. code
Install SDK
<PackageReference Include="Swashbuckle.AspNetCore" Version="5.5.1" />
config file
"Swagger": {
"ClientId": ""
},
Add the following code to Startup.cs in the ConfigureServices method:
services.AddSwaggerGen(o =>
{
// Setup our document's basic info
o.SwaggerDoc("v1", new OpenApiInfo
{
Title = "Protected Api",
Version = "1.0"
});
// Define that the API requires OAuth 2 tokens
o.AddSecurityDefinition("oauth2", new OpenApiSecurityScheme
{
Type = SecuritySchemeType.OAuth2,
Flows = new OpenApiOAuthFlows
{
Implicit = new OpenApiOAuthFlow
{
Scopes = new Dictionary<string, string>
{
{ "api://872ebcec-c24a-4399-835a-201cdaf7d68b/user_impersonation","allow user to access api"}
},
AuthorizationUrl = new Uri($"https://login.microsoftonline.com/{Configuration["AzureAD:TenantId"]}/oauth2/v2.0/authorize"),
TokenUrl = new Uri($"https://login.microsoftonline.com/{Configuration["AzureAD:TenantId"]}/oauth2/v2.0/token")
}
}
});
o.AddSecurityRequirement(new OpenApiSecurityRequirement{
{
new OpenApiSecurityScheme{
Reference = new OpenApiReference{
Id = "oauth2",
Type = ReferenceType.SecurityScheme
}
},new List<string>()
}
});
});
Add the following code to the Configure method:
app.UseSwagger();
app.UseSwaggerUI(c =>
{
c.OAuthClientId(Configuration["Swagger:ClientId"]);
c.OAuthScopeSeparator(" ");
c.OAuthAppName("Protected Api");
c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1");
});
Test

Resources