Get JWT Token net5 upgrade

Good evening,

I was trying to upgrade to net5, with ServiceStack 5.10.2.
Everything seems to be working now, except the authentication.

I am using JwtAuthProvider in conjunction with CredentialsAuthProvider.

These are current responses I get before and after upgrade when I try to request a login:

/auth/credentials?UserName=admin&Password=admin

Before:

{
  "UserId": "1",
  "SessionId": "QYuLgmPy5...",
  "UserName": "admin",
  "DisplayName": "Admin User",
  "BearerToken": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUz... more info...",
  "RefreshToken": "eyJ0eXAiOiJKV1RSIiwiYWxnIjoiS... more info.. ",
  "ProfileUrl": "data:image/svg+xml,%3Csvg .... more info...",
  "Roles": [],
  "Permissions": [],
  "ResponseStatus": {}
}

After:

{
  "UserId": "1",
  "SessionId": "h35KOFDU...",
  "UserName": "admin",
  "DisplayName": "Admin User",
  "ProfileUrl": "data:image/svg+xml,%3Csv.... more info..",
  "Roles": [],
  "Permissions": [],
  "ResponseStatus": {}
}

I am using swagger-ui to perform exact request.

Thanks in advance!

Is this a POST request? GET’s for authentication should be disabled by default. Can you provide the full HTTP Request/Response headers (replace any sensitive info with xxxx) as well as your AuthFeature registration.

Sure, here you go:

AuthFeature registration:

var authProviders = new List<IAuthProvider>
{
    new JwtAuthProvider(AppSettings) {
        RequireSecureConnection = false,
        AllowInQueryString = true
    },
    new CredentialsAuthProvider(),
    new ApiKeyAuthProvider()
    {
        RequireSecureConnection = false,
        SessionCacheDuration = TimeSpan.FromMinutes(30)
    }
};
var authFeature = new AuthFeature(SessionFactory, authProviders.ToArray());
appHost.Plugins.Add(authFeature);

appHost.Plugins.Add(new RegistrationFeature());

Request:

General:
Request URL: http://localhost:5000/auth?provider=credentials&UserName=admin&Password=admin
Request Method: POST
Status Code: 200 OK
Remote Address: [::1]:5000
Referrer Policy: strict-origin-when-cross-origin

Response Headers:
Access-Control-Allow-Headers: Content-Type, Allow, Authorization
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, PATCH, OPTIONS, HEAD
Access-Control-Allow-Origin: *
Content-Type: application/json; charset=utf-8
Date: Thu, 10 Dec 2020 08:12:12 GMT
Server: Kestrel
Set-Cookie: ss-id=pZ5PSQsA37XW3Ifo5VrK; path=/; samesite=lax; httponly
Set-Cookie: ss-pid=zod97GTnT49bFbhxiNt3; expires=Mon, 10 Dec 2040 08:12:12 GMT; path=/; samesite=lax; httponly
Set-Cookie: ss-opt=temp; expires=Mon, 10 Dec 2040 08:12:12 GMT; path=/; samesite=lax; httponly
Set-Cookie: X-UAId=1; expires=Mon, 10 Dec 2040 08:12:12 GMT; path=/; samesite=lax; httponly
Transfer-Encoding: chunked
Vary: Accept
X-Powered-By: ServiceStack/5.102 NetCore/OSX

