Why i can't attach authorization to header in apollo client? (Nestjs, Graphql, Apollo) - graphql

I'm trying to bind frontend user registration and authorization to my Nestjs/Graphql server. The functionality of creation and authorization works, I create a user and put an access token in the cookie. But the problem is that I can't navigate through closed endpoints because Apollo doesn't allow me to attach the token to the request headers.
My server on backend (main.ts):
async function bootstrap() {
const app = await NestFactory.create(AppModule, { cors: true });
app.useGlobalPipes(new ValidationPipe())
app.useGlobalGuards(new JwtAuthGuard(new Reflector()))
app.enableCors({
origin: 'http://localhost:3000',
credentials: true,
allowedHeaders: 'Origin,X-Requested-With,Content-Type,Accept,Authorization,authorization'
})
await app.listen(3001);
}
bootstrap();
Graphql module in AppModule:
GraphQLModule.forRoot({
autoSchemaFile: join(process.cwd(), 'src/schema/gql'),
sortSchema: true,
driver: ApolloDriver
})
Apollo client on a frontend:
import { parseCookies } from 'nookies'
import { setContext } from '#apollo/client/link/context';
import { ApolloClient, InMemoryCache, createHttpLink } from '#apollo/client'
const cookies = parseCookies()
const httpLink = createHttpLink({
uri: 'http://localhost:3001/graphql'
});
const authLink = setContext((_, { headers }) => {
const token = cookies.access_token
return {
headers: {
...headers,
authorization: token ? `Bearer ${token}` : "",
}
}
});
export const client = new ApolloClient({
cache: new InMemoryCache(),
credentials: 'same-origin',
link: authLink.concat(httpLink)
})
My Rrequest Headers:
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,/;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate, br
Accept-Language: ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7
Cache-Control: max-age=0
Connection: keep-alive
Cookie: access_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImVxMUBnbWFpbC5jb20iLCJzdWIiOjUsImlhdCI6MTY1NzM2NTI2OSwiZXhwIjoxNjU3MzY4ODY5fQ.AdbThqNQl746P-T653jkpvdKXTdrVOsj0SUsjzoTQDo
DNT: 1
Host: localhost:3000
Referer: http://localhost:3000/playlists
sec-ch-ua: ".Not/A)Brand";v="99", "Google Chrome";v="103", "Chromium";v="103"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Windows"
Sec-Fetch-Dest: document
Sec-Fetch-Mode: navigate
Sec-Fetch-Site: same-origin
Sec-Fetch-User: ?1
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36

The reason why your setup doesn't work is probably because HttpOnly is set to true for your cookies and therefore aren't accessible through the JavaScript Document.cookie API. So your authLink doesn't have any effect. So, if you want to include it in the headers you either set HttpOnly to false, which is not preferred, or you store the token in LocaleStorage after authentication.
However as the cookies are set by your server, you can just make sure the cookies are send over to the server, by including credentials within your requests. The credentials however should be set within createHttpLink instead of ApolloClient.
But:
your graphql server is running at port 3001,
your frontend app is served through port 3000.
So credentials : same-origin won't include the headers, as different ports are different origins. Set credentials to include, instead of same-origin. On the server, the token can be accessed through the request.cookies instead of the http-headers. You don't need to parseCookies on the client side.

Related

esp32, esp32_https_server library, self-signed certificate, cors and 499 status code

