Skip to content

how it works jwt authentication

Shaylen Reddy edited this page Jun 15, 2024 · 2 revisions

JWT Authentication

How It's Configured

For the Web Backend-For-Frontend, it has the following configuration

var authenticationScheme = "SeelansTyresWebBffAuthenticationScheme";

builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(authenticationScheme, configure =>
    {
        configure.Authority = builder.Configuration["IdentityServer"];
        configure.Audience = "SeelansTyresWebBff";
        configure.TokenValidationParameters.ValidTypes = new[] { "at+jwt" };
        configure.RequireHttpsMetadata = false;
    });

For the Address Service, it's like this

builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(configure =>
    {
        configure.Authority = builder.Configuration["IdentityServer"];
        configure.Audience = "AddressService";
        configure.TokenValidationParameters.ValidTypes = new[] { "at+jwt" };
        configure.RequireHttpsMetadata = false;
    });

The only major difference is the Audience

Token Samples

In order to explain this better, I had to log the three tokens received by the Mvc Frontend from IdentityServer4, as well as an exchanged token

As mentioned in a previous wiki page about Authentication, IdentityServer4 issues three tokens, the id_token, access_token and refresh_token

The purpose of the id_token is to create a claims principal to log the user in, here's that sample

eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYmYiOjE2ODA4MzMyMzAsImV4cCI6MTY4MDgzMzUzMCwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo1MDA1IiwiYXVkIjoic2VlbGFuc3R5cmVzbXZjZnJvbnRlbmQiLCJub25jZSI6IjYzODE2NDMwMDI0NDE5NzExMS5aVEZrTkdVNFpHRXRaREJrT0MwME1tUTNMVGt4T0RFdE9URTBObUpsTnpZME1XWTFOekE0WlRJMU1tWXRZV0ZqWWkwME5USXlMV0kyWVdRdFl6TTJNemN4Tm1JelpHTTIiLCJpYXQiOjE2ODA4MzMyMzAsImF0X2hhc2giOiJDSVVMcHJBRVZwUHRfekgyVk9aLTJRIiwic19oYXNoIjoiY1Vna2U4aDRIVHZ5Q25laHhJcWIwZyIsInNpZCI6IjgyNEJBM0ZGRTZGRDQyQ0M2NkZDNEEyRkE2QkE2OTMxIiwic3ViIjoiNmM3OWRmOWUtNjhjOS00YzdkLTY5NTgtMDhkYjM3MGNjODRjIiwiYXV0aF90aW1lIjoxNjgwODMzMjMwLCJpZHAiOiJsb2NhbCIsImFtciI6WyJwd2QiXX0.GFIV4VluRK9a9kSdEsk8oJdPmtSA7VGg0ZHfMJWSgLCZwRsGcGq_7Pdk1Ny5a4gxI7PVu7MFOAnjCzzz8cgJKa1j7PNeuDiWLuJODMLWBF3U1eYPXIsXISLYDBByhinprUGKxm44JBBSIv-PcOSdhjDngxJWRKD6pZDjlgcfqiQl_bF-405PHZAEy3JFD5SG7NGLWwkV1IOmdOzyCr_EC4_nRkJXY0LIFRMZ2VDqSa1QxetKrlFSWqJCHCvQtnk0bFkdhR3g3UqjA9jyNEjmAcfAbUPscSlohepz5KtjpRIG3vWghlwS7glt_c-aDMUDAbKbpG69GEUY80MAEqhhqw

The purpose of the access_token is to access downstream microservices, here's a sample

eyJhbGciOiJSUzI1NiIsInR5cCI6ImF0K2p3dCJ9.eyJuYmYiOjE2ODA4MzMyMzAsImV4cCI6MTY4MDgzNjgzMCwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo1MDA1IiwiYXVkIjpbIlNlZWxhbnNUeXJlc012Y0JmZiIsImh0dHA6Ly9sb2NhbGhvc3Q6NTAwNS9yZXNvdXJjZXMiXSwiY2xpZW50X2lkIjoic2VlbGFuc3R5cmVzbXZjZnJvbnRlbmQiLCJzdWIiOiI2Yzc5ZGY5ZS02OGM5LTRjN2QtNjk1OC0wOGRiMzcwY2M4NGMiLCJhdXRoX3RpbWUiOjE2ODA4MzMyMzAsImlkcCI6ImxvY2FsIiwianRpIjoiNkI2NjE4RDA4MkMzOTMxMURBRDk0Q0VERThCNkYyNEQiLCJzaWQiOiI4MjRCQTNGRkU2RkQ0MkNDNjZGQzRBMkZBNkJBNjkzMSIsImlhdCI6MTY4MDgzMzIzMCwic2NvcGUiOlsib3BlbmlkIiwicHJvZmlsZSIsIlNlZWxhbnNUeXJlc012Y0JmZi5mdWxsYWNjZXNzIiwicm9sZSIsIm9mZmxpbmVfYWNjZXNzIl0sImFtciI6WyJwd2QiXX0.nInvNEblI4x09QtZeAeUCEjv7SfLH7WUiMy09qn7jOszmcVjSfHiLkDJHPeSrdVPv5TtxFsnWx8Turn354leg9kl7FheqFIsfaU_lmyVNtsMuBXhwmX05gRWRWmaTn9KV47ixHPgFcvC4kZ3PWWZzKJQ3OkdPKjX3VYNdnEQkaqbpFCzLYuqbdZFZQHWWqIUyQP2ApR7B23JvMxuedTjo2txu3n1UXxjjDdQ2HjPw0ReHfAn9t0AeImeQ91sdVKmpIH3tHQRW8VPyHDuxRBFugZT3OepIBPPvTKU0CR8uK9zg4puFGoPKqy-LAyQEituDcbiWRZJw1FNsUQVtshVOw

