Typescript client get JWT

I am having a few issues with the typescript client and Identity auth.

  1. When making request inside nextjs getStaticProps() the authentication request successeds but subsequent request will receive a 401. Same code in a server component in dev mode works fine.
export async function generateStaticParams() {
  
  let client = JsonApiClient.create("https://localhost:5001");
  client.credentials = "include";
  let req = new Authenticate();
  req.provider = "credentials";
  req.userName = "user";
  req.password = "pass";
  const response = await client.post(req);
  console.log("AUTH", response, client.cookies);  //authenticates successfully and cookie present
  let queryCompanies = new QueryCompanies();
  const data = await client.get(queryCompanies);  //401 when running in build with static export option but fine in dev mode

  return [data];
}
  1. Can I get the bearer token in the response of an Authenticate request? I have tried posting to https://localhost:5001/session-to-token to get a token to test with but it returns an empty object (cookie is present and can hit authenticated endpoint in postman). Ideally I want someone to login and get the token as a response.

relevant configs from my project (ServiceStack 8.2.3):

//plugin
services.AddPlugin(new AuthFeature(IdentityAuth.For<ApplicationUser>(options =>
{
    options.SessionFactory = () => new CustomUserSession();
    options.CredentialsAuth();
    options.JwtAuth(x =>
    {
        x.IncludeConvertSessionToTokenService = true;
    });
}

//program.cs
services.AddAuthentication()
    .AddJwtBearer(options => {
        options.TokenValidationParameters = new()
        {
            ValidIssuer = config["JwtBearer:ValidIssuer"],
            ValidAudience = config["JwtBearer:ValidAudience"],
            IssuerSigningKey = new SymmetricSecurityKey(
                Encoding.UTF8.GetBytes(config["JwtBearer:IssuerSigningKey"])),
            ValidateIssuerSigningKey = true,
        };
    })
services.ConfigureApplicationCookie(options =>
{
    // Cookie settings
    options.Cookie.HttpOnly = true;
    options.ExpireTimeSpan = TimeSpan.FromDays(150);
    options.SlidingExpiration = true;
    options.Cookie.SameSite = SameSiteMode.None;
    options.DisableRedirectsForApis();
});

JWTs are returned in Secure HttpOnly Token Cookies in order for them to not be accessible in JavaScript and susceptible to XSS attacks. For the same reason there’s no built-in APIs that clients can call to return a JWT.

If you’re not running in a browser you can check to see if you have access to the JWT in the ss-tok cookie.

If you want to make your own APIs available that returns JWTs you can easily create them from:

var jwtProvider = services.GetRequiredService<IIdentityJwtAuthProvider>();
var jwt = jwtProvider.CreateJwtBearerToken(...);

I don’t quite get what is going on.

If I use fetch inside build:

  const response = await fetch("https://localhost:5001/api/Authenticate", {
    method: "POST", 
    mode: "cors", 
    cache: "no-cache", 
    credentials: "include", 
    headers: {
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
        provider: "credentials",
        userName: "user",
        password: "pass",
    }),
  });

Then I get all cookies including the ss-tok.

If I try to use servicestack client in same way:

  let client = JsonApiClient.create("https://localhost:5001");
 client.credentials = "include";
 client.mode = "cors";
 client.useTokenCookie = true;
 let req = new Authenticate();
 req.provider = "credentials";
 req.userName = "user";
 req.password = "pass";
 const response2 = await client.post(req);

Then it only returns these cookies:

  '.AspNetCore.Identity.Application': {
    name: '.AspNetCore.Identity.Application',
    value: '',
    path: '/',
    expires: Invalid Date
  },
  '30 Apr 2024 05:56:57 GMT': {
    name: '30 Apr 2024 05:56:57 GMT',
    value: null,
    path: '/',
    secure: true,
    samesite: 'none',
    httpOnly: true
  }

And the subsequent query request will fail with 401.

What is bizarre is that if I do a fetch first and then an authenticate with ss client after the ss client will actually get the ss-tok cookie.

This works:

const response = await fetch("https://localhost:5001/api/Authenticate", {
	method: "POST", 
	mode: "cors",
	cache: "no-cache", 
	credentials: "include", 
	headers: {
	  "Content-Type": "application/json",
	},
	body: JSON.stringify({
		provider: "credentials",
		userName: "user",
		password: "pass",
	}),
});

  let client = JsonApiClient.create("https://localhost:5001");
  client.credentials = "include";
  client.mode = "cors";
  client.useTokenCookie = true;
  let req = new Authenticate();
  req.provider = "credentials";
  req.userName = "user";
  req.password = "pass";
  const response2 = await client.post(req);

  let queryCompanies = new QueryCompanies();
  const data = await client.get(queryCompanies);
  console.log(data); //works if a fetch auth was done first

I don’t understand why the servicestack client isn’t getting the ss-tok cookie without a fetch being done first.

If your node fetch supports cookies you can try disable the client from managing them with:

client.manageCookies = false

This will stop client from trying to manage them, you can then use responseFilter to inspect the fetch response.

  let client = JsonApiClient.create("https://localhost:5001");
  client.credentials = "include";
  client.mode = "cors";
  client.manageCookies = false;
  client.responseFilter = (res) => {
    console.log("COOKIE", res.headers.get("set-cookie"));
    return res;
  }
  let req = new Authenticate();
  req.provider = "credentials";
  req.userName = "user";
  req.password = "pass";
  const response2 = await client.api(req);

It still doesn’t return the ss-tok cookie

Typically you wouldn’t be able to access HttpOnly cookies from JavaScript but if you’re saying that in your node fetch implementation you can, the JsonServiceClient should be using the same fetch implementation.

You can inspect/change the fetch request that’s made in the requestFilter or JsonServiceClient.globalRequestFilter if there’s difference in fetch behavior than I’m assuming the request is different. Here’s the source code for the JsonServiceClient to see how it works.


Note you don’t need to use JsonApiClient as the latest version uses /api by default, so you can use the default constructor:

const client = new JsonServiceClient(baseUrl)
1 Like

So there were a few things going on.

NextJs caches fetch requests, so when I used fetch first it would cache the successful request and give the response to the servicestack client. It was giving me intermittent results which is why I was getting confused.

I inspected requests on the API after disabling the NextJs cache and the requests were identical apart from the cache control header so I added that header with the request filter and then the ss-tok gets returned (although it’s impossible to observe this on the api global response filter as it appears to get added later in pipeline).

So this is the working code:

const client = new JsonServiceClient("https://localhost:5001");
client.credentials = "include";
client.mode = "no-cors";
client.manageCookies = true; //needs to be explicitly set to true or doesn't send cookies
client.requestFilter = (req: IRequestInit) => {
	req.next = {revalidate: 0};
	return req;
}

const req = new Authenticate();
req.provider = "credentials";
req.userName = "user";
req.password = "pass";
const response = await client.api(req);

const queryCompanies = new QueryCompanies();
const data = await client.api(queryCompanies);

console.log(data);

Thanks Mythz, I got there in the end :slight_smile:

1 Like