I am trying to get the cookie after a successful login but I cant figure out how.
On the ASP Net Core Identity API using swagger the browser gets the cookie but when I use the fetch api I cant get the cookie. I tried returning the return response.json(); but this does not work. I also have the redirecting to home page on login Success but Iam not sure exactly how to return the return response.json(); if that is needed.
Both the Identity api and the JS - Cleint are running on localhost.
JS - Fetch API - POST:
function IdentityPost(formID, postUrl) {
const currForm = document.getElementById(formID); // Get the Form
var submitBtn = currForm.elements.namedItem("triggerSubmit"); // Get the submit button of the form
// Listen for Form- Submit
currForm.addEventListener('submit',function handler(e)
{
e.preventDefault(); // Prevent page reload on Submit
submitBtn.disabled = true; // Disable the submit button
LoadingMsg(); // Show Loading Message
// Get form data as string---------------------------------------------------------------
const formData = new FormData(this); // "this" = this Form
const searchParams = new URLSearchParams(formData); // Get the form data params
let formQueryString = searchParams.toString(); // Get the form data params as string
// POST ----------------------------------------------------------------------------------
fetch(identityApiUri + postUrl + formQueryString, // #1 = API-Address, #2 = API - Controller/Mehod, #3 = form data as sring
{
method: 'POST',
credentials: 'same-origin'
}).then(function (response)
{
// IF OK
if (response.status == 200 || response.status == 201) // Status 201 = "Created"
{
RemoveLoadingMsg();
SuccessMsg("Success");
currForm.reset(); // Reset the form
submitBtn.disabled = false; // Enable Submit button
if (document.referrer.split('/')[2] === window.location.host) // Return to previous page if local
{
history.back(); // Go back to previouse page
}
else
{
window.location.href = "/"; // RETURN TO Home
}
}
else // If Bad STATUS
{
return Promise.reject(response); // Triggers Catch method
}
}).catch(function (err) // If Exception
{
RemoveLoadingMsg();
// Show Error
try // Because of JSON Parse and err.text()
{
err.text().then(errorMessage => {
var error = errorMessage.substring(1, errorMessage.length - 1); // Remove the [..] form the Msg
ErrorMsg(error); // Get the error and display
});
}
catch(e)
{
console.warn("Post Exception - Probably No connection to hte server");
ErrorMsg(err + " - Server is probably offline"); // Get the error and display
}
submitBtn.disabled = false; // Enable Submit button
console.warn('Post Exception:', err);
});
this.removeEventListener('submit', handler); // Remove Event Listener
});
}
ASP Net Core - Identity API - Startup:
I have enabled CORS Any Origin. - Not sure if I need ti include the .AllowCredentials(). If I try to enable it it says that I cant have .AllowAnyOrigin() enabled. I am accesing the Api directly from the Client "Browser".
using Leanheat.Identity.API.DBContexts;
using Leanheat.Identity.API.Filters;
using Leanheat.Identity.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.HttpsPolicy;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Authorization;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.OpenApi.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Leanheat.Identity.API
{
public class Startup
{
// Startup
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// Configure Services =================================================================================
public void ConfigureServices(IServiceCollection services)// This method gets called by the runtime. Use this method to add services to the container.
{
// Log in - DbContext
services.AddDbContextPool<LeanheatIdentityApiContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("IdentityContextConnection")));
// UnitOfWork - Filter
services.AddScoped<UnitOfWorkFilter>();
services.AddControllers(config => { config.Filters.AddService<UnitOfWorkFilter>(); }); // UnitOfWork for all Controllers
// CORS - Allow calling the API from WebBrowsers
services.AddCors();
// Log In
services.AddIdentity<ApplicationUser, IdentityRole>(options =>
{
// Password settings
options.Password.RequireDigit = false;
options.Password.RequireLowercase = false;
options.Password.RequireNonAlphanumeric = false;
options.Password.RequireUppercase = false;
options.Password.RequiredLength = 6;
options.Password.RequiredUniqueChars = 1;
}).AddEntityFrameworkStores<LeanheatIdentityApiContext>().AddDefaultTokenProviders(); // AddDefaultTokenProviders is used for the Update Log In Password etc.
// Log In
// Make all Controllers protected by default so only Authorized Users can accsess them, for Anonymouse Users use [AlloAnonymouse] over the controllers.
services.AddMvc(options => {
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
options.Filters.Add(new AuthorizeFilter(policy));
}).AddXmlSerializerFormatters();
//services.AddControllers();
// Swagger
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "Leanheat.Identity.API", Version = "v1" });
});
}
// Configure ===========================================================================================
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
{
// Default Code------------------------------------------------------------------------------------>
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
// Swagger
app.UseSwagger();
app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "Leanheat.Identity.API v1"));
}
app.UseHsts(); // Allow HTTPS
app.UseHttpsRedirection();
app.UseRouting();
// CORS - Allow calling the API from WebBrowsers
app.UseCors(x => x
.AllowAnyMethod()
.AllowAnyHeader()
.AllowAnyOrigin()
.SetIsOriginAllowed(origin => true));// allow any origin
// Log In
app.UseAuthentication(); // UseAuthentication SHOULD ALWAYS BE BEFORE Authorization
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
}
}
The login Method in the Identity API:
// Log In ===================================================================================
[HttpPost]
[Route("LogIn")]
[AllowAnonymous]
public async Task<IActionResult> LogIn(string email, string password, bool rememberMe)
{
if(email != null && password !=null)
{
var result = await signInManager.PasswordSignInAsync(email, password, rememberMe, false);
if (result.Succeeded) // If Login Ok
{
return new JsonResult(result);
}
return StatusCode(401, "[\n \"Invalid Log In\" \n]"); // If Erors return errors
}
return StatusCode(401, "[\n \"Email or Password cant be empty\" \n]");
}
Using swagger I can get the cookie in the browser:
EDIT - Almost working:
I added to the Identity API in the startup.cs this:
services.AddCors(options =>
{
options.AddDefaultPolicy(builder =>
builder.SetIsOriginAllowed(_ => true)
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials());
});
And in the Js Post Code:
// POST ----------------------------------------------------------------------------------
fetch(identityApiUri + postUrl + formQueryString, // #1 = API-Address, #2 = API - Controller/Mehod, #3 = form data as sring
{
method: 'POST',
mode: 'no-cors',
headers: {
'Access-Control-Allow-Origin': '*'
},
credentials: 'include'
And now I get the cookie but I get also exception:
The server is returning Statuscode 200 and I can now get the cookie but I get exception in the fetch api post method.
Ok: false - but I get the cookie and the server returns status 200.
You're calling fetch() from a different origin than the api, right? If so, this sounds like a simple CORS issue.
By default, CORS does not include credentials such as cookies. You have to opt in by both setting the credentials mode on the client side, and the Access-Control-Allow-Credentials header on the server side.
https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Credentials
With the fetch() api, the credentials mode is set with the credentials: 'include' option. As far as the server side, I'm not familiar with ASP but it sounds like it provides some conveniency method to set the relevant header.
As you hint at in your post, when the Access-Control-Allow-Credentials header is set to true, the * value - meaning any origin - actually can’t be used in the Access-Control-Allow-Origin header, so you will have to be explicit about what origin you want allowed - ie the origin of your client application, the origin being defined as the combination of the protocol, domain, and port.
Now it Works thanks to #IAmDranged
JS - Fetch Api - Post Method:
fetch(identityApiUri + postUrl + formQueryString, // #1 = API-Address, #2 = API - Controller/Mehod, #3 = form data as sring
{
method: 'POST',
mode: 'cors',
//headers: {
// 'Access-Control-Allow-Origin': 'https://localhost',
//},
credentials: 'include'
}).then(function (response)
{ ...........................
Asp Net core Identity API - Startup.cs:
// Configure ===========================================================================================
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseRouting();
// CORS
app.UseCors(x => x
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials()
.WithOrigins("https://localhost:44351")); // Allow only this origin
//.SetIsOriginAllowed(origin => true));// Allow any origin
.................................................
So the fix was:
Add to JS - fetch method:
mode: 'cors',
credentials: 'include'
Add to Asp net Core - startup.cs
app.UseCors(x => x
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials()
.WithOrigins("https://localhost:44351")); // Allow only this origin
//.SetIsOriginAllowed(origin => true));// Allow any origin
The .WithOrigins("https://localhost:44351")); allows only the client to use the api and if I use .SetIsOriginAllowed(origin => true)); without the WithOrigins part - it allows all.
Related
I am working on CMS which contains many domains (so our URLs can look like www.example.com/group or www.example.com/de). I want to make a call from frontend based on the current domain:
window.addEventListener('CookiebotOnAccept', (e) => {
const domain = window.location.pathname.replace(/^\/([^\/]*).*$/, '$1');
const URL = `${window.location.protocol}//${window.location.host}//${domain}/UpdateCookies`;
console.log(domain)
fetch(URL, {credentials: 'include'})
.then((response) => {
if (!response.ok) {
throw new Error("Cookie consent update was not succesful")
}
})
.catch((error) => {
console.error('There has been a problem with your fetch operation: ', error)
})
}, false)
...and then I need to be able to send it to an endpoint in the backend, which needs to have a dynamic routing based on the domain it currently receives requests from.
Backend endpoint:
[Route("CookieHandler")]
public class CookieHandlerController : Controller
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly ICookieService _cookieService;
public CookieHandlerController(IHttpContextAccessor httpContextAccessor, ICookieService cookieService)
{
_httpContextAccessor = httpContextAccessor;
_cookieService = cookieService;
}
[HttpGet]
[Route("UpdateCookies")]
public IActionResult UpdateCookies()
{
var requestCookies = _httpContextAccessor.HttpContext?.Request.Cookies;
foreach (var cookie in requestCookies)
{
_cookieService.UpdateCookiesAccordingToConsent(cookie.Key);
}
return Ok();
}
}
So, basically, the endpoint routing needs to look like this:
www.example.com/group/updatecookies
or
www.example.com/fr/updatecookies
The reason for such a strange setup is that there are some Http-only cookies which have their specific path value based on their domain. One more thing to add is that there are so many domains that simply hardcoding them might not work.
I have a paid developer account with Google. I have verified my People API is enabled and that I am using the correct API Key and Client ID. When I try the Google Login Start, I get this error in the console for reason.result.error...
{
code: 401,
message: "The request does not have valid authentication credentials.",
status: "UNAUTHENTICATED"
}
I verified that the API key and ClientID are correct. I start off with a method like this for the login button...
didClickGoogleLogin = () => {
// 1. Load the JavaScript client library.
gapi.load('client', this.googleLoginStart);
}
Then it calls the googleLoginStart method...
googleLoginStart = () => {
// 2. Initialize the JavaScript client library.
gapi.client.init({
'apiKey': <API KEY HERE>,
// clientId and scope are optional if auth is not required.
'clientId': <CLIENT ID HERE>,
'scope': 'profile',
}).then(function() {
// 3. Initialize and make the API request.
return gapi.client.request({
'path': 'https://people.googleapis.com/v1/people/me?personFields=metadata,names,emailAddresses',
})
}).then((response) => {
console.log(response.result);
const googleID = response.result.metadata.sources[0].id;
const googleEmail = response.result.emailAddresses[0].value;
this.updateUser(googleID, googleEmail);
}, function(reason) {
console.log("reason: ", reason);
});
}
This was working for me in another web app I wrote a few months ago, but it's not working there now either.
Any ideas why I'm getting an invalid creds error when the API is enabled, the API Key & Client ID are correct?
I am calling the Web API from the my react component using fetch when I used to run it as one application, there was no problem, but when I am running the application react separate from API, I am getting the CORS error, my fetch call is as below,
componentDidMount() {
console.log(clientConfiguration)
fetch(clientConfiguration['communitiesApi.local'])
.then((response) => {
return response.json();
})
.then(data => {
console.log(data);
let communitiesFromApi = data.map(community => { return { value: community, display: community } });
this.setState({ communities: [{ value: '', display: 'Select a Community...' }].concat(communitiesFromApi) });
}).catch(error => {
console.log(error);
});
};
and my POST call using Axios as below also.
handleDownload = (e) => {
e.preventDefault();
var formData = new FormData();
formData.append('communityname', this.state.selectedCommunity);
formData.append('files', JSON.stringify(this.state['checkedFiles']));
let url = clientConfiguration['filesApi.local'];
let tempFiles = clientConfiguration['tempFiles.local'];
axios({
method: 'post',
responseType: 'application/zip',
contentType: 'application/zip',
url: url,
data: formData
})
.then(res => {
var fileName = `${this.state['selectedCommunity']}.zip`;
saveAs(`https://localhost:44352/TempFiles/${res.data}`, fileName);
});
};
Here is my server side api code:
[HttpGet("{communityName}")]
public string Get(string communityName)
{
string rootPath = Configuration.GetValue<string>("ROOT_PATH");
string communityPath = rootPath + "\\" + communityName;
string[] files = Directory.GetFiles(communityPath);
List<string> strippedFiles = new List<string>();
foreach (string file in files)
{
strippedFiles.Add(file.Replace(communityPath + "\\", ""));
}
return JsonConvert.SerializeObject(strippedFiles);
}
[HttpPost]
public string Post([FromForm] string communityName, [FromForm] string files) //FileContentResult
{
var removedInvalidCharsFromFileName = removeInvalidCharsFromFileName(files);
var tFiles = removedInvalidCharsFromFileName.Split(',');
string rootPath = Configuration.GetValue<string>("ROOT_PATH");
string communityPath = rootPath + "\\" + communityName;
byte[] theZipFile = null;
using (MemoryStream zipStream = new MemoryStream())
{
using (ZipArchive zip = new ZipArchive(zipStream, ZipArchiveMode.Create, true))
{
foreach (string attachment in tFiles)
{
var zipEntry = zip.CreateEntry(attachment);
using (FileStream fileStream = new FileStream(communityPath + "\\" + attachment, FileMode.Open))
using (Stream entryStream = zipEntry.Open())
{
fileStream.CopyTo(entryStream);
}
}
}
theZipFile = zipStream.ToArray();
}
////return File(theZipFile, "application/zip", communityName + ".zip");
string tempFilesPath = Configuration.GetValue<string>("Temp_Files_Path");
if (!System.IO.Directory.Exists(tempFilesPath))
System.IO.Directory.CreateDirectory(tempFilesPath);
System.IO.File.WriteAllBytes($"{tempFilesPath}\\{communityName}.zip", theZipFile);
//return System.IO.File.ReadAllBytes($"{tempFilesPath}\\Test.zip");
//return $"{tempFilesPath}\\{communityName}.zip";
return $"{communityName}.zip";
}
And I am getting the error for Get as below: "Access to fetch at 'https://localhost:44368/api/communities' from origin 'http://localhost:3000' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled."
You'll need to modify your sever. You'll need to
Enable CORS on the server side and
Add the domain where you'll be hosting your front-end to your list of Allowed Origins.
Adding mode: 'no-cors' to the fetch method should do the trick
fetch(clientConfiguration['communitiesApi.local'], {
mode: 'no-cors'
})
.then((response) => {
return response.json();
})
.then(data => {
console.log(data);
let communitiesFromApi = data.map(community => { return { value: community, display: community } });
this.setState({ communities: [{ value: '', display: 'Select a Community...' }].concat(communitiesFromApi) });
}).catch(error => {
console.log(error);
});
When using axios I like to use Allow CORS: Access-Control-Allow-Origin from chrome web store, pretty handy when developing web apps on localhost
You need to add cors on the server-side
This can easily be done by stopping the server and then
npm install cors
and then adding this to your main routers file if you are using multiple files for routing
const express = require("express");
const router = express.Router();
const cors = require("cors");
router.use(cors());
and you are all setup for multi files router.
For single file router you should use the following code:
const express = require("express")
const app = express()
const cors = require("cors")
app.use(cors())
and you are all setup
This should solve the error
thank you I could able to resolve this issue by implementing CORS on my Web API, here is the Code I did, but yours too work great in situations where the Web Api is already implemented and we need to consume the Api and there is not way to go and modify the api, then yours from the client side works. Here is my change in the Web API
public void ConfigureServices(IServiceCollection services)
{
string configValue = Configuration.GetValue<string>("CORSComplianceDomains");
string[] CORSComplianceDomains = configValue.Split("|,|");
services.AddCors(options =>
{
options.AddDefaultPolicy(
builder =>
{
builder.WithOrigins("http://localhost:3000");
});
options.AddPolicy("AnotherPolicy",
builder =>
{
builder.WithOrigins(CORSComplianceDomains)
.AllowAnyHeader()
.AllowAnyMethod();
});
});
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
// In production, the React files will be served from this directory
services.AddSpaStaticFiles(configuration =>
{
configuration.RootPath = "ClientApp/build";
});
}
And added the urls in the appsettings.json file so that any user can add the new urls without much sweating.
"CORSComplianceDomains": "http://localhost:3000|,|http://www.contoso.com"
Thank you very much - I put my answer here so that someone can get it - thanks for jumping in and helping please - I appreciated it - thank you so much.
I had a pretty similar issue on a react project back in the day, to fix that i had to change my package.json writing "proxy": "your origin" in my case was something like "proxy": "http://localhost:5000".
Hope you can solve your issue.
I was getting the same error in the browser logs, but I'm not using React.
Turns out I'm loading my page by IP, but my javascript calls the API using the server domain name. So the browser thinks it's a cross-site request and blocks it. If you are getting the same message and the internet search engine brought you here, check if it's not the same case for you.
If that's the case, you can solve it by finding out if the access is through domain or IP, and use that in the request, instead of having it fixed on one or the other.
Chrome CORS extension worked for me. Please add this extension and also watch video to ensure that you are using it correctly.
Extension name:
Allow CORS: Access-Control-Allow-Origin
I am experiencing an issue when developing an application and attempting to utilize a refresh token. I am using ADFS for authentication, where I get an id_token that expires every hour and a refresh token that lasts 8 hours.
In development, the below script works perfectly as intended and reaches out to the server for a refresh.
In production, it gets new tokens, but it never retries the original request. I am trying to find out why it is different on webpack-dev-server vs production.
Any help would be much appreciated!
P.S. Using Babel Presets: babel-preset-env and babel-preset-stage-2
axios.js
import axios from 'axios'
// Set baseURL for development and production
const baseURL = process.env.NODE_ENV === 'development' ? '//localhost:3001/api' : '/api'
// Create instance of axios with correct baseURL
const instance = axios.create({
baseURL
})
// Intercept responses
instance.interceptors.response.use((response) => {
return response
}, async (error) => {
// Pull config, status and data from the error
const { config, response: { status, data } } = error
// Pull tokens from local storage
let currentTokens = JSON.parse(localStorage.getItem('tokens')) || null
// If response errors at 401, token is still valid and we have tokens in localStorage
if(status === 401 && data.token_invalid === undefined && currentTokens && !config._retry) {
config._retry = true
try {
// Ask server for new token
const authenticate = await instance.post('/user/login', {refresh_token: currentTokens.refresh_token})
// Pull tokens and success from authenticated request
const { tokens, success } = authenticate.data
// If successful, set access_token, id_token, headers and localStorage
if(success) {
currentTokens.access_token = tokens.access_token
currentTokens.id_token = tokens.id_token
const bearer = `Bearer ${tokens.id_token}`
config.headers['Authorization'] = bearer
Object.assign(instance.defaults, {headers: {Authorization: bearer}})
localStorage.setItem('tokens', JSON.stringify(currentTokens))
// Rerun original request
return instance(config)
}
} catch (e) {
// Catch any errors
console.log(e)
return
}
} else if(data && data.token_invalid !== undefined && data.token_invalid) {
// If refresh has expired, take user to ADFS to reauthenticate
location = `${process.env.OAUTH_CLIENT_EP}?client_id=${process.env.AZURE_CLIENT_ID}&redirect_uri=${process.env.REDIRECT_URI}&resource=${process.env.REDIRECT_URI}&response_type=code`
return
} else {
// Console log all remaining errors
return
}
})
export default instance
Found the issue. It appears that since I'm using both relative and absolute urls for the baseURL, the absolute URL in development is being processed correctly, however the relative URL is being chained to the original request.
In other words, sending in production, the url looks like: /api/api/actual/request, where it should just be /api/actual/request.
I solved this by adding a API_URL to my config files, and input the absolute url for both development and production and the updated my instance creation to the following.
const instance = axios.create({
baseURL: process.env.API_URL
})
Thanks to all who viewed and attempted to help. Have a great day everyone!
I would like to know if it is possible to do this, because I'm not sure if I'm wrong or if it isn't possible. Basically, what I want to do is to create a wrap function for native fetch javascript function. This wrap function would implement token validation process, requesting a new accessToken if the one given is expired and requesting again the desired resource. This is what I've reached until now:
customFetch.js
// 'url' and 'options' parameters are used strictely as you would use them in fetch. 'authOptions' are used to configure the call to refresh the access token
window.customFetch = (url, options, authOptions) => {
const OPTIONS = {
url: '',
unauthorizedRedirect: '',
storage: window.sessionStorage,
tokenName: 'accessToken'
}
// Merge options passed by user with the default auth options
let opts = Object.assign({}, OPTIONS, authOptions);
// Try to update 'authorizarion's header in order to send always the proper one to the server
options.headers = options.headers || {};
options.headers['Authorization'] = `Bearer ${opts.storage.getItem(opts.tokenName)}`;
// Actual server request that user wants to do.
const request = window.fetch(url, options)
.then((d) => {
if (d.status === 401) {
// Unauthorized
console.log('not authorized');
return refreshAccesToken();
}
else {
return d.json();
}
});
// Auxiliar server call to get refresh the access token if it is expired. Here also check if the
// cookie has expired and if it has expired, then we should redirect to other page to login again in
// the application.
const refreshAccesToken = () => {
window.fetch(opts.url, {
method: 'get',
credentials: 'include'
}).then((d) => {
// For this example, we can omit this, we can suppose we always receive the access token
if (d.status === 401) {
// Unauthorized and the cookie used to validate and refresh the access token has expired. So we want to login in to the app again
window.location.href = opts.unauthorizedRedirect;
}
return d.json();
}).then((json) => {
const jwt = json.token;
if (jwt) {
// Store in the browser's storage (sessionStorage by default) the refreshed token, in order to use it on every request
opts.storage.setItem(opts.tokenName, jwt);
console.log('new acces token: ' + jwt);
// Re-send the original request when we have received the refreshed access token.
return window.customFetch(url, options, authOptions);
}
else {
console.log('no token has been sent');
return null;
}
});
}
return request;
}
consumer.js
const getResourcePrivate = () => {
const url = MAIN_URL + '/resource';
customFetch(url, {
method: 'get'
},{
url: AUTH_SERVER_TOKEN,
unauthorizedRedirect: AUTH_URI,
tokenName: TOKEN_NAME
}).then((json) => {
const resource = json ? json.resource : null;
if (resource) {
console.log(resource);
}
else {
console.log('No resource has been provided.');
}
});
}
I'll try to explain a little better the above code: I want to make transparent for users the token validation, in order to let them just worry about to request the resource they want. This approach is working fine when the accessToken is still valid, because the return request instruction is giving to the consumer the promise of the fetch request.
Of course, when the accessToken has expired and we request a new one to auth server, this is not working. The token is refreshed and the private resource is requested, but the consumer.js doesn't see it.
For this last scenario, is it possible to modify the flow of the program, in order to refresh the accessToken and perform the server call to get the private resource again? The consumer shouldn't realize about this process; in both cases (accessToken is valid and accessToken has expired and has been refreshed) the consumer.js should get the private requested resource in its then function.
Well, finally I've reached a solution. I've tried to resolve it using a Promise and it has work. Here is the approach for customFetch.js file:
window.customFetch = (url, options, authOptions) => {
const OPTIONS = {
url: '',
unauthorizedRedirect: '',
storage: window.sessionStorage,
tokenName: 'accessToken'
}
// Merge options passed by user with the default auth options
let opts = Object.assign({}, OPTIONS, authOptions);
const requestResource = (resolve) => {
// Try to update 'authorizarion's header in order to send always the proper one to the server
options.headers = options.headers || {};
options.headers['Authorization'] = `Bearer ${opts.storage.getItem(opts.tokenName)}`;
window.fetch(url, options)
.then((d) => {
if (d.status === 401) {
// Unauthorized
console.log('not authorized');
return refreshAccesToken(resolve);
}
else {
resolve(d.json());
}
});
}
// Auxiliar server call to get refresh the access token if it is expired. Here also check if the
// cookie has expired and if it has expired, then we should redirect to other page to login again in
// the application.
const refreshAccesToken = (resolve) => {
window.fetch(opts.url, {
method: 'get',
credentials: 'include'
}).then((d) => {
if (d.status === 401) {
// Unauthorized
window.location.href = opts.unauthorizedRedirect;
}
return d.json();
}).then((json) => {
const jwt = json.token;
if (jwt) {
// Store in the browser's storage (sessionStorage by default) the refreshed token, in order to use it on every request
opts.storage.setItem(opts.tokenName, jwt);
console.log('new acces token: ' + jwt);
// Re-send the original request when we have received the refreshed access token.
requestResource(resolve);
}
else {
console.log('no token has been sent');
return null;
}
});
}
let promise = new Promise((resolve, reject) => {
requestResource(resolve);
});
return promise;
}
Basically, I've created a Promise and I've called inside it to the function which calls to server to get the resource. I've modified a little the request(now called requestResource) and refreshAccessToken in order to make them parametrizable functions. And I've passed to them the resolve function in order to "resolve" any function once I've received the new token.
Probably the solution can be improved and optimized, but as first approach, it is working as I expected, so I think it's a valid solution.
EDIT: As #Dennis has suggested me, I made a mistake in my initial approach. I just had to return the promise inside the refreshAccessToken function, and it would worked fine. This is how the customFetch.js file should look (which is more similar to the code I first posted. In fact, I've just added a return instruction inside the function, although removing the start and end brackets would work too):
// 'url' and 'options' parameters are used strictely as you would use them in fetch. 'authOptions' are used to configure the call to refresh the access token
window.customFetch = (url, options, authOptions) => {
const OPTIONS = {
url: '',
unauthorizedRedirect: '',
storage: window.sessionStorage,
tokenName: 'accessToken'
}
// Merge options passed by user with the default auth options
let opts = Object.assign({}, OPTIONS, authOptions);
// Try to update 'authorizarion's header in order to send always the proper one to the server
options.headers = options.headers || {};
options.headers['Authorization'] = `Bearer ${opts.storage.getItem(opts.tokenName)}`;
// Actual server request that user wants to do.
const request = window.fetch(url, options)
.then((d) => {
if (d.status === 401) {
// Unauthorized
console.log('not authorized');
return refreshAccesToken();
}
else {
return d.json();
}
});
// Auxiliar server call to get refresh the access token if it is expired. Here also check if the
// cookie has expired and if it has expired, then we should redirect to other page to login again in
// the application.
const refreshAccesToken = () => {
return window.fetch(opts.url, {
method: 'get',
credentials: 'include'
}).then((d) => {
// For this example, we can omit this, we can suppose we always receive the access token
if (d.status === 401) {
// Unauthorized and the cookie used to validate and refresh the access token has expired. So we want to login in to the app again
window.location.href = opts.unauthorizedRedirect;
}
return d.json();
}).then((json) => {
const jwt = json.token;
if (jwt) {
// Store in the browser's storage (sessionStorage by default) the refreshed token, in order to use it on every request
opts.storage.setItem(opts.tokenName, jwt);
console.log('new acces token: ' + jwt);
// Re-send the original request when we have received the refreshed access token.
return window.customFetch(url, options, authOptions);
}
else {
console.log('no token has been sent');
return null;
}
});
}
return request;
}