Create Blazor Server Login Page to fetch Jwt - asp.net-core

We have a Blazor server app which is packaged as an electron desktop app.
Identity cannot reside alongside the app it must be remote so I decided to use Jwt. I have created a Jwt server & have JwtBearer middleware enabled in the Blazor server app startup.cs (see code).
At present we can attach a Jwt to context.Token manually in 'JwtBearerEvents --> OnMessageReceived' and middleware picks this up so we can use [Authorize] & also include Policies [Authorize(Policy="account_write")] this works great.
The Jwt is created manually in Postman (calling our Jwt server and the pasting in the result - see code) as I cannot figure out how to have a login page that uses the Un/Pw to fetch a Jwt from the server.
I appreciate HTTP is only availabe for a few requests before SignalR kicks in, and that is the crux of the problem but im hoping there is a way around this.
I thought I could use a combination of Blazor and razor pages, ie login page is cshtml and saves token to LocalStorage or Cookie then we start blazor (Javascript autostart off for the blazor.server.js) & read the cookie - I have not been able to get this to work as I cannot get razor and cshtml working together.
Any ideas how can I create a login page to fetch the Jwt & attach it to the context?
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = false,
ValidateAudience = false,
ValidateLifetime = true,
RequireExpirationTime = true,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(key),
};
options.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
// Is token in Querystring?
var accessToken = context.Request.Query["access_token"];
Debug.WriteLine($"JwtBearer - OnMessageReceived - access_token in QS: {ShorterJWT(accessToken)}");
//
// Attach the token manually
//
// Jwt fetched manually using Postman - 10 year
accessToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjNjMzIxMjI2LTFhNGYtNGQ3NS1iZjM1LTIwYjQ1NmVlYmYzOSIsImJvb2tlciI6InRydWUiLCJuYW1lIjoiQm9iIENyYXRjaGV0IiwibmJmIjoxNjI5NzM3MTk4LCJleHAiOjE5NDUyNjk5OTgsImlhdCI6MTYyOTczNzE5OH0.N9HUFM3zIl9BkoPJlPj_WQIUhhQ2Rk53ymEEm42LTD4";
context.Token = accessToken;
return Task.CompletedTask;
}
};
});````

Related

ASP.NET Core Refresh Token Logic still calling /signin-oidc endpoint