i am working on an ESP32 project. one of my goals is to communicate with the ESP32 from a website using javascript fetch or XMLHttpRequest().
the ESP32 is connected to my local network and i am using the esp32_https_server library. it uses a self-signed certificate which the browser indicates as valid (but issues a warning, "Connection not protected" due to the self-signed certificate). the website has a CA certificate and is secure.
in testing, the esp32 is conected via USB to my computer, idealy i would like it to stand alone.
the problem i am experiencing is that i cannot seem to connect to the esp32. i keep getting status code 499 errors.
my questions are:
1) how do i successfully connect to the esp32 server from a secure website to get data frome the esp32?
2) how do i do this when the esp32 is not connected to my pc via the usb cable?
please see more info regarding the esp32 set up and responses below.
here's the esp32 code:
ResourceNode *nodeRoot = new ResourceNode("/", "GET", [](HTTPRequest *req, HTTPResponse *res) {
ResourceParameters *params = req->getParams();
std::string action = params->getRequestParameter("action");
String aksie = action.c_str();
Serial.println("Aksie: " + aksie);
if (aksie != "upload_data" && aksie != "upload_current_temp")
{
// this should be home page displayed
// Set the response status
res->setStatusCode(200);
res->setStatusText("success");
res->println("Secure Hello World!!!");
}
else
{
// either uploads..
processParams(aksie, res);
}
});
secureServer->registerNode(nodeRoot);
and here's the code that processes the "upload_current_temp" request:
if (action == "upload_current_temp")
{
// get random temperature
int currentTemp = random(0, 9);
String temp = String(currentTemp);
Serial.println("upload current temperature");
Serial.println("uploadCurrentTemp: " + temp);
std::string tem = temp.c_str();
// Set the response status
res->setStatusCode(200);
res->setStatusText("success current temperature");
StaticJsonDocument<200> doc;
doc["temperature"] = temp;
// Produce a minified JSON document
String output;
serializeJson(doc, output);
Serial.println("curent temp json output: " + output);
deserializeJson(doc, output);
// Set the content type of the response
res->setHeader("Content-Type", "application/json");
res->setHeader("Access-Control-Allow-Origin", "*");
res->setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS");
// As HTTPResponse implements the Print interface, this works fine. Just remember
// to use *, as we only have a pointer to the HTTPResponse here:
serializeJson(doc, *res);
}
and also in setUp() i have this line:
secureServer->setDefaultHeader("Access-Control-Allow-Origin", "*"); //replace * with actual address
when using:
const xhr = new XMLHttpRequest();
const url = 'https://192.168.0.102/?action=upload_current_temp';
xhr.open('GET', url);
xhr.responseType = 'text';
xhr.onload = function () {
const data = xhr.response;
console.log(data);
if (this.readyState == 4 && this.status == 200) {
var obj = JSON.parse(this.responseText);
console.log("getCurTemp(), responseText: " + JSON.stringify(this.responseText, null, 2));
currentTemperature = obj.temperature;
console.log("current temperature: " + currentTemperature);
document.getElementById('currentTemp').innerHTML = currentTemperature;
}
};
xhr.send();
i get these errors (in opera):
499 (Request has been forbidden by antivirus)
has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
and in chrome:
has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
with these headers (opera):
Request URL: https://192.168.0.102/?action=upload_current_temp
Request Method: GET
Status Code: 499 Request has been forbidden by antivirus
Remote Address: 192.168.0.102:443
Referrer Policy: no-referrer-when-downgrade
Cache-Control: no-store, no-cache, must-revalidate, max-age=0
Connection: close
Content-Length: 52266
Content-Type: text/html; charset=utf-8
Expires: Mon, 04 Dec 1999 21:29:02 GMT
Pragma: no-cache
Accept: /
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9
Connection: keep-alive
Host: 192.168.0.102
Origin: https://istimuli.co.uk
Referer: https://istimuli.co.uk/?code=66b72f8e-400c-4adb-ad42-f4efec391d06
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: cross-site
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.132 Safari/537.36 OPR/67.0.3575.79
action: upload_current_temp
and when using :
var url = "https://192.168.0.102/?action=upload_current_temp";
var request = new Request(url, {
method: 'GET',
mode: 'cors', // no-cors, *cors, same-origin
headers: {
'Content-Type': 'application/json'
}
});
fetch(request).then(function (response) {
// Convert to JSON
return response.json();
}).then(function (data) {
console.log("temp: " + JSON.stringify(data));
return data;
}).catch(function (error) {
console.log('Request failed', error)
return 000;
});
i get these errors in opera:
499 (Request has been forbidden by antivirus)
has been blocked by CORS policy: Response to preflight request doesn't pass access control check: 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.
and in chrome:
has been blocked by CORS policy: Response to preflight request doesn't pass access control check: 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.
and these are the headers (opera):
1 requests
51.3 KB transferred
51.0 KB resources
Request URL: https://192.168.0.102/?action=upload_current_temp
Request Method: OPTIONS
Status Code: 499 Request has been forbidden by antivirus
Remote Address: 192.168.0.102:443
Referrer Policy: no-referrer-when-downgrade
Cache-Control: no-store, no-cache, must-revalidate, max-age=0
Connection: close
Content-Length: 52266
Content-Type: text/html; charset=utf-8
Expires: Mon, 04 Dec 1999 21:29:02 GMT
Pragma: no-cache
Accept: /
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9
Access-Control-Request-Headers: content-type
Access-Control-Request-Method: GET
Connection: keep-alive
Host: 192.168.0.102
Origin: https://istimuli.co.uk
Referer: https://istimuli.co.uk/?code=66b72f8e-400c-4adb-ad42-f4efec391d06
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: cross-site
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.132 Safari/537.36 OPR/67.0.3575.79
action: upload_current_temp

Axios is not sending cookies

