Window Authentications + JWT -> Token Expire after Refresh with multiple request with same time (Promise) (JavaScript,Web Api Net Core)

 

To implement a scenario where you combine Windows authentication with JWT (JSON Web Token) for token refresh in a JavaScript application communicating with a WebApi backend, you'll need to follow a series of steps. Here’s a structured approach to achieve this:

1. Set UpWindows Authentication on WebApi

First, ensure your WebApi is configured to support Windows Authentication. This typically involves:

  • Configure WebApi: Set authentication mode to Windows in your web.config or appsettings.json depending on your project type (e.g., ASP.NET Core or ASP.NET Framework).

  • Enable CORS: If your JavaScript application is on a different domain, configure CORS (Cross-Origin Resource Sharing) in your WebApi to allow requests from your client application domain.

  • Authorize Controllers: Use [Authorize] attribute on controllers or actions that require Windows authentication.

2. JWT Integration

Since Windows Authentication does not directly provide JWTs, you'll need a mechanism to issue JWTs once the user is authenticated via Windows Authentication. Here’s how you can integrate JWTs:

  • Generate JWT: Upon successful Windows authentication, generate a JWT containing necessary claims (e.g., user ID, roles, expiration time).

  • Send JWT to Client: Return the JWT to your JavaScript client upon successful authentication.

3. Token Refresh Mechanism

To implement token refresh using JWTs:

  • Token Expiration: Ensure your JWTs have an expiration time. The client will need to refresh the token before it expires.

  • Refresh Token Endpoint: Implement a secure endpoint in your WebApi to refresh JWTs. This endpoint should accept a refresh token (if you're using one) or simply the expired JWT.

  • Client-Side Implementation: In your JavaScript client, implement logic to handle token expiration and refresh:


For Example : Refresh Token with Web Api dotnet core


 [HttpPost("RefreshToken")]
 public async Task<ApiResponse> RefreshToken([FromBody] RefreshAuthenticateModel model)
 {
     var principal = GetPrincipalFromExpiredToken(model.AccessToken);
     if (principal != null && !string.IsNullOrEmpty(model.UserToken))
     {
         if (DateTime.Compare(CommonExtensions.FromUnixTime(long.Parse((principal.Identity as ClaimsIdentity).Claims.FirstOrDefault(c => c.Type == "exp").Value)), DateTime.UtcNow) < 0)
         {
             var userDetails = await _UserService.GetUsersDetailsBaseOnUserToken(GetTokenValueExpireToken(principal, "Id").ToInt());
             if ((userDetails?.Id ?? 0) > 0)
             {
                 List<Core.Domain.Settings.Settings> lst = await _settingService.GetAllAsync();
                 userDetails.UserToken = Guid.NewGuid().ToString();
                 await _UserService.UpdateAsync(userDetails, 0, "");
                 Tuple<string, string> Permission = await GetPermissions(userDetails);
                 return ApiResponseHelper.GenerateResponse(
                 ApiStatusCode.Status200OK, "Login Successfully.", new
                 {
                     userDetails.UserToken,
                     Token = await CommonExtensions.GenerateToken(userDetails, Convert.ToString(_Configuration["AuthenticationSettings:SecretKey"])),                           
                 });
             }
         }
     }
     return ApiResponseHelper.GenerateResponse(ApiStatusCode.Status400BadRequest, "Authentication Failed! Invalid username or password.");
 }

//ClaimsPrincipal for JWT

private ClaimsPrincipal GetPrincipalFromExpiredToken(string? token)
{
    var tokenValidationParameters = new TokenValidationParameters
    {
        ValidateAudience = false,
        ValidateIssuer = false,
        ValidateIssuerSigningKey = true,
        IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_Configuration["AuthenticationSettings:SecretKey"])),
        ValidateLifetime = false
    };

    var tokenHandler = new JwtSecurityTokenHandler();
    var principal = tokenHandler.ValidateToken(token, tokenValidationParameters, out SecurityToken securityToken);
    if (securityToken is not JwtSecurityToken jwtSecurityToken || !jwtSecurityToken.Header.Alg.Equals(SecurityAlgorithms.HmacSha256, StringComparison.InvariantCultureIgnoreCase))
        throw new SecurityTokenException("Invalid token");

    return principal;

}

4. JavaScript : Call  Method For (Get, Post and Post with Form Data) With Multiple Request same time (below Example).


const getData = async (url, options = {
    method: 'GET',
}) => {
    options.headers = {
        ...options.headers,
        'Authorization': `Bearer ${localStorage.getItem("Token")}`
    };
    try {
        const response = await fetch(url, options);
        // Token might be expired, attempt to refresh
        return response.status === 423 ? retrypolicy(url, options) : response.json();
    } catch (error) {
        throw error;
    }
};