Request Headers:
Accept: application/json
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9,es;q=0.8
Connection: keep-alive
Content-Length: 2
Content-Type: application/json
Cookie: _ga=GA1.1.1067860742.1605236947; cookie-alert=true; a_session_console_legacy=eyJpZCI6IjVmYWRmOGVjOTE5ZTYiLCJzZWNyZXQiOiJjZTU2OWRjN2YyY2JhYTk3NjRjMTg1NWE1ZGE2NjNjYjUwOGMxYmNlZjlkMWYyNmE3MmFkMGExM2ZlODhiYzQ3YmQyNGI0ZWNkZjA2MWU1Mzk1YTExMmVlNDAyZmMwYjA4OTY4ZmU5MTdiOWJiM2MxODQyNWM2M2FjMGRmYmRlMmYwYjllODI2ZDNhMWQ4YzEzMjk2MjlkZTZhNWFiMDliNDFmOTAxN2VkODFhM2UyZjc3YjQ1MjQ4MDNmY2NmZGQ0NzM0Nzg4MmQ4NTQzYmRmZjBjOTc5ZjU1NjRmYjBhZWZkMGFlZjkyNjA1NjY5MmJkOTczMGEyNGFiMTAzMDZkIn0%3D; PGADMIN_LANGUAGE=en; zaius_js_version=2.21.3; z_idsyncs=; intercom-id-lg2jb7h4=66af4e73-5a2c-4cfb-ab2f-5858ceb88323; intercom-session-lg2jb7h4=; wordpress_test_cookie=WP%20Cookie%20check; wordpress_logged_in_ba744f26b85aba10a73bbf41268edf3c=admin%7C1607554258%7CSdtySzAheT5NfEtbP7lWqGb45XHSXf1wUAiUTT3cxPU%7C0cb00862d0876ae97c4584cde486918fd45b7e9a8a76774f7665e25d3da29764; wp-settings-time-1=1607528425; location={%22versionId%22:1808%2C%22location%22:{%22zipcode%22:%22%22%2C%22latlng%22:{%22lat%22:-23.5505199%2C%22lng%22:-46.63330939999999}}}; _gid=GA1.1.1757550573.1607539385; vuid=012ac9df-7f73-441e-922b-cec2499daaee%7C1607555753992; pga4_session=4808143c-7dd7-4634-897d-c0646a5bf740!nWhCP9GjitHCTIqTCejPIrZMAYo=; ss-opt=temp; X-UAId=1; ss-id=w48rteVuhaQ2N0Ee6045; ss-pid=msFeoidVS7DzACGCr9sb
Host: localhost:5000
Origin: http://localhost:5000
Referer: http://localhost:5000/swagger-ui/
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-origin
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36

Query String Parameters:
provider: credentials
UserName: admin
Password: XXXX

Request Payload:
{}

Also, I am loading my AppSettings this way (where I have my jwt.AuthKeyBase64):

        var builder = new ConfigurationBuilder()
            .SetBasePath(env.ContentRootPath)
            .AddJsonFile("appsettings.Development.json")
            .AddJsonFile("appsettings.json");

        var config = builder.Build();

        app.UseServiceStack(new AppHost
        {

            AppSettings = new MultiAppSettings(new EnvironmentVariableSettings(), new NetCoreAppSettings(config))
        });

I’m not able to repro this, it’s always returning the bearerToken/refreshToken with every successful credentials auth on both http://localhost:5000 and https://localhost:5001:

This is the logic which determines when JWT Tokens are populated:

You should be able to find out what condition is preventing populating tokens with a Dummy Auth Provider with an IAuthResponseFilter that tests the same conditions as the JWT AuthProvider, e.g:

public class DummyAuthProvider : AuthProvider, IAuthResponseFilter
{
    public DummyAuthProvider() => Provider = "dummy";

    public void Execute(AuthFilterContext authContext)
    {
        var jwt = (JwtAuthProvider)AuthenticateService.GetJwtAuthProvider();
        if (authContext.DidAuthenticate && jwt.SetBearerTokenOnAuthenticateResponse && authContext.Session.IsAuthenticated)
        {
            if (!jwt.RequireSecureConnection || authContext.AuthService.Request.IsSecureConnection)
            {
                //... will populate jwt tokens
            }
        }
        //.. wont populate jwt tokens
    }

    public override bool IsAuthorized(IAuthSession session, IAuthTokens tokens, Authenticate request = null) => 
        throw new NotImplementedException();
    public override Task<object> AuthenticateAsync(IServiceBase authService, IAuthSession session, Authenticate request,
        CancellationToken token = default) => throw new NotImplementedException();
    public Task ResultFilterAsync(AuthResultContext authContext, CancellationToken token = default) => Task.CompletedTask;
}

That’s added to your list of AuthProviders:

authProviders.Add(new DummyAuthProvider());

It did not go to line:
//… will populate jwt tokens

because jwt.SetBearerTokenOnAuthenticateResponse is false.

The other big difference a I see in migration is the way the application builds in Program.cs:

Before:

public static void Main(string[] args)
{
    BuildWebHost(args).Run();
}

public static IWebHost BuildWebHost(string[] args) =>
    WebHost.CreateDefaultBuilder(args)
        .UseModularStartup<Startup>()
        .Build();

After:

public static void Main(string[] args)
{
    var host = new WebHostBuilder()
        .UseKestrel()
        .UseContentRoot(Directory.GetCurrentDirectory())
        .UseModularStartup<Startup>()
        .UseUrls(Environment.GetEnvironmentVariable("ASPNETCORE_URLS") ?? "http://localhost:5000/")
        .Build();

    host.Run();
}

What is the difference between these two?
If I use the way it was before (in Program.cs) it still works.
Is there any advantage on using the new approach (with UseKestrel())?

Then it’s due to the custom MultiAppSettings which returns the types default Type value instead of the default missing value. You should be able to override it with:

new JwtAuthProvider {
     SetBearerTokenOnAuthenticateResponse = true
}