I have two apps, the server-side app which is written in Laravel and the client-side app, written in VueJS. The vue app consumes the api provided by the laravel app.
The auth flow:
The user attempts to log in, the server sends two tokens to the client, a) access_token and b) refresh_token upon successful login. The server also sends the refresh token in the form of an httpOnly cookie to the client so that when the access token is expired, it can be refreshed using the refresh token from the cookie.
The problem:
When the user logs in, in the response, the server sends the following Set-Cookie header:
Set-Cookie:
refresh_token=tokenvalue;
expires=Mon, 04-Nov-2019 09:13:28 GMT; Max-Age=604800;
path=/v1/refresh; domain=http://app.test; httponly; samesite=none
This means that I expect the cookie to be sent to the server whenever there is a request to the /v1/refresh endpoint. However, the cookie is not present in the request. (I've logged $request->cookie('refresh_token') in controller but it logs null).
This whole token refreshing mechanism is handled in a vuex action:
export function refreshToken({commit}, payload) {
return new Promise((resolve, reject) => {
// axios.defaults.withCredentials = true;
// here, payload() function just converts the url to:
// "http://app.test/v1/refresh"
axios.post(payload('/refresh'), {}, {
withCredentials: true, transformRequest: [(data, headers) => {
delete headers.common.Authorization;
return data;
}]
}).then(response => {
let token = response.data.access_token;
localStorage.setItem('token', token);
commit('refreshSuccess', token);
resolve(token);
}).catch(err => reject(err));
});
}
As you can see, I've set the withCredentials config to true. I am also sending the Access-Control-Allow-Credentials: true from the server. Here is my cors middleware:
public function handle($request, Closure $next)
{
$whiteList = ['http://localhost:8080'];
if (isset($request->server()['HTTP_ORIGIN'])) {
$origin = $request->server()['HTTP_ORIGIN'];
if (in_array($origin, $whiteList)) {
header('Access-Control-Allow-Origin: ' . $request->server()['HTTP_ORIGIN']);
header('Access-Control-Allow-Methods: GET, POST, PATCH, PUT, DELETE, OPTIONS');
header('Access-Control-Allow-Headers: Origin, Content-Type, Authorization');
header('Access-Control-Allow-Credentials: true');
header('Access-Control-Expose-Headers: Content-Disposition');
}
}
return $next($request);
}
I don't know what have I done wrong. My PHP version is: 7.3.5. Here are the request headers of /v1/refresh endpoint:
Accept: application/json, text/plain, */*
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9,bn;q=0.8
Connection: keep-alive
Content-Length: 15
Content-Type: application/x-www-form-urlencoded
Host: app.test
Origin: http://localhost:8080
Referer: http://localhost:8080/products
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.70 Safari/537.36
...and the response headers:
Access-Control-Allow-Credentials: true
Access-Control-Allow-Headers: Origin, Content-Type, Authorization
Access-Control-Allow-Methods: GET, POST, PATCH, PUT, DELETE, OPTIONS
Access-Control-Allow-Origin: http://localhost:8080
Access-Control-Expose-Headers: Content-Disposition
Cache-Control: no-cache, private
Connection: keep-alive
Content-Type: application/json
Date: Mon, 28 Oct 2019 09:40:31 GMT
Server: nginx/1.15.5
Transfer-Encoding: chunked
X-Powered-By: PHP/7.3.5
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 59
I don't know the inner-workings of browser's cookie storing mechanism, I also don't know if an httpOnly cookie can be found in the filesystem, but in despair, to know whether the browser is indeed saving the cookie, I googled and found that cookies are stored in ~/Library/Application Support/Google/Chrome/Default/Cookies file, which is an SQLite file. I opened that file and searched for my cookie 🍪, but it wasn't there either (maybe httpOnly cookies are stored somewhere else?).
Now, my question is, how do I retrieve the cookie from the client-side app?
Since your Vue App and Laravel (API) has different HOST, it will not working.
You can re-check your server response:
Set-Cookie: refresh_token=tokenvalue; expires=Mon, 04-Nov-2019 09:13:28 GMT; Max-Age=604800; path=/v1/refresh; domain=http://app.test; httponly; samesite=none
It sets the cookie to http://app.test, not http://localhost:8080. So, there is no refresh_token cookie set in your http://localhost:8080.
The very typical solution is:
You need to use subdomain, and let your cookie set to the
domain=.app.test (whole domain). I mean, you need to make sure
Laravel and Vue under the same domain.
You don't need to get the refresh_token from cookie again in your Laravel app. First, you just need to save your refresh_token you get from API, to the either localStorage or cookie at your Vue App. Then, just send your refresh_token via forms (form-data). Finally, get your refresh_token via $request->get('refresh_token').
Here is the example, just to illustrate what i mean for the second solution.
Let's assume (typically) the http://app.test/api/login would response:
{
"token_type": "Bearer",
"expires_in": 31622399,
"access_token": "xxx",
"refresh_token": "xxx"
}
import Cookies from 'js-cookie'
async login() {
const { data } = await axios.post('http://app.test/api/login', {
email: 'hi#app.test',
password: 'secret',
})
const refreshToken = data.refresh_token
Cookies.set('refresh_token', refreshToken)
},
async refreshToken() {
const refreshToken = Cookies.get('refresh_token')
const response = await axios.post('http://app.test/api/refresh-token', {
refresh_token: refreshToken,
})
}

Angular 2/4 how to add multiple headers to http post

I've problem with adding additonal headers to html post.
with curl works very well like with postman.
I can't add Authorization header to post request.
my problem is similar to https://stackoverflow.com/questions/39408413/angular2-http-post-how-to-send-authorization-header
my code:
getToken(){
let headers = new Headers;
headers.append('Authorization', 'Basic ' + btoa(Spotify.clientId + ':' + Spotify.clientSecret));
headers.append('Content-Type', 'application/x-www-form-urlencoded');
let options = new RequestOptions({ headers: headers });
let params = new URLSearchParams();
params.append('grant_type', 'client_credentials');
console.log(
this.http.post(Spotify.tokenUrl, params.toString(), options).subscribe()
)
}
Now, when I try to get this none of those two headers aren't added
OPTIONS /api/token HTTP/1.1
Host: accounts.spotify.com
Connection: keep-alive
Access-Control-Request-Method: POST
Origin: http://172.21.7.171:4200
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML,
like Gecko) Chrome/59.0.3071.86 Safari/537.36
Access-Control-Request-Headers: authorization
Accept: */ *
Referer: http://172.21.7.171:4200/
Accept-Encoding: gzip, deflate, br
Accept-Language: pl,en;q=0.8,en-US;q=0.6
But when I comment Authorization header, Content-Type is added and a get invalid_client with error 400
POST /api/token HTTP/1.1
Host: accounts.spotify.com
Connection: keep-alive
Content-Length: 29
Accept: application/json, text/plain, */ *
Origin: http://172.21.7.171:4200
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML,
like Gecko) Chrome/59.0.3071.86 Safari/537.36
Content-Type: application/x-www-form-urlencoded
Referer: http://172.21.7.171:4200/
Accept-Encoding: gzip, deflate, br
Accept-Language: pl,en;q=0.8,en-US;q=0.6
But when I comment Content-Type header and append Authorization I have the same issue like first time - no added header :/
I'have angular 4.2 - installed and upgraded from ng cli
How to add additonal headers to POST? in Postman this work perfect
===== Edited ====
Thanks for reply, but both solutions don't work
getToken(){
let options = new RequestOptions();
options.headers = new Headers();
options.headers.append('Authorization', Spotify.token);
options.headers.append('Content-Type', 'application/x-www-form-urlencoded')
let params = new URLSearchParams();
params.append('grant_type', 'client_credentials');
console.log(
this.http.post(Spotify.tokenUrl, params.toString(), options).subscribe()
)
}
now if I comment 'Authorization' Content-Type is added to Request with AUthorization i none header isn't added and body isn't sent too
========= edited =========
According to this topic Angular2 - set headers for every request I've created default-request-options.service.ts
import { Injectable } from '#angular/core';
import { BaseRequestOptions, RequestOptions, Headers } from '#angular/http';
#Injectable()
export class DefaultRequestOptions extends BaseRequestOptions {
private superHeaders: Headers;
get headers() {
// Set the default 'Content-Type' header
this.superHeaders.set('Content-Type', 'application/json');
const token = localStorage.getItem('authToken');
if(token) {
this.superHeaders.set('Authorization', `Bearer ${token}`);
} else {
this.superHeaders.delete('Authorization');
}
return this.superHeaders;
}
set headers(headers: Headers) {
this.superHeaders = headers;
}
constructor() {
super();
}
}
export const requestOptionsProvider = { provide: RequestOptions, useClass: DefaultRequestOptions };
in app.module.ts
import { requestOptionsProvider, DefaultRequestOptions } from './default-request-options.service'
...
providers: [
requestOptionsProvider
],
now in my service
import { DefaultRequestOptions } from '../default-request-options.service';
getToken(){
let params = new URLSearchParams();
params.append('grant_type', 'client_credentials');
let options = new DefaultRequestOptions();
//options.headers = new Headers();
// options.headers.append('Authorization', Spotify.basicCode);
//options.headers.append('Content-Type', 'application/x-www-form-urlencoded');
// options.body = params.toString();
// options.method = 'post';
console.log(
this.http.post(Spotify.tokenUrl, params.toString(), options).subscribe()
)
}
And I have errors still :/ Headers aren't added :/
I've made some investigations, tests. When I try send only 'Content-type=application/x-www-form-urlencoded' I get reply with error 'bad client' because I don't send Authorization. Problem occours when I try to add another header with Authorization.
I've made similar with GET method and I can add only 'Authorization' header, none other. If I added for example "X-Authorization" header that GET method is changed to OPTIONS method like in POST :/
In console I get error:
XMLHttpRequest cannot load https://accounts.spotify.com/api/token.
Response to preflight request doesn't pass access control check: No
'Access-Control-Allow-Origin' header is present on the requested
resource. Origin 'http://localhost:4200' is therefore not allowed
access.
Is there any way to provide more in one header in http post request?
I added only Content-Type header with body. I got reply with 400 code because my request was without Authorization header. In firefox I edited this reqest by adding Authorization to headers and I got what I wanted code 200 with token :)
So the problem is with Angular not with browser.
==== EDITED ====
Spotify says that i shouldn't use clientid and client+secret. i should use this:
let options = new RequestOptions()
options.headers = new Headers();
options.params = new URLSearchParams();
options.params.append('client_id', Spotify.clientId);
options.params.append('redirect_uri', 'http://localhost:4200/callback');
options.params.append('scope', 'user-read-private user-read-email');
options.params.append('response_type', 'token');
options.params.append('state', '789935');
this.http.get('https://accounts.spotify.com/authorize', options )
.subscribe(
res=> {
console.log('res',res)
}
)
now i get 200 but callback isnt displayed. in console i have:
XMLHttpRequest cannot load https://accounts.spotify.com/authorize?client_id=2100FakeClientId45688548d5a2b9…scope=user-read-private%20user-read-email&response_type=token&state=789935. No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://localhost:4200' is therefore not allowed access.
and this comes from "callback:1"
I've made route for callback and callbackComponent
import { Component } from '#angular/core';
// import { AuthService } from './auth.service';
#Component({
template: `<h2>callback</h2>`
})
export class CallbackComponent {
}
but in console I've error still :/
localhost/:1 XMLHttpRequest cannot load https://accounts.spotify.com/authorize?client_id=21006d1ceeFakeClient548d5a2b9…scope=user-read-private%20user-read-email&response_type=token&state=789935. No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://localhost:4200' is therefore not allowed access.
but this time this comes from 'localhost/:1
In postman I het full html to redirect. How can I make this redirection in Angular 2
Well, if you are using latest angular HttpClient, it is very easy. See the example.
this.httpClient.post(url, body, {
headers: new HttpHeaders().set('Authorization', 'Bearer ' + this.getAuthAccessToken())
.set('Content-Type', 'application/json')
})
Hope it helps. Thanks
You can make use of Interceptors to put headers on every request:
This tutorial will help you accomplish that:
https://medium.com/codingthesmartway-com-blog/angular-4-3-httpclient-accessing-rest-web-services-with-angular-2305b8fd654b
What I do and works is that
this.options = new RequestOptions({
headers: new Headers({
'header1': 'value1',
// And more
})
});
And I use this.options in my requests.
Hope this helps you
you can set header like that
let options = new RequestOptions();
options.headers = new Headers();
options.headers.append('Authorization', 'Basic ' + btoa(Spotify.clientId + ':' + Spotify.clientSecret));
options.headers.append('Content-Type', 'application/x-www-form-urlencoded');
Also, in Angular 5
new class introduce so you can set easily
header = new HttpHeaders();
header.set('Content-Type', 'application/x-www-form-urlencoded');
One way to add multiple headers to http post without using new Headers() would be the following:
$http.post(url, entity, {
headers: {
customHeader1: customHeader1Value,
customHeader2: customHeader2Value
}
}).then(...
I hope it helps.

CORS in Ajax-requests against an MVC controller with IdentityServer3-authorization

I'm currently working on site that uses various Ajax-requests to save, load and autocomplete data. It is build using C#, MVC and JQuery. All actions on the MVC controllers require the users to be authorized, and we use IdentityServer3 for authentication. It was installed using NuGet, and the current version is 2.3.0.
When I open the page and push buttons, everything is working just fine. The problem seem to occur when a certain session expires. If I stay idle for a while, and try to use an Ajax-function, it generates the following error:
XMLHttpRequest cannot load https://identityserver.domain.com/connect/authorize?client_id=Bar&redirect_uri=http%3a%2f%2flocalhost%3a12345&response_mode=form_post&response_type=id_token+token&scope=openid+profile+email+phone+roles [...]. No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://localhost:12345' is therefore not allowed access.
From what I know about Ajax, the problem itself is pretty simple. The MVC site has lost track of the current session, and it is asking the client to authenticate again. The response I get from the Ajax-request is a "302 Found", with a Location-header that points to our IdentityServer. The IdentityServer happens to be on another domain, and while this works fine when you are performing regular HTTP-requests, it does not work particularly well for Ajax-requests. The "Same Origin Policy" is straight up blocking the Ajax-function from authenticating. If I refresh the page, I will be redirected to the IdentityServer and authenticate normally. Things will then go back to normal for a few minutes.
The solution is probably to add an extra header in the response message from the IdentityServer, that explicitly states that cross-origin requests are allowed for this service.
I am currently not getting this header from the IdentityServer (checked in Fiddler).
According to the docs, it should be enabled by default. I have checked that we have indeed enabled CORS this way:
factory.CorsPolicyService = new Registration<ICorsPolicyService>(new DefaultCorsPolicyService { AllowAll = true });
This is one of my clients:
new Client
{
Enabled = true,
ClientName = "Foo",
ClientId = "Bar",
ClientSecrets = new List<Secret>
{
new Secret("Cosmic")
},
Flow = Flows.Implicit,
RequireConsent = false,
AllowRememberConsent = true,
AccessTokenType = AccessTokenType.Jwt,
PostLogoutRedirectUris = new List<string>
{
"http://localhost:12345/",
"https://my.domain.com"
},
RedirectUris = new List<string>
{
"http://localhost:12345/",
"https://my.domain.com"
},
AllowAccessToAllScopes = true
}
These settings do not work. I am noticing that I have an extra forward slash in the URIs here, but if I remove them, I get the default IdentityServer-error that states that the client is not authorized (wrong URI). If I deploy the site (instead of running a localhost debug), I use the domain name without a trailing slash, and I get the exact same behaviour as I do in debug. I do notice that there is no trailing slash in the error message above, and I figured this could be the problem until I saw the same thing in the deployed version of the site.
I also made my own policy provider, like this:
public class MyCorsPolicyService : ICorsPolicyService
{
public Task<bool> IsOriginAllowedAsync(string origin)
{
return Task.FromResult(true);
}
}
... and I plugged it into the IdentityServerServiceFactory like this:
factory.CorsPolicyService = new Registration<ICorsPolicyService>(new MyCorsPolicyService());
The idea is for it to return true regardless of origin. This did not work either; exactly the same results as before.
I've read about a dozen other threads on this particular subject, but I'm getting nowhere. To my knowledge, we are not doing anything unusual when it comes to the setup of the different sites. It's all pretty much out-of-the-box. Any advice?
----- UPDATE -----
The problem persists. I have now tried some fresh tactics. I read somewhere that cookie authentication was bad for Ajax-requests, and that I should be using bearer tokens instead. I set this up in Ajax like this:
$(function () {
$(document).ajaxSend(function (event, request, settings) {
console.log("Setting bearer token.");
request.setRequestHeader("Authorization", "Bearer " + $bearerToken);
});
});
Both the console in Chrome and Fiddler confirms that the token is indeed present and sent by JQuery. The token I use comes from the access_token-property on claims principal object from HttpContext.GetOwinContext().Authentication.User.
This didn't do much. I still get a 302-response from the server, and Fiddler reveals that the token is not sent on the following Ajax-request (which is a GET-request) to the IdentityServer.
From there, I read this thread:
Handling CORS Preflight requests to ASP.NET MVC actions
I tried to put this code in to the startup.cs of the IdentityServer, but there does not appear to be a "preflight" request going in. All I see in Fiddler is this (from the beginning):
1 - The initial Ajax-request from the client to the MVC controller:
POST http://localhost:12345/my/url HTTP/1.1
Host: localhost:12345
Connection: keep-alive
Content-Length: pretty long
Authorization: Bearer <insert long token here>
Origin: http://localhost:12345
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.106 Safari/537.36
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Accept: application/json, text/javascript, */*; q=0.01
X-Requested-With: XMLHttpRequest
Referer: http://localhost:12345/my/url
Accept-Encoding: gzip, deflate
Accept-Language: nb-NO,nb;q=0.8,no;q=0.6,nn;q=0.4,en-US;q=0.2,en;q=0.2
Cookie: OpenIdConnect.nonce.<insert 30 000 lbs of hashed text here>
param=fish&morestuff=salmon&crossDomain=true
2 - The redirect response from the MVC controller:
HTTP/1.1 302 Found
Cache-Control: private
Location: https://identityserver.domain.com/connect/authorize?client_id=Bar&redirect_uri=http%3a%2f%2flocalhost%3a12345%2f&response_mode=form_post&response_type=id_token+token&scope=openid+profile+email [...]
Server: Microsoft-IIS/10.0
X-AspNetMvc-Version: 5.2
X-AspNet-Version: 4.0.30319
Set-Cookie: OpenIdConnect.nonce.<lots of hashed text>
X-SourceFiles: <more hashed text>
X-Powered-By: ASP.NET
Date: Fri, 15 Jan 2016 12:23:08 GMT
Content-Length: 0
3 - The Ajax-request to the IdentityServer:
GET https://identityserver.domain.com/connect/authorize?client_id=Bar&redirect_uri=http%3a%2f%2flocalhost%3a12345%2f&response_mode=form_post&response_type=id_token+token&scope=openid+profile+email [...]
Host: identityserver.domain.com
Connection: keep-alive
Accept: application/json, text/javascript, */*; q=0.01
Origin: http://localhost:12345
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.106 Safari/537.36
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Referer: http://localhost:12345/my/url
Accept-Encoding: gzip, deflate, sdch
Accept-Language: nb-NO,nb;q=0.8,no;q=0.6,nn;q=0.4,en-US;q=0.2,en;q=0.2
4 - The response from IdentityServer3
HTTP/1.1 302 Found
Content-Length: 0
Location: https://identityserver.domain.com/login?signin=<some hexadecimal id>
Server: Microsoft-IIS/8.5
Set-Cookie: SignInMessage.<many, many, many hashed bytes>; path=/; secure; HttpOnly
X-Powered-By: ASP.NET
Date: Fri, 15 Jan 2016 12:23:11 GMT
5 - The meltdown of Chrome
XMLHttpRequest cannot load https://identityserver.domain.com/connect/authorize?client_id=Bar&blahblahblah. No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://localhost:12345' is therefore not allowed access.
I was having a similar issue using OWIN Middleware for OpenIDConnect with a different identity provider. However, the behavior occurred after 1 hour instead of 5 minutes. The solution was to check if the request was an AJAX request, and if so, force it to return 401 instead of 302. Here is the code that performed this:
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
ClientId = oktaOAuthClientId,
Authority = oidcAuthority,
RedirectUri = oidcRedirectUri,
ResponseType = oidcResponseType,
Scope = oauthScopes,
SignInAsAuthenticationType = "Cookies",
UseTokenLifetime = true,
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthorizationCodeReceived = async n =>
{
//...
},
RedirectToIdentityProvider = n => //token expired!
{
if (IsAjaxRequest(n.Request))
{
n.Response.StatusCode = 401;//for web api only!
n.Response.Headers.Remove("Set-Cookie");
n.State = NotificationResultState.HandledResponse;
}
return Task.CompletedTask;
},
}
});
Then, I used an Angular interceptor to detect a statusCode of 401, and redirected to the authentication page.
I came across this problem as well and UseTokenLifetime = false was not solving the problem since you loose the token validity on STS.
When I tried to reach the authorized api method, I still got 401 even if I was valid on Owin.
The solution I found is keeping UseTokenLifetime = true as default but to write a global ajax error handler (or angular http interceptor) something like this:
$.ajaxSetup({
global: true,
error: function(xhr, status, err) {
if (xhr.status == -1) {
alert("You were idle too long, redirecting to STS") //or something like that
window.location.reload();
}
}});
to trigger the authentication workflow.
I had this issue recently, it was caused by the header X-Requested-With being sent with the AJAX request. Removing this header or intercepting it and handling it with a 401 will put you on the right track.
If you don't have this header, the issue is most likely being caused by a different header triggering the Access-Control-Allow-Origin response.
As you found, nothing you do in Identity Server regarding CORS will solve this.
As it turns out, the problem was in the client configuration in MVC. I was missing the UseTokenLifetime property, which should have been set to false.
app.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
ClientId = "Bar",
Scope = "openid profile email phone roles",
UseTokenLifetime = false,
SignInAsAuthenticationType = "Cookies"
[...]
For some reason, IdentityServer sets all these cookies to expire within 5 minutes of them being distributed. This particular setting will override IdentityServer's tiny expiration time, and instead use aprox. 10 hours, or whatever the default is in your client application.
One could say that this is good enough for solving the problem. It will however inevitably return if the user decides to spend 10 hours idling on the site, clicking nothing but Ajax-buttons.
https://github.com/IdentityServer/IdentityServer3/issues/2424
Assumptions:
.NET Framework 4.8 WebForms
OWIN-based auth lib i.e. Microsoft.Owin.Security.OpenIdConnect v4.2.2.0
UseOpenIdConnectAuthentication() with Azure AD endpoint
UseTokenLifetime=true
In Layout.Master:
$.ajaxSetup({
global: true,
error: function (xhr, status, err) {
if (xhr.status == 401) {
window.location.reload();
}
}
});
In startup.cs:
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
...
Notifications = new OpenIdConnectAuthenticationNotifications()
{
...
RedirectToIdentityProvider = RedirectToIdentityProvider
}
});
...
public Task RedirectToIdentityProvider(RedirectToIdentityProviderNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions> context)
{
if (IsAjaxRequest(context.Request))
{
context.Response.StatusCode = 401;
context.Response.Headers.Remove("Set-Cookie");
context.State = NotificationResultState.HandledResponse;
}
}
public bool IsAjaxRequest(this IOwinRequest request)
{
if (request == null)
{
throw new ArgumentNullException("Woopsie!");
}
var context = HttpContext.Current;
var isCallbackRequest = false;
if (context != null && context.CurrentHandler != null && context.CurrentHandler is System.Web.UI.Page page)
{
isCallbackRequest = page.IsCallback;
}
return isCallbackRequest || (request.Cookies["X-Requested-With"] == "XMLHttpRequest") || (request.Headers["X-Requested-With"] == "XMLHttpRequest");
}

