Blazor WASM - PWA - Offline behaviour with server-side requests - asp.net-core

I've created a new project using the default project template for a Blazor Webassembly app, with identity, asp.net core hosted, and PWA options checked. This gives us a WASM blazor SPA app with some basic offline PWA ability, and a server-side asp.net core app.
When authenticating with identity, the pages are served by the Server app. As such, they can't be reached when the app is running offline. This is to be expected, but the user experience (out of the box) in this scenario is poor with default 404, or "you have no internet" pages rendered by the browser.
I'd like to clean this up so the user is safely/gracefully warned about the failure to GET the remote pages. I started to look at the service-worker.js provided by the template and it already contains code to deal with "connect" and "identity" urls and ensure they are fetched from the server-side. I've attempted to add to this by using the approach shown here - https://googlechrome.github.io/samples/service-worker/custom-offline-page/
async function onFetch(event) {
let cachedResponse = null;
var shouldServeIndexHtml = true;
console.info('Service worker: onFetch');
if (event.request.method === 'GET') {
// For all navigation requests, try to serve index.html from cache
// If you need some URLs to be server-rendered, edit the following check to exclude those URLs
shouldServeIndexHtml = event.request.mode === 'navigate'
&& !event.request.url.includes('/connect/')
&& !event.request.url.includes('/Identity/');
const request = shouldServeIndexHtml ? 'index.html' : event.request;
const cache = await caches.open(cacheName);
cachedResponse = await cache.match(request);
}
console.info('Service worker event.request.url: ' + event.request.url);
if (cachedResponse != null) {
console.info('Service worker we have cachedResponse: ' + cachedResponse.url);
return cachedResponse
} else {
console.info('Service worker cachedResponse is null, starting fetch ');
try {
const networkResponse = await fetch(event.request);
return networkResponse;
} catch (error) {
// catch is only triggered if an exception is thrown, which is likely
// due to a network error.
// If fetch() returns a valid HTTP response with a response code in
// the 4xx or 5xx range, the catch() will NOT be called.
console.log('Fetch failed; returning offline page instead.', error);
console.log('Fetch failed shouldServeIndexHtml ',shouldServeIndexHtml);
if (shouldServeIndexHtml == false) {
console.log('Fetch failed, attempting fallback');
const request = 'index.html';
const cache = await caches.open(cacheName);
cachedResponse = await cache.match(request);
if (cachedResponse != null) {
console.info('Service worker we have fallback cachedResponse: ' + cachedResponse.url);
return cachedResponse
} else {
console.info('Service worker fallback cachedResponse is null');
}
}
}
}
}
Code is rough at the moment, apologies!
This almost works. If a GET is attempted to a server page (identified with shouldServeIndexHtml = false), it now attempts the fetch in a try/catch. The catch then does fire, and I try to route to "index.html", but here is where it all falls down. This ends with the Blazor client-side Router going to the NotFound layout.
This makes me think that this isn't the best approach to this, but has anyone else attempted to sort this out using the service worker, or via another method?

Related

How to use OAuth Authorization Code for CLIs

Trying to allow a CLI I'm developing to "login" via web browser and obtain an access token for the user's account, similar to how gcloud and github's CLIs do it. I realize it'll be using the OAuth Authorization Code flow.
But what about the client_secret?
I've found out that github cli just doesn't care about exposing it, and it's right there in the source code: https://github.com/cli/cli/blob/6a8deb1f5a9f2aa0ace2eb154523f3b9f23a05ae/internal/authflow/flow.go#L25-L26
Why is this not a problem? or is it?
I'm not yet using OAuth for the cli's login
STANDARDS
The CLI app is a native public client and should use authorization code flow + PKCE rather than a fixed client secret. It should also follow the flow from
RFC8252 and receive the browser response using a local HTTP (loopback) URI.
THIS IMPLEMENTATION
Looks like the github code here uses a client secret and does not use PKCE. You may have to provide a client secret if using this library, but it cannot be kept secret from users. Any user could easily view it, eg with an HTTP proxy tool.
CODE
If the infrastructure enables you to follow the standards, aim for something similar to this Node.js code.
* The OAuth flow for a console app
*/
export async function login(): Promise<string> {
// Set up the authorization request
const codeVerifier = generateRandomString();
const codeChallenge = generateHash(codeVerifier);
const state = generateRandomString();
const authorizationUrl = buildAuthorizationUrl(state, codeChallenge);
return new Promise<string>((resolve, reject) => {
let server: Http.Server | null = null;
const callback = async (request: Http.IncomingMessage, response: Http.ServerResponse) => {
if (server != null) {
// Complete the incoming HTTP request when a login response is received
response.write('Login completed for the console client ...');
response.end();
server.close();
server = null;
try {
// Swap the code for tokens
const accessToken = await redeemCodeForAccessToken(request.url!, state, codeVerifier);
resolve(accessToken);
} catch (e: any) {
reject(e);
}
}
}
// Start an HTTP server and listen for the authorization response on a loopback URL, according to RFC8252
server = Http.createServer(callback);
server.listen(loopbackPort);
// Open the system browser to begin authentication
Opener(authorizationUrl);
});
}
/*
* Build a code flow URL for a native console app
*/
function buildAuthorizationUrl(state: string, codeChallenge: string): string {
let url = authorizationEndpoint;
url += `?client_id=${encodeURIComponent(clientId)}`;
url += `&redirect_uri=${encodeURIComponent(redirectUri)}`;
url += '&response_type=code';
url += `&scope=${scope}`;
url += `&state=${encodeURIComponent(state)}`;
url += `&code_challenge=${encodeURIComponent(codeChallenge)}`;
url += '&code_challenge_method=S256';
return url;
}
/*
* Swap the code for tokens using PKCE and return the access token
*/
async function redeemCodeForAccessToken(responseUrl: string, requestState: string, codeVerifier: string): Promise<string> {
const [code, responseState] = getLoginResult(responseUrl);
if (responseState !== requestState) {
throw new Error('An invalid authorization response state was received');
}
let body = 'grant_type=authorization_code';
body += `&client_id=${encodeURIComponent(clientId)}`;
body += `&redirect_uri=${encodeURIComponent(redirectUri)}`;
body += `&code=${encodeURIComponent(code)}`;
body += `&code_verifier=${encodeURIComponent(codeVerifier)}`;
const options = {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body,
};
const response = await fetch(tokenEndpoint, options);
if (response.status >= 400) {
const details = await response.text();
throw new Error(`Problem encountered redeeming the code for tokens: ${response.status}, ${details}`);
}
const data = await response.json();
return data.access_token;
}