And lastly, the refresh_token. This is issued when the client requests offline_access as a scope and is used to retrieve a new access token when one expires and is NOT a json web token [JWT]

4243C270351289A9544716EE81B3F468D03189274964E76F6E6BC787BB16B1BC

Looking at the encoded version of the tokens is of course not useful to us, copy each JWT and head over to jwt.io and paste it there to see its contents

A JWT has three components to it and is split by dots [.]

  • A header containing it's algorithm and token type [the token type is used during validation]
  • The payload containing all the user's claims
  • A signature to verify that the contents of the token haven't been tampered with

The Access Token

The focus here is going to be on the payload of the access token

Here is the decoded payload from jwt.io

{
  "nbf": 1680833230,
  "exp": 1680836830,
  "iss": "http://localhost:5005",
  "aud": [
    "SeelansTyresWebBff",
    "http://localhost:5005/resources"
  ],
  "client_id": "seelanstyresmvcfrontend",
  "sub": "6c79df9e-68c9-4c7d-6958-08db370cc84c",
  "auth_time": 1680833230,
  "idp": "local",
  "jti": "6B6618D082C39311DAD94CEDE8B6F24D",
  "sid": "824BA3FFE6FD42CC66FC4A2FA6BA6931",
  "iat": 1680833230,
  "scope": [
    "openid",
    "profile",
    "SeelansTyresWebBff.fullaccess",
    "role",
    "offline_access"
  ],
  "amr": [
    "pwd"
  ]
}