CORS Issue When Requesting from ExtJS to node.js. Request or Response Header Incorrect?

I am having issues with making an ExtJS AJAX request to the Nodejs server between two different domains within our network and will appreciate any help. Response fails when attempting from both http and https from ExtJS client side but a Curl from my local via http returns 200 OK with proper data. We are working with content type application/json.
ExtJS onReady function has enabled CORS:
Ext.onReady(function () {
Ext.Ajax.cors = true;
Ext.Ajax.useDefaultXhrHeader = false;
... (code removed)
})
A test from my ExtJS client side on a known working URL that will properly create the ExtJS datastore (brings back 200 OK):
Ext.create('Ext.data.Store', {
id : 'countryStore',
model : 'country',
autoLoad : true,
autoDestroy: true,
proxy: {
type: 'rest',
url : 'https://restcountries.eu/rest/v1/all',
},
reader: {
type : 'json',
headers: {'Accept': 'application/json'},
totalProperty : 'total',
successProperty: 'success',
messageProperty: 'message'
}
});
However, when attempting a request to our Nodejs server via
http:
Ext.create('Ext.data.Store', {
id : 'circuits',
model : 'circuit',
autoLoad : true,
autoDestroy: true,
proxy: {
type: 'rest',
url : 'http://ourNodeJsServerDomain:5500/v3/circuits',
},
reader: {
type : 'json',
headers: {'Accept': 'application/json'},
totalProperty : 'total',
successProperty: 'success',
messageProperty: 'message'
}
});
returns the following in Chrome's console:
Mixed Content: The page at 'https://ourExtJsDevClientSide' was loaded over HTTPS, but requested an insecure XMLHttpRequest endpoint 'http://ourNodeJsServerDomain:5500/v3/circuits?_dc=1430149427032&page=1&start=0&limit=50'. This request has been blocked; the content must be served over HTTPS.
Now, when attempted over https:
Firefox shows:
Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at https://ourNodeJsServerDomain:5500/v3/circuits?_dc=1430151516741&page=1&start=0&limit=50. This can be fixed by moving the resource to the same domain or enabling CORS.
and the Request Header doesn't show "application/json", is this an issue?:
Accept
text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Encoding
gzip, deflate
Accept-Language
en-US,en;q=0.5
Host
ourNodeJsServerDomain:5500
Origin
https://ourExtJsDevClientSide
Referer
https://ourExtJsDevClientSide
User-Agent
Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:37.0) Gecko/20100101 Firefox/37.0
I then tried with Curl to see what the responses were to help debug
on http gives a 200 OK response but Access-Control-Allow-Origin is undefined even when we are defining it as "*":
curl http://ourNodeJsServerDomain:5500/v3circuits?_limit=1 -v
> GET /v3/circuits?_limit=1 HTTP/1.1
> User-Agent: curl/7.37.1
> Host: ourNodeJsServerDomain:5500
> Accept: */*
>
< HTTP/1.1 200 OK
< X-Powered-By: Express
< Access-Control-Allow-Origin: undefined
< Access-Control-Allow-Methods: GET
< Access-Control-Allow-Headers: Content-Type
< Content-Type: application/json; charset=utf-8
< Content-Length: 1126
< ETag: W/"MlbRIlFPCV6w7+PmPoVYiA=="
< Date: Mon, 27 Apr 2015 16:24:18 GMT
< Connection: keep-alive
<
[
{ *good json data returned here* } ]
then when I attempt to Curl via https
curl https://ourNodeJsServerDomain:5500/v3/circuits?_limit=1 -v
* Server aborted the SSL handshake
* Closing connection 0
curl: (35) Server aborted the SSL handshake
We have enabled CORS on our Nodejs server:
router
.all('*', function(req, res, next){
res.setHeader('Content-Type', 'application/json');
// res.setHeader("Access-Control-Allow-Origin", req.headers.origin);
// console.log('\n\nreq.headers.origin ===================== ' + req.headers.origin);
//I have tried allowing all * via res.SetHeader and res.header and neither is defining the Access-Control-Allow-Origin properly when curling
//res.setHeader("Access-Control-Allow-Origin", "*");
res.header("Access-Control-Allow-Origin", "*");
// res.header("Access-Control-Allow-Headers", "X-Requested-With");
res.header('Access-Control-Allow-Methods', 'GET');
res.header('Access-Control-Allow-Headers', 'Content-Type');
I have attempted to be detailed in my thought process and I am willing to try new ways to determine how to understand and solve this.
* SOLUTION *
The issue is mixed content from the browser. Our client UI is on https (secure) whereas we were requesting http (unsecure) content from the nodejs server. We needed to allow for our nodejs server to run on https
We generated SSL certifications and implemented them onto our nodejs server.
Within the nodejs code, we enabled CORS with the CORS module and are running both http and https servers:
// enable CORS for all requests
var cors = require('cors');
app.use(cors());
// for certifications
var credentials = {
key: fs.readFileSync('our.key'),
cert: fs.readFileSync('our.crt')
};
var httpServer = http.createServer(app);
var httpsServer = https.createServer(credentials, app);
httpServer.listen(port, function() {
console.log('HTTP server listening on port ' + port);
});
httpsServer.listen(httpsPort, function() {
console.log('HTTPS server listening on port ' + httpsPort);
});
There seems to be issues with both CORS and HTTPS in your server... You should try this middleware for the CORS part, and make it work when accessing the page in raw HTTP first. As far as I know, you'll have to use different ports for HTTP and HTTPS. And you will also probably need to enable CORS credentials. As I said, I think you'd better make it work in HTTP first ;)
Then, on the Ext part, as already mentioned in comments, you should probably disable default headers (or you'll have to make all of them accepted by your server; see the first comment to this answer). But you need to do that on the proxy, because apparently it replaces the global setting in Ext.Ajax.
So, something like this:
Ext.create('Ext.data.Store', {
id : 'countryStore',
model : 'country',
autoLoad : true,
autoDestroy: true,
proxy: {
type: 'rest',
url : 'https://restcountries.eu/rest/v1/all',
useDefaultXhrHeader: false, // <= HERE
reader: {
type : 'json',
headers: {'Accept': 'application/json'},
totalProperty : 'total',
successProperty: 'success',
messageProperty: 'message'
}
} // <= and notice this change
});
Probably unrelated, but note that your indendation was incorrect and hid the fact that the reader option was applied to the store itself instead of the proxy (so it was ignored).

Resources