let isRefreshing = false;
let refreshSubscribers = [];
const onRefreshed = (token) => {
    refreshSubscribers.forEach((callback) => callback(token));
    refreshSubscribers = [];
};
const addRefreshSubscriber = (callback) => {
    refreshSubscribers.push(callback);
};


function retrypolicy(url, options, Isblob = false) {
    if (!isRefreshing) {
        isRefreshing = true;
        try {
            return RefreshToken().then(() => {
                isRefreshing = false;
                onRefreshed(localStorage.getItem("Token"));
                // Retry original request with new token
                options.headers['Authorization'] = `Bearer ${localStorage.getItem("Token")}`;
                return fetch(url, options).then((response) => { return Isblob ? response : response.json(); });
            });
        } catch (refreshError) {
            isRefreshing = false;
            // Handle refresh token failure (e.g., logout user)
            throw refreshError;
        }
    } else {
        // Queue the requests while refreshing
        return new Promise((resolve, reject) => {
            addRefreshSubscriber(() => {
                options.headers['Authorization'] = `Bearer ${localStorage.getItem("Token")}`;
                fetch(url, options).then(resolve).catch(reject);
            });
        }).then((response) => { return Isblob ? response : response.json(); });
    }
}

//---------------- Below Method for Post and Post with Form Data ----------//


async function RequestMethod(url, data, Method, ContentType, Isblob = false) {
    if ((ContentType || "").length == 0)
        ContentType = 'application/json; charset=utf-8';
    if ((Method || "").length == 0)
        Method = 'POST';
    let options = {
        method: Method, // *GET, POST, PUT, DELETE, etc.
        mode: 'cors', // no-cors, *cors, same-origin
        cache: 'no-cache', // *default, no-cache, reload, force-cache, only-if-cached
        credentials: 'same-origin', // include, *same-origin, omit
        headers: {
            'Content-Type': ContentType,
            'Authorization': 'Bearer ' + localStorage.getItem("Token")
        },
        redirect: 'follow', // manual, *follow, error
        referrerPolicy: 'no-referrer', // no-referrer, *no-referrer-when-downgrade, origin, origin-when-cross-origin, same-origin, strict-origin, strict-origin-when-cross-origin, unsafe-url
        body: data // body data type must match "Content-Type" header
    };
    // Default options are marked with *
    const response = await fetch(url, options);
    return response.status === 423 ? retrypolicy(url, options, Isblob) : Isblob ? response : response.json();
}

// Example POST method implementation:
async function postData(url, data, Method, ContentType) {
    return await RequestMethod(url, data, Method, ContentType);
}

async function postDataFromData(url, data) {
    let options = {
        method: 'POST', // *GET, POST, PUT, DELETE, etc.
        body: data,
        headers: {
            'Authorization': 'Bearer ' + localStorage.getItem("Token")
        },
        processData: false,
        contentType: false,
        crossDomain: true
    };
    // Default options are marked with *
    const response = await fetch(url, options);
    return response.status === 423 ? retrypolicy(url, options, false) : response.json();
}

async function postDataWithImage(url, data, Method) {
    let options = {
        method: Method, // *GET, POST, PUT, DELETE, etc.
        body: data,
        headers: {
            'Authorization': 'Bearer ' + localStorage.getItem("Token")
        }
    };
    const response = await fetch(url, options);
    return response.status === 423 ? retrypolicy(url, options) : response.json(); // parses JSON response into native JavaScript objects
}

async function FileDownloadData(url, data, Method = "", ContentType = "") {
    const response = await RequestMethod(url, data, Method, ContentType, true);
    if (response.ok) {
        return response.blob().then(blob => {
            return {
                contentType: response.headers.get("Content-Type"),
                raw: blob,
                FileName: response.headers.get("FileName")
            }
        });
    }
    return null;
}

Note : If you're dealing with HTTP 401 errors in the browser and you want to handle them by displaying a sign-in popup, it's important to understand the context in which these errors occur.

When a server responds with a 401 status code, it typically means that the client's request lacks valid authentication credentials (such as JWT tokens or HTTP basic authentication). However, if you're using Windows Authentication, the browser may automatically handle the authentication challenge, prompting the user to enter their credentials in a popup dialog provided by the browser itself. This behavior is typical for intranet applications or those running within a domain environment.

so we need to use custom error code 401 to 423  for Authentication fails.

Post a Comment

0 Comments