The following values are of importance here:

  • iss: Issuer [http://localhost:5005] | The creator of the token
  • aud: Audience [SeelansTyresWebBff] | The intended recipient of the token
  • sub: Subject [6c79df9e-68c9-4c7d-6958-08db370cc84c] | The id of the logged in user
  • scope: Scope [SeelansTyresWebBff.fullaccess] | The granted permissions

Bringing In Some Context

In order to complete the explanation, some pieces of the other wiki pages need to be brought here

The Mvc Frontend OpenIDConnect Configuration

builder.Services.AddAuthentication(options =>
{
    options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
    .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme)
    .AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, options =>
    {
        options.ClientId = builder.Configuration["ClientCredentials:ClientId"];
        options.ClientSecret = builder.Configuration["ClientCredentials:ClientSecret"];

        options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
        options.Authority = builder.Configuration["IdentityServer"];
        options.RequireHttpsMetadata = false;
        
        options.ResponseType = "code";
        options.SaveTokens = true;
        
        options.GetClaimsFromUserInfoEndpoint = true;

        options.Scope.Add("SeelansTyresWebBff.fullaccess");
        options.Scope.Add("offline_access");
        options.Scope.Add("role");

        options.ClaimActions.MapUniqueJsonKey(ClaimTypes.Role, ClaimTypes.Role);
    });

Ocelot's Configuration

{
    "Routes": [
        {
            "DownstreamPathTemplate": "/api/customers/{customerId}/addresses",
            "DownstreamScheme": "http",
            "DownstreamHostAndPorts": [
                {
                    "Host": "localhost",
                    "Port": "5011"
                }
            ],
            "UpstreamPathTemplate": "/addressservice/api/customers/{customerId}/addresses",
            "UpstreamHttpMethod": [ "Get", "Post" ],
            "AuthenticationOptions": {
                "AuthenticationProviderKey": "SeelansTyresWebBffAuthenticationScheme"
            },
            "DelegatingHandlers": [
                "AddressServiceDelegatingHandler"
            ]
        }
    ]
}

Bringing It All Together

The Mvc Frontend client requests the following scopes:

  • SeelansTyresWebBff.fullaccess
  • offline_access
  • role

The openid and profile scopes are added by default

Requesting SeelansTyresWebBff.fullaccess adds SeelansTyresWebBff as an audience and is reflected in the access token

{
    "aud": [
        "SeelansTyresWebBff",
        "http://localhost:5005/resources" // ignore this
    ]
}

The requested scopes are reflected in the access token too

{
    "scope": [
        "openid",
        "profile",
        "SeelansTyresWebBff.fullaccess",
        "role",
        "offline_access"
    ]
}

Looking at Ocelot's configured AuthenticationOptions, it has SeelansTyresWebBffAuthenticationScheme as the AuthenticationProviderKey which must match the configuration in Program.cs and does

var authenticationScheme = "SeelansTyresWebBffAuthenticationScheme";

Having it set like this does have the advantage of having many different types of authentication configured

When a request is received at the gateway, the access token is first validated before any other processing occurs

Sample of an Exchanged Access Token

The raw token

eyJhbGciOiJSUzI1NiIsInR5cCI6ImF0K2p3dCJ9.eyJuYmYiOjE2ODA4MzMyNjYsImV4cCI6MTY4MDgzNjg2NiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo1MDA1IiwiYXVkIjpbIkFkZHJlc3NTZXJ2aWNlIiwiaHR0cDovL2xvY2FsaG9zdDo1MDA1L3Jlc291cmNlcyJdLCJjbGllbnRfaWQiOiJzZWVsYW5zdHlyZXNtdmNiZmZ0b2Rvd25zdHJlYW0iLCJzdWIiOiI2Yzc5ZGY5ZS02OGM5LTRjN2QtNjk1OC0wOGRiMzcwY2M4NGMiLCJhdXRoX3RpbWUiOjE2ODA4MzMyNjYsImlkcCI6ImxvY2FsIiwianRpIjoiMkM2OTFGN0QzMjc5MTlFRjcwNDA2MjEyMTQzRUU4OEIiLCJpYXQiOjE2ODA4MzMyNjYsInNjb3BlIjpbIkFkZHJlc3NTZXJ2aWNlLmZ1bGxhY2Nlc3MiLCJvcGVuaWQiLCJwcm9maWxlIiwicm9sZSJdLCJhbXIiOlsiYWNjZXNzX3Rva2VuIiwicHdkIl19.KvwuvAeHHONNpzjFEe7z33bEdg2xqlvrgTKG3H9COlwZUjx7Ucf1iyYZkgdIq2xvbHmpzbaY5aFUQ-YPKhlEvU83UVBpFeieYAWqVYYfOSBZ4dYTJBxSdbIhlGTdzi2w4Y_ryNVKVC0JxJFx3j87F5_VImykTfaQeI_f2Y9XLV7sbP2WPVZ7uR3AYxgb7GIT-1Fs3OsHqTwXnyRAtWF6agiLYNKCrtkJwDp2c5DXbdIHis9odmpN-u3vYXuIgZaNtFPGW9Zq_pKWynLE796Tp5QrWmNWmZN2CJp4o9g4h5T7g5v-KIgagwcPfuNUvcwX_P1aw4CUfPTyOONW1ucQMw

The payload after being decoded

{
  "nbf": 1680833266,
  "exp": 1680836866,
  "iss": "http://localhost:5005",
  "aud": [
    "AddressService",
    "http://localhost:5005/resources"
  ],
  "client_id": "seelanstyreswebbfftodownstream",
  "sub": "6c79df9e-68c9-4c7d-6958-08db370cc84c",
  "auth_time": 1680833266,
  "idp": "local",
  "jti": "2C691F7D327919EF70406212143EE88B",
  "iat": 1680833266,
  "scope": [
    "AddressService.fullaccess",
    "openid",
    "profile",
    "role"
  ],
  "amr": [
    "access_token",
    "pwd"
  ]
}

Here we can see that AddressService.fullaccess is added as a scope, and AddressService is included as an audience

Also something to point out is the sub claim [6c79df9e-68c9-4c7d-6958-08db370cc84c], it's passed on from the original access token

If the user had a role, it'll also show up as one of the claims

Example

This is the same example used to explain Ocelot

Scenario: GET all addresses for customer 00000000-0000-0000-0000-000000000000

Request

Upstream: GET http://localhost:5050/addressservice/api/customers/00000000-0000-0000-0000-000000000000/addresses

Downstream: [Forwarded] GET http://localhost:5011/api/customers/00000000-0000-0000-0000-000000000000/addresses

Sequence Diagram

sequenceDiagram
    participant mvc as Mvc Frontend
    participant webbff as Web Backend-For-Frontend
    participant addressservice as Address Service
    mvc ->> webbff: Makes GET request
    webbff ->> webbff: Validates the access token
    webbff ->> addressservice: Forwards GET request downstream
    addressservice ->> addressservice: Validates the access token
    addressservice -->> webbff: 200 OK
    webbff -->> mvc: 200 OK

Loading

This diagram skips over the token exchange process as it doesn't contribute to where in the process access tokens are validated

Clone this wiki locally