Okay, so I am working on creating an OIDC client that will also handle refresh tokens. I have made some progress, but have some questions.
Here is my ConfigureServices
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie(options =>
{
options.LoginPath = "/Login/Index";
options.Events.OnValidatePrincipal = async context => await OnValidatePrincipalAsync(context);
})
.AddOpenIdConnect(options =>
{
options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.Authority = Configuration["auth:oidc:authority"];
options.ClientId = Configuration["auth:oidc:clientid"];
options.ClientSecret = Configuration["auth:oidc:clientsecret"];
options.ResponseType = OpenIdConnectResponseType.Code;
options.GetClaimsFromUserInfoEndpoint = true;
options.SaveTokens = true;
options.UseTokenLifetime = true;
options.SignedOutRedirectUri = "https://contoso.com";
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidIssuer = Configuration["auth:oidc:authority"],
ValidAudience = Configuration["auth:oidc:clientid"],
ValidateIssuerSigningKey = true,
ClockSkew = TimeSpan.FromSeconds(3)
};
});
services.AddAccessTokenManagement();
services.Configure<OidcOptions>(Configuration.GetSection("oidc"));
}
Here is my OnValidatePrincipalAsync(context)
private async Task OnValidatePrincipalAsync(CookieValidatePrincipalContext context)
{
const string AccessTokenName = "access_token";
const string RefreshTokenName = "refresh_token";
const string ExpirationTokenName = "expires_at";
if (context.Principal.Identity.IsAuthenticated)
{
var exp = context.Properties.GetTokenValue(ExpirationTokenName);
var expires = DateTime.Parse(exp, CultureInfo.InvariantCulture).ToUniversalTime();
if (expires < DateTime.UtcNow)
{
// If we don't have the refresh token, then check if this client has set the
// "AllowOfflineAccess" property set in Identity Server and if we have requested
// the "OpenIdConnectScope.OfflineAccess" scope when requesting an access token.
var refreshToken = context.Properties.GetTokenValue(RefreshTokenName);
if (refreshToken == null)
{
context.RejectPrincipal();
return;
}
var cancellationToken = context.HttpContext.RequestAborted;
// Obtain the OpenIdConnect options that have been registered with the
// "AddOpenIdConnect" call. Make sure we get the same scheme that has
// been passed to the "AddOpenIdConnect" call.
//
// TODO: Cache the token client options
// The OpenId Connect configuration will not change, unless there has
// been a change to the client's settings. In that case, it is a good
// idea not to refresh and make sure the user does re-authenticate.
var serviceProvider = context.HttpContext.RequestServices;
var openIdConnectOptions = serviceProvider.GetRequiredService<IOptionsSnapshot<OpenIdConnectOptions>>().Get("OpenIdConnect");
openIdConnectOptions.Scope.Clear();
openIdConnectOptions.Scope.Add("email");
openIdConnectOptions.Scope.Add("profile");
openIdConnectOptions.Scope.Add("offline_access");
var configuration = openIdConnectOptions.Configuration ?? await openIdConnectOptions.ConfigurationManager.GetConfigurationAsync(cancellationToken).ConfigureAwait(false);
// Set the proper token client options
var tokenClientOptions = new TokenClientOptions
{
Address = configuration.TokenEndpoint,
ClientId = openIdConnectOptions.ClientId,
ClientSecret = openIdConnectOptions.ClientSecret,
};
var httpClientFactory = serviceProvider.GetService<IHttpClientFactory>();
using var httpClient = httpClientFactory.CreateClient();
var tokenClient = new TokenClient(httpClient, tokenClientOptions);
var tokenResponse = await tokenClient.RequestRefreshTokenAsync(refreshToken, cancellationToken: cancellationToken).ConfigureAwait(false);
if (tokenResponse.IsError)
{
context.RejectPrincipal();
return;
}
// Update the tokens
var expirationValue = DateTime.UtcNow.AddSeconds(tokenResponse.ExpiresIn).ToString("o", CultureInfo.InvariantCulture);
context.Properties.StoreTokens(new[]
{
new AuthenticationToken { Name = RefreshTokenName, Value = tokenResponse.RefreshToken },
new AuthenticationToken { Name = AccessTokenName, Value = tokenResponse.AccessToken },
new AuthenticationToken { Name = ExpirationTokenName, Value = expirationValue }
});
// Update the cookie with the new tokens
context.ShouldRenew = true;
}
}
}
I've done some experimenting which includes not using the Configuration to get the OpenIdConnectOptions in my OnValidatePrincipal and just create a new OpenIdConnectOptions object , and I still have not been able to understand my issue.
Here are my Current Issues
First Issue
I seem to be able to successfully send a request to the token endpoint after my desired period of time (every 2 minutes and five seconds). I notice that my client application is making a request to the ?authorize endpoint of my authorization server, even though I don't believe I have it configured to do so in my OnValidatePrincipalContext fucntion. I created an all new OpenIdConnectOptions object because I thought the current configuration was triggering it.
First Question
When is this signin-oidc request triggered? I think that's what's triggering the request to my authN server's authorize endpoint. I should not have to query this endpoint if I'm doing silent refresh?
Second Issue
My authorization server is picking up the openid scope when my client makes this request:
POST https://<authorization-server>/oauth/oidc/token HTTP/1.1
Accept: application/json
Content-Type: application/x-www-form-urlencoded
grant_type=refresh_token&refresh_token=<refresh-token>&client_id=<client-id>&client_secret=<client-secret>
But, in my OnValidatePrincipalContext function I explicitly remove the openid scope by calling
openIdConnectOptions.Scope.Clear();
openIdConnectOptions.Scope.Add("email");
openIdConnectOptions.Scope.Add("profile");
openIdConnectOptions.Scope.Add("offline_access");
Second Question
How do I properly handle the Oidc configuration middleware so that when I go to request a new refresh token the correct request is built and sent to my authN server? Am I doint the wrong kind of authentication scheme (i.e cookie vs bearer)? If I am, how can I tell?
Thank you.
When is this signin-oidc request triggered?
Its triggered by the authorization server when the user have successfully authenticated and given consent to the requested scopes. It will ask the browser to post the authorization code to this endpoint. Its typically performed done by using a self-submitting HTML form that will create a post request to this endpoint.
You should always ask for the openid scope, otherwise it won't work.
A picture showing the flow for the endpoint is:
For the second question one alternative is to take a look at the IdentityModel.AspNetCore library. This library can automatically handle the automatic renewal of the access token using the refresh token.
See also this blog post

ASP NET CORE Identity and Checktoken URL