How to redirect from GraphQL middleware resolver on authentication fail?

Introduction:
I am using GraphQL Mesh as a gateway between my app and an API. I use Apollo Client as the GraphQL client. When a user wants to visit the first screen after hitting the log-in button, I do a query to load data from a CMS. This query has to go through the gateway. In the gateway I do an auth check to see if the user has a valid JTW access token, if not, I want to redirect back to the sign-in page. If the user has a token, he is let through.
The gateway is-auth.ts resolver:
const header = context.headers.authorization;
if (typeof header === "undefined") {
return new Error("Unauthorized: no access token found.");
} else {
const token = header.split(" ")[1];
if (token) {
try {
const user = jwt.verify(token, process.env.JWT_SECRET as string);
} catch (error) {
return new Error("Unauthorized: " + error);
}
} else {
return new Error("Unauthorized: no access token found.");
}
}
return next(root, args, context, info);
},
Problem: Right now, I am returning Errors in the authentication resolver of the gateway, hoping that I could pick them up in the error object that is sent to Apollo Client and then redirect off of that. Unfortunately, I don't get that option, since the Errors are thrown immediately, resulting in an error screen for the user (not what I want). I was hoping this would work in order to redirect to the sign-in from the client-side, but it does not work:
const { data, error } = await apolloClient(accessToken).query({
query: gql`
query {
...where my query is.
}
`,
});
if (error) {
return {
redirect: {
permanent: false,
destination: `/sign-in`,
},
};
}
Does anyone perhaps have a solution to this problem?
This is the GraphQL Mesh documentation on the auth resolver, for anyone that wants to see it: https://www.graphql-mesh.com/docs/transforms/resolvers-composition. Unfortunately, it doesn't say anything about redirects.
Kind regards.

Why is Axios sending an extra 204 preflight request with my POST request?

Whenever I send a POST request using Vue.js (3.x), an additional request to the same URL is being made with HTTP status code 204 & type of "preflight".
What is this preflight request and how can I fix it so it isn't sent as a duplicate?
Register.vue
async submit() {
this.button = true;
try {
const response = await axios.post(`register`, this.form);
if(response.data.success == false)
{
console.log(response.data.message);
}
else
{
this.$router.push('/');
}
}
catch (error)
{
let { errors } = error.response.data;
this.button = false;
this.errors = {};
Object.keys(errors).forEach(element => {
this.errors[element] = errors[element][0];
});
}
},
This is not an issue and is controlled by the browser by design.
It is not something Axios or any other HTTP client decides to send.
A preflight request is a CORS OPTIONS request & are automatically sent by browsers specifically to check if the server would support the call you are trying to make in terms of method, headers and origin.
You can safely ignore the requests if they do not fail as that means that the server will not be rejecting your request on the basis of the aforementioned factors.
Your issue relates to the endpoint not existing as you are getting a 404 Not Found error - check to see if the endpoint exists or if you are calling it correctly.

Middleware writes response when starting up the project