I’m assuming the difference was your custom MultiAppSettings change and not related to the library or framework upgrade.

The first is the default recommended ASP .NET Core WebHost builder with all default recommended options whilst the later is building its own host and not using the default options. Similar to the difference between web and selfhost project templates.

Thanks that worked with the BearerToken to be included in response so now I am able to send it on every new request.

But now when I try to post to an endpoint with [Authenticate] attribute, it rejects me with a 401.

Regarding Program.cs, I guess I assumed original project was scaffolded with selfhost, but it seems that it was a web template. So maybe I need to start all over again, by starting with web template instead to see if this solves the issue, unless you have any other thing on mind? I am not sure if there are other differences between web and selfhost templates…

With all integration issues you should inspect the raw HTTP Headers to see if Authentication token is actually being sent and if it is, what the specific Auth error is returned which may help identify where to look.

As for the setup, I’d stick with the defaults unless you have a reason not to use them.

I started again from scratch, this time with web template, but I could’t make it to work.

For now I will stay with Net 3.1 and ServiceStack 5.9.2, it’s working properly at this version.

Just FYI
This was the request I was sending which response was 401 for net5, ServiceStack 5.10.2:

General:
Request URL: http://localhost:5000/SIF/Checkout/1
Request Method: POST
Status Code: 401 Unauthorized
Remote Address: [::1]:5000
Referrer Policy: strict-origin-when-cross-origin

Response Headers:
Access-Control-Allow-Headers: Content-Type, Allow, Authorization
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, PATCH, OPTIONS, HEAD
Access-Control-Allow-Origin: *
Content-Length: 0
Date: Fri, 11 Dec 2020 15:17:02 GMT
Server: Kestrel
Set-Cookie: ss-id=Fc3jdgpHn69Pc7r5WGbm; path=/; samesite=lax; httponly
Set-Cookie: ss-pid=I4M7PV4bHMAQ25pj5pu6; expires=Tue, 11 Dec 2040 15:17:02 GMT; path=/; samesite=lax; httponly
Vary: Accept
WWW-Authenticate: credentials realm="/auth/credentials"
X-Powered-By: ServiceStack/5.102 NetCore/OSX

Request Headers:
Accept: */*
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9,es;q=0.8
Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1Ni... (complete JWT..)
Connection: keep-alive
Content-Length: 1401
Content-Type: application/json
Host: localhost:5000
Origin: http://localhost:4000
Referer: http://localhost:4000/
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-site
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36

Request Payload:
{Id:1}

Nothing stands out except that it’s a CORS request but Authorization is an allowed header so the browser allows sending the Authorization header. Did you try it without MultiAppSettings? i.e. with just:

app.UseServiceStack(new AppHost {
    AppSettings = new NetCoreAppSettings(config),
});

In case it impacted any of the other default configuration values.

If you try again later & still have the issue, put together a small repro and I’ll take a look.

You were right, MultiAppSettings is the issue.

This does not work:

app.UseServiceStack(new AppHost
        {
            AppSettings = new MultiAppSettings(new NetCoreAppSettings(Configuration))
        });

But this does work:

app.UseServiceStack(new AppHost
        {
            AppSettings = new NetCoreAppSettings(Configuration)
        });

Notice that I am not using this anymore:

var builder = new ConfigurationBuilder()
        .SetBasePath(env.ContentRootPath)
        .AddJsonFile("appsettings.Development.json")
        .AddJsonFile("appsettings.json");

    var config = builder.Build();

Now the problem is how can I use MultiAppSettings as I still require reading from Environment Variables…

Can you try with the latest v5.10.3 on MyGet? I’ve made a change 10 days ago that should hopefully resolve this issue.

1 Like

It worked! Thanks a lot!

1 Like

I sent a PR just to change your launch.json to point to net5 folder.

I am facing the same issue in ServiceStack 6.0.2.
bearerToken is not set after credentials authentication.

I managed to bypass this by setting UseTokenCookie = false in JwtAuthProvider configuration.

I tried to narrow it down with DumyAuthProvider approach.

Part that apparently prevents jwt population is “authContext.AuthResponse.BearerToken == null” as bearer token already exists.

FYI this is the expected behavior in v6 which defaults to using Token Cookies by default:

https://docs.servicestack.net/releases/v6#jwt-changes

When using Token Cookies JWTs are only returned in HttpOnly, Secure Cookies which prevents XSS exploits from being able to capture and use JWT Auth Tokens to make authorized requests.

It’s also better for token management where clients don’t need to manually handle tokens when authorizing their HTTP/Service Client & allows the server to transparently refresh JWTs.

1 Like