Well, I'm trying to use ASP NET CORE 2.1 with OAuth2 to authenticate in a IdP (Identity Provider), so I have the following:
services.AddAuthentication()
.AddJwtBearer(options =>
{
// The API resource scope issued in authorization server
options.Audience = "resource.server.api";
// URL of my authorization server
options.Authority = "https://myidp.com.br";
});
// Making JWT authentication scheme the default
services.AddAuthorization(options =>
{
options.DefaultPolicy = new AuthorizationPolicyBuilder(JwtBearerDefaults.AuthenticationScheme)
.RequireAuthenticatedUser()
.Build();
});
When I try to call my API thought POSTMAN, I got following:
System.InvalidOperationException: IDX20803: Unable to obtain configuration from: 'https://myidp.com.br/.well-known/openid-configuration'.
Well, I don't have well-known URL in my IdP and I can't add it in this moment of project. Is there other way to configure URLs manually without well-known ?
Another important thing: We have a URL https://myidp.com.br/oauth/tokeninfo that check if JWT TOKEN is valid or not.
I assume you are using the Asymmetric Keys . Usually, the public key information is automatically retrieved from the discovery document. If you need to specify it manually, you’ll need to get the key parameters and create a SecurityKey object . You can refer to belwo links for code samples :
https://github.com/IdentityServer/IdentityServer4/blob/master/samples/Clients/src/MvcManual/Controllers/HomeController.cs#L148
Verifying JWT signed with the RS256 algorithm using public key in C#
You can also write the custom JwtSecurityTokenHandler in the System.IdentityModel.Tokens.Jwt package , and override the ValidateToken event to implement the custom validation logic .
You can also not using the AddJwtBearer middleware , the code sample is same as above , create your keys and apply to the validation .
Normally , the noraml process of validating token is :
Decode token
Validate claims(issuer,audience,expire time...)
Validate signature
Creating user principal and sign in user
Updated :
You can also add your own signature validation to the TokenValidationParameters :
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(options =>
{
options.RequireHttpsMetadata = false;
options.SaveToken = true;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = false,
ValidateAudience = false,
SignatureValidator =
delegate (string token, TokenValidationParameters parameters)
{
var jwt = new JwtSecurityToken(token);
var httpClient = new HttpClient();
var requestData = new HttpRequestMessage
{
Method = HttpMethod.Get,
RequestUri = new Uri("xxxxxx"),
};
//pass toekn to your endpoint and check result
if (false)
{
throw new Exception("Token signature validation failed.");
}
return jwt;
}
};
});

aspnet core 2.2 External Authentication

Created an Authentication Api to handle the auth for several apps. This is a basic auth. username and pw. No OAuth with Google etc. The api gets called with the credentials and it responds with an AthenticationResult. It works correctly except on AuthenticationResult.Success. As I learned I cannot serialize the ClaimsPrincipal. As I am reading it seems the answer it to convert to a token. Is this correct? The AuthenticationResult.Failed serializes w/o issue. What is the best solution here. I will continue to look.
thx for reading
General Steps
That's correct, you'll need to complete the following steps:
Return a token from your authentication API.
Configure your application for JWT Bearer authentication.
Include that token as part of an authorize header on every request to the server.
Require authentication/authorization in your controllers.
There is an excellent ASP.NET Core 2.2 JWT Authentication Tutorial you should check out.
There's too much code involved to post all of it in it's entirety, but here are some key snippets (some code slightly modified for greater clarity out of context from the tutorial):
Some Key Code Snippets
Creating the token
var tokenHandler = new JwtSecurityTokenHandler();
var key = Encoding.ASCII.GetBytes(_appSettings.Secret);
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(new Claim[]
{
// 'user' is the model for the authenticated user
// also note that you can include many claims here
// but keep in mind that if the token causes the
// request headers to be too large, some servers
// such as IIS may reject the request.
new Claim(ClaimTypes.Name, user.Id.ToString())
}),
Expires = DateTime.UtcNow.AddDays(7),
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)
};
var token = tokenHandler.CreateToken(tokenDescriptor);
return tokenHandler.WriteToken(token);
Configuring JWT Authentication (in Startup.cs ConfigureServices method)
var appSettings = appSettingsSection.Get<AppSettings>();
var key = Encoding.ASCII.GetBytes(appSettings.Secret);
services.AddAuthentication(x =>
{
x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(x =>
{
x.RequireHttpsMetadata = false;
x.SaveToken = true;
x.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(key),
ValidateIssuer = false,
ValidateAudience = false
};
});
Don't forget to configure the app to actually use authentication in Startup.cs Configure method:
app.UseAuthentication();

Validate access token if sent by request headers on [AllowAnonymous] method in ASP.NET Core with OpenIddict

I am using OpenIddict for JWT authentication in my ASP.NET Core 2.0 app.
I have a method with [AllowAnonymous] attribute. User is Authenticated on client (access token is sent), but access token is invalid (for some reason which is not important right now) so
contextAccessor?.HttpContext?.User?.FindFirst(ClaimTypes.NameIdentifier)
returns null.
Problem is that I return a different set of data if user is authenticated or anonymous. Server thinks user is anonymous, and client thinks user is logged in.
I would like to return http error code (not 200) if request has Authorization: Bearer eyJhb.... request header, but on server User is null. How?
This already works if method has Authorize attribute (it returns 403), but not on AllowAnonymous Controller method.
I think I need something like AutomaticAuthenticate that exist in ASP.NET Core 1. I would like to return 401 if Context.Token is set, but user does not exist.
This is my setup:
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(o =>
{
o.Authority = this.Configuration["Authentication:OpenIddict:Authority"];
o.Audience = "Web"; //Also in Auhorization.cs controller.
o.RequireHttpsMetadata = !this.Environment.IsDevelopment();
if (this.Environment.IsDevelopment())
{
//This ensures that access token is valid, if application is restarted. See also AddOpenIddict in this file.
o.TokenValidationParameters = new TokenValidationParameters()
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes("ThisIsSecretKeyOnlyInDevelopmentSoItIsSafe")),
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
RequireExpirationTime = true
};
}
o.Events = new JwtBearerEvents()
{
OnMessageReceived = context =>
{
if (context.Request.Path.ToString().StartsWith("/HUB/"))
context.Token = context.Request.Query["access_token"];
else
{
if (context.Request.Query.ContainsKey("access_token")) //We can download file with acces_token in url
context.Token = context.Request.Query["access_token"];
}
return Task.CompletedTask;
},
//It would be nice to report error to user with custom status code, but this is not possible: https://stackoverflow.com/questions/48649717/addjwtbearer-onauthenticationfailed-return-custom-error?noredirect=1#comment84308248_48649717
OnAuthenticationFailed = context =>
{
this._logger.LogInformation(Log.OpPIS, "JWT authentication failed.");
if (this.Environment.IsDevelopment())
Debug.Write(context.Exception.Message);
return Task.CompletedTask;
}
};
});