Using ASP.Net Core, C#
I have a middleware where i check for particular cookie is present otherwise returning a 400 response. My problem is the middleware fires up starting the project itself and check the cookie is present or not and then shows the response text in the swagger index page, which i dont want.the middleware fies when swagger loads. I want this condition to be executed only for the requests.
public async Task InvokeAsync(HttpContext context)
{
var pl = context.Request.Cookies["pl"];
var sig = context.Request.Cookies["sig"];
if (string.IsNullOrWhiteSpace(pl) || string.IsNullOrWhiteSpace(sig))
{
context.Response.StatusCode = StatusCodes.Status400BadRequest;
context.Response.ContentType = "application/json";
await context.Response.WriteAsync("Invalid Data");
return;
}
// If success i process and do something
// Call the next delegate/middleware in the pipeline
await _next(context);
}
I can prevent firing the middleware when swagger loads and fire only for api requests. But is this better approach or any other better are there.
app.UseWhen(context => `context.Request.Path.ToString().Contains("/api"),HandleBranch);`

How to customize the authorization error produced by OpenIddict?

I'm using OpenIddict for auth in a .NET Core 2 API. Client side I'm relying on any API errors to follow a custom scheme. However, when e.g. a refresh token has been outdated, I can't seem to find out how to customize the error sent back.
The /token endpoint is never reached, so the error is not under "my control".
The result of the request is a status code 400, with the following JSON:
{"error":"invalid_grant","error_description":"The specified refresh token is no longer valid."}
I've tried to use a custom middleware to catch all status codes (which it does), but the result is returned before the execution of my custom middleware has completed.
How can I properly customize the error or intercept to change it? Thanks!
You can use OpenIddict's event model to customize the token response payloads before they are written to the response stream. Here's an example:
MyApplyTokenResponseHandler.cs
public class MyApplyTokenResponseHandler : IOpenIddictServerEventHandler<ApplyTokenResponseContext>
{
public ValueTask HandleAsync(ApplyTokenResponseContext context)
{
var response = context.Response;
if (string.Equals(response.Error, OpenIddictConstants.Errors.InvalidGrant, StringComparison.Ordinal) &&
!string.IsNullOrEmpty(response.ErrorDescription))
{
response.ErrorDescription = "Your customized error";
}
return default;
}
}
Startup.cs
services.AddOpenIddict()
.AddCore(options =>
{
// ...
})
.AddServer(options =>
{
// ...
options.AddEventHandler<ApplyTokenResponseContext>(builder =>
builder.UseSingletonHandler<MyApplyTokenResponseHandler>());
})
.AddValidation();
The /token endpoint is never reached, so the error is not under "my control".
In fact ,the /token is reached, and the parameter of grant_type equals refresh_token. But the rejection logic when refresh token expired is not processed by us. It is some kind of "hardcoded" in source code :
if (token == null)
{
context.Reject(
error: OpenIddictConstants.Errors.InvalidGrant,
description: context.Request.IsAuthorizationCodeGrantType() ?
"The specified authorization code is no longer valid." :
"The specified refresh token is no longer valid.");
return;
}
if (options.UseRollingTokens || context.Request.IsAuthorizationCodeGrantType())
{
if (!await TryRedeemTokenAsync(token))
{
context.Reject(
error: OpenIddictConstants.Errors.InvalidGrant,
description: context.Request.IsAuthorizationCodeGrantType() ?
"The specified authorization code is no longer valid." :
"The specified refresh token is no longer valid.");
return;
}
}
The context.Reject here comes from the assembly AspNet.Security.OpenIdConnect.Server.
For more details, see source code on GitHub .
I've tried to use a custom middleware to catch all status codes (which it does), but the result is returned before the execution of my custom middleware has completed.
I've tried and I'm pretty sure we can use a custom middleware to catch all status codes. The key point is to detect the status code after the next() invocation:
app.Use(async(context , next )=>{
// passby all other end points
if(! context.Request.Path.StartsWithSegments("/connect/token")){
await next();
return;
}
// since we might want to detect the Response.Body, I add some stream here .
// if you only want to detect the status code , there's no need to use these streams
Stream originalStream = context.Response.Body;
var hijackedStream = new MemoryStream();
context.Response.Body = hijackedStream;
hijackedStream.Seek(0,SeekOrigin.Begin);
await next();
// if status code not 400 , pass by
if(context.Response.StatusCode != 400){
await CopyStreamToResponseBody(context,hijackedStream,originalStream);
return;
}
// read and custom the stream
hijackedStream.Seek(0,SeekOrigin.Begin);
using (StreamReader sr = new StreamReader(hijackedStream))
{
var raw= sr.ReadToEnd();
if(raw.Contains("The specified refresh token is no longer valid.")){
// custom your own response
context.Response.StatusCode = 401;
// ...
//context.Response.Body = ... /
}else{
await CopyStreamToResponseBody(context,hijackedStream,originalStream);
}
}
});
// helper to make the copy easy
private async Task CopyStreamToResponseBody(HttpContext context,Stream newStream, Stream originalStream){
newStream.Seek(0,SeekOrigin.Begin);
await newStream.CopyToAsync(originalStream);
context.Response.ContentLength =originalStream.Length;
context.Response.Body = originalStream;
}