How to redirect to log in page on 401 using JWT authorization in ASP.NET Core

I have this JWT authorization configuration in my Startup.cs:
services.AddAuthentication(opts =>
{
opts.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
opts.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
opts.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(opts =>
{
opts.RequireHttpsMetadata = false;
opts.SaveToken = true;
opts.TokenValidationParameters = new TokenValidationParameters()
{
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("my_secret_key")),
ValidIssuer = "iss",
ValidAudience = "aud",
ValidateIssuerSigningKey = true,
ValidateLifetime = true
};
});
My HomeController has [Authorize] attribute. So upon access to Home/Index, I get a 401 response and I am presented with a blank page. I want to redirect to my Account/LogIn page but I am not sure how to do it.
I read that this shouldn't automatically redirect because it won't make sense to API calls if they are not authorized and then you redirect them, so what is the proper way on how I would get them to the login page on 401.
Please bear in mind that in this project, I have both Web API and Action methods with [Authorize] attributes so I need to redirect only when it is an action method.
You may use StatusCodePages middleware. Add the following inot your Configure method:
app.UseStatusCodePages(async context => {
var request = context.HttpContext.Request;
var response = context.HttpContext.Response;
if (response.StatusCode == (int)HttpStatusCode.Unauthorized)
// you may also check requests path to do this only for specific methods
// && request.Path.Value.StartsWith("/specificPath")
{
response.Redirect("/account/login")
}
});
I read that this shouldn't automatically redirect because it won't make sense to API calls
this relates to API calls, that returns data other than pages. Let's say your app do call to API in the background. Redirect action to login page doesn't help, as app doesn't know how to authenticate itself in background without user involving.
Thanks for your suggestion... after spending a good time on google i could find your post and that worked for me. You raised a very good point because it does not make sense for app API calls.
However, I have a situation where the Actions called from the app has a specific notation route (/api/[Controller]/[Action]) which makes me possible to distinguish if my controller has been called by Browser or App.
app.UseStatusCodePages(async context =>
{
var request = context.HttpContext.Request;
var response = context.HttpContext.Response;
var path = request.Path.Value ?? "";
if (response.StatusCode == (int)HttpStatusCode.Unauthorized && path.StartsWith("/api", StringComparison.InvariantCultureIgnoreCase))
{
response.Redirect("~/Account/Login");
}
});
This works for both Razor Pages and MVC Views as follows: response.Redirect("/Login"); for Razor Pages. response.Redirect("/Home/Login"); for MVC Views. In order for this to work, the Authorize filter has to be added to the Controller. The code block also has to be added between app.UseAuthentication(); and app.UseAuthorization(); methods.
app.UseStatusCodePages(async context =>
{
var response = context.HttpContext.Response;
if (response.StatusCode == (int)HttpStatusCode.Unauthorized ||
response.StatusCode == (int)HttpStatusCode.Forbidden)
response.Redirect("/Home/Login");
});