Laravel Passport - consuming your own api with javascript 401 Unauthenticated - laravel

I hope you're doing well. I'm struggling since several days with Laravel Passport, trying to consume my own api with javascript / vuejs. The last evenings were spend reading nearly every existing post about my problem, but I could not find any solution. I hope you can help me, thank you at this point.
I've set up a fresh laravel application and installed Laravel Passport like it its described in this offical doc for Laravel 5.8 (https://laravel.com/docs/5.8/passport#introduction). I've done every single point from the tutorial multiple times in different applications, without any success. To test my setup, I'm using this small axios request.
axios.get('/oauth/clients')
.then(response => {
console.log(response);
}).catch(error => {
console.log(response);
})
When I'm logged in, its works fine, when I use a bearer token and use postman to test '/oauth/clients' it works also fine, but when I'm not logged in it always returns 401 Unauthenticated. My goal is to create an api, that I can consume from my own application as an unauthenticated user without a bearer token, but not from the outside. I thought Laravel Passport is the correct way to do this.
My request headers:
:authority: laravel.dev
:method: GET
:path: /oauth/clients
:scheme: https
accept: application/json, text/plain, */*
accept-encoding: gzip, deflate, br
accept-language: de-DE,de;q=0.9,en-US;q=0.8,en;q=0.7
cache-control: no-cache
cookie: XSRF-TOKEN=eyJpdiI6IkxTTGs2cEtvbXc5cmhIZDhEV1lBXC9nPT0iLCJ2YWx1ZSI6IlpPcjdCbmlDcURraG03MndjUTVYeTV1WXl1YWx3MVdNWjFrZ1QrRENJcnlhZVVCNWtZdzl5VDN6ZjdkclBGK0siLCJtYWMiOiI3ZmQ4YjlkZmU3OWMyNjNiYWVhNzJhMWVkOGRhOGJhMzJkNWQwZmZjMDhmZDM2Y2IxYWRkODJiNzFhNmQ5NDA1In0%3D; laravel_session=eyJpdiI6IlpKc3EyS2R4NjAyM0t5XC8xaTR6SjJBPT0iLCJ2YWx1ZSI6Im1JRlRReVFHQ01jTlNQT3BkYlI1V0dNeE1BMkVvODhnWXNjM1VPRUFBRWRBRnl1N1diUmpOSkVJUmc0NTdsVFgiLCJtYWMiOiI0ZTZkNzE2OGQ5MzJkY2FmYzEzOWZiYjA3YTRiM2MxOGQ3OGJmNWIzMTI4ODA5MDMxYWVlMjJmOTk1M2FjZDdlIn0%3D
pragma: no-cache
referer: https://laravel.dev/
user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.100 Safari/537.36
x-csrf-token: 4Opsva9oDk6LkAapLGqqh9AuPzWKfVH4PYoxcMB1
x-requested-with: XMLHttpRequest
x-xsrf-token: eyJpdiI6IkxTTGs2cEtvbXc5cmhIZDhEV1lBXC9nPT0iLCJ2YWx1ZSI6IlpPcjdCbmlDcURraG03MndjUTVYeTV1WXl1YWx3MVdNWjFrZ1QrRENJcnlhZVVCNWtZdzl5VDN6ZjdkclBGK0siLCJtYWMiOiI3ZmQ4YjlkZmU3OWMyNjNiYWVhNzJhMWVkOGRhOGJhMzJkNWQwZmZjMDhmZDM2Y2IxYWRkODJiNzFhNmQ5NDA1In0=
Response headers:
cache-control: no-cache, private
content-type: application/json
date: Sun, 07 Jul 2019 19:05:25 GMT
server: nginx/1.15.5
set-cookie: XSRF-TOKEN=eyJpdiI6IklTWEkrRTJxYXpiRFNQUm0zM3RLYWc9PSIsInZhbHVlIjoiMDEzSmVlMldQOXBrVUQxQjlCUGVVN2dpUk9VWmwrUlRjend3XC9CaUk1S2V6dUFlTDZtVWttREV5cVBPOFwveVQ0IiwibWFjIjoiZWZhMzA3OGVhYmY2NWNjNjgyMTg4NzAxZjQzNWYxZGQyMjVmMWY3MjAwNjllZTM5ODFiNjFiNWY1NGFjOGM0NiJ9; expires=Sun, 07-Jul-2019 21:05:25 GMT; Max-Age=7200; path=/
set-cookie: laravel_session=eyJpdiI6IlFLUDN0bGhBNW1UTytlbGgrcEZ0ekE9PSIsInZhbHVlIjoibHFvM09CSTFyZ05Kd0E0REVCekNOYmRocm01cnBVY3h1YjFOaXpyTXVcL05QSTNYdFFBdVpXZHd4NDVURGwralwvIiwibWFjIjoiYzM0ZWNhNzQ5MTJhY2VmNTZjZmM1YjFiNTgzYjU0NmZhZGIzZjVjYTFkOGNjYWEwNDEwODUyMzc5OWI2Njc3YiJ9; expires=Sun, 07-Jul-2019 21:05:25 GMT; Max-Age=7200; path=/; httponly
status: 401
Important note: I've shared the project with a friend of mine, he works on MacOs with Laravel in a docker container and the authorization works on his device without any changes. I'm running on a windows machine with Laravel Homestead. Could this problem be caused by Laravel Homestead or nginx?

The route 'oauth/clients' is defined under the 'oauth' prefix (e.g., /oauth/any-route) using both the 'web' and 'auth' middlewares. This explains why you are receiving a 401 error when you are not logged in, as the 'auth' middleware is failing.
/vendor/laravel/passport/src/RouteRegistrar.php
/**
* Register the routes needed for managing clients.
*
* #return void
*/
public function forClients()
{
$this->router->group(['middleware' => ['web', 'auth']], function ($router) {
$router->get('/clients', [
'uses' => 'ClientController#forUser',
'as' => 'passport.clients.index',
]);
...
});
}
Also, you should not be using 'web' middleware for your API routes. The API is stateless and should not receive or respond with any session data. This means no need for CSRF/XSRF tokens, which I can see are being transmitted in your example.
You need to place your API routes within the specified API routes file. You'll notice that the authorization middleware 'auth:api' is also slightly different for API routes.
/routes/api.php
Route::middleware(['auth:api'])->group(function () {
Route::get('/your/path', 'Api\YourController#index');
});
Then in Postman, simply pass your bearer token and expected response type. And don't forget to add the 'api' prefix to your route, as you will do for all routes within the api.php routes file.
/api/your/path
Authorization Bearer {your_token}
Accept application/json
Hope this helps and good luck!

Thank you for your answer #matticustard. I've written a small controller to test the authorization and put the route in the following groupe with auth:api middleware:
Route::group(['prefix' => 'v1', 'middleware' => 'auth:api', 'namespace' => 'Api\v1', 'as' => 'api.'], function () {});
When I use a bearer token and postman it works fine, but I want to use this route also from my application as an user who is not logged in. But only from my application not from the outside, how can I achieve this?
axios.get('/api/v1/test')
.then(response => {
console.log(response);
}).catch(error => {
console.log(response);
})
Thank you for your time :)

Related

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,
})
}

Cannot get TrustProxies middleware to work

After pointed in the right direction here Laravel 5.4 relative instead of absolute 302 redirects
I've been trying to get Laravel TrustProxies middleware to work, but seems to be ignoring X_FORWARDED_PROTO header.
My scenario
My app in Laravel (just upgraded from 5.4 to 5.5) is behind a load balancer, which translates all traffic from HTTPS to HTTP.
My problem
All redirects are going over HTTP instead of original protocol HTTPS.
Attempted Solution
Upgrade from Laravel 5.4 to 5.5 and take advantage of the TrustProxies middleware now shipped with Laravel out of the box.
Middleware has:
protected $proxies = '*';
/**
* The current proxy header mappings.
*
* #var array
*/
protected $headers = [
Request::HEADER_FORWARDED => 'FORWARDED',
Request::HEADER_X_FORWARDED_FOR => 'X_FORWARDED_FOR',
Request::HEADER_X_FORWARDED_HOST => 'X_FORWARDED_HOST',
Request::HEADER_X_FORWARDED_PORT => 'X_FORWARDED_PORT',
Request::HEADER_X_FORWARDED_PROTO => 'X_FORWARDED_PROTO',
];
App\Http\Kernel has registered the middleware:
protected $middleware = [
\Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode::class,
\Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
\App\Http\Middleware\TrimStrings::class,
\Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
\App\Http\Middleware\TrustProxies::class,
];
My findings:
Tcp dump from the server reveals the header:
Request:
GET / HTTP/1.1
X_FORWARDED_PROTO: HTTPS
Host: mywebsiteaddress.com
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.12; rv:56.0) Gecko/20100101 Firefox/56.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Upgrade-Insecure-Requests: 1
But the Response has Location over HTTP:
HTTP/1.1 302 Found
Date: Wed, 08 Nov 2017 18:03:48 GMT
Server: Apache/2.4.18 (Ubuntu)
Cache-Control: no-cache, private
Location: http://mywebsiteaddress.com/home
Set-Cookie: laravel_session=eyJp...In0%3D; expires=Wed, 08-Nov-2017 20:03:48 GMT; Max-Age=7200; path=/; HttpOnly
Content-Length: 376
Keep-Alive: timeout=5, max=100
Connection: Keep-Alive
Content-Type: text/html; charset=UTF-8
Additional comments:
Since my app was upgraded from 5.4 to 5.5, I copied the class TrustProxies that otherwise would've been there in a 5.5 fresh installation. Then I registered it in the Kernel.
Maybe I'm missing a step here.
My hope:
That my tiredness is not clouding my mind that I'm overlooking a simple mistake.
Any suggestions, thank you in advance!
Update:
Enabled log_forensics module in Apache and I see the x-forwarded-proto header in the request.
GET / HTTP/1.1
X_FORWARDED_PROTO:HTTPS
Host:mywebsiteaddress.com
User-Agent:Mozilla/5.0 (Macintosh; Intel Mac OS X 10.12; rv%3a56.0) Gecko/20100101 Firefox/56.0
Accept:text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language:en-US,en;q=0.5
Accept-Encoding:gzip, deflate, br
Connection:keep-alive
Upgrade-Insecure-Requests:1
Cache-Control:max-age=0
Any clue why Laravel may not have in the headers array?
It was indeed tiredness.
The load balancer has been working with X_FORWARDED_PROTO header for C# (IIS) apps, so the network team set the header the same way this time.
But for Laravel, the header has to be in the form of X-FORWARDED-PROTO which I understand is the right name (dashes instead of underscores).
That is why Laravel (Symfony in reality) was discarding the header from the request.

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");
}

localhost request headers not sending cookies

I am building a nodeJS server which allows users to login using an AJAX post on a client application. The server responds with a cookie that, after successful login, keeps track of the user. I can't however seem to get the client application to send the cookie back to the server. It never seems to respond to the server including the cookie it just recieved.
On the fist call that's made to the server, I login with my credentials. the server responds with this:
In the response headers:
Set-Cookie:SID=0xtW36rYCiV; path=/; expires=Tue, 24-Jun-2014 14:14:51 GMT; secure
In the subsequent request headers:
No cookie is sent back to the server
In my client application I am using the following code:
jQuery.ajax( {
url: this.domain + this.url,
async: this.async,
type: 'POST',
dataType: this.dataType,
crossDomain: true,
cache: true,
accepts: 'text/plain',
data: this.postVars,
success: this.onData.bind( this ),
error: this.onError.bind( this ),
xhrFields: {
withCredentials: true
}
});
Note I am using the xhrFields.
And in my node server I am responding with this: (notice I am including all the CORRS variables)
if ( request.headers.origin )
{
if ( request.headers.origin.match( /mydomain\.net/ ) || request.headers.origin.match( /appname\.mydomain\.com/ ) || request.headers.origin.match( /localhost/ )
|| request.headers.origin.match( /localhost\.com/ )
|| request.headers.origin.match( /localhost\.local/ )
|| request.headers.origin.match( /localtest\.com/ ) )
{
response.setHeader( 'Access-Control-Allow-Origin', request.headers.origin );
response.setHeader( 'Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE,OPTIONS' );
response.setHeader( 'Access-Control-Allow-Headers', 'Content-Type, Authorization, Content-Length, X-Requested-With' );
response.setHeader( "Access-Control-Allow-Credentials", "true" );
};
}
response.writeHead( 200, { "Content-Type": "application/json" });
response.end( JSON.stringify( json ) );
}
I have also edited my windows hosts file to include these test domains so that I don't have to use an IP or localhost iteself:
127.0.0.1 localhost
127.0.0.1 tooltest.com
127.0.0.1 localhost.com
127.0.0.1 localhost.local
But no matter what I do, or which of the above hosts I use, it never seems to work. It seems to only be related to localhost via ajax because if I go directly to the server url in question - it works.
EDIT 1
So for example - I open the client application and try to login to the server at foo.com/user/log-in. The headers for the request and response are as follows:
Request:
Remote Address:188.xxx.xxx.xx:7000
Request URL:https://foo.com:7000/user/log-in
Request Method:POST
Status Code:200 OK
Request Headersview source
Accept:undefined
Accept-Encoding:gzip,deflate,sdch
Accept-Language:en-GB,en-US;q=0.8,en;q=0.6
Connection:keep-alive
Content-Length:45
Content-Type:application/x-www-form-urlencoded; charset=UTF-8
Host:foo.com:7000
Origin:http://localhost
Referer:http://localhost/animate-ts/trunk/bin/
User-Agent:Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/35.0.1916.153 Safari/537.36
Form Dataview sourceview URL encoded
user:mat
password:testpassword
rememberMe:true
Response:
Response Headersview source
Access-Control-Allow-Credentials:true
Access-Control-Allow-Headers:Content-Type, Authorization, Content-Length, X-Requested-With
Access-Control-Allow-Methods:GET,PUT,POST,DELETE,OPTIONS
Access-Control-Allow-Origin:http://localhost
Connection:keep-alive
Content-Type:application/json
Date:Tue, 24 Jun 2014 14:14:21 GMT
Set-Cookie:SID=0xtW36rYCiV; path=/; expires=Tue, 24-Jun-2014 14:14:51 GMT; secure
Transfer-Encoding:chunked
As I undertand it, Foo.com has told the browser to store the cookie 0xtW36rYCiV. However when I make the very next request (foo.com/user/is-logged-in) to see if the user is logged in, no cookies are sent to foo. Foo.com looks for the cookie with the ID 0xtW36rYCiV but can't find anything. When I look at the 2nd request in dev tools I can see:
Does anyone have any other ideas? I thought I covered everything in the above, but im starting to think it just wont work :(
Thanks
Mat
My cookie isn't marked secure, and I'm facing this problem trying to send a GET request to my server (https://auth.mywebsite.com) from http://localhost:3000/.
I have the same auth flow as OP here.
First, sign in to auth.website.com, and receive back cookie in the response header:
set-cookie: Authorization=Bearer%20blahblah.blahblah.blahblah; Path=/; Expires=Wed, 05 Aug 2020 02:07:57 GMT; HttpOnly
Note, I don't have the Secure flag in set-cookie.
I noticed in Chrome devtools there was this warning that popped up when hovering over the set-cookie line:
"This Set-Cookie didn't specify a "SameSite" attribute and was defaulted to "SameSite=Lax," and was blocked because it came from a cross-site response which was not the response to a top-level navigation. The Set-Cookie had to have been set with "SameSite=None" to enable cross-site usage."
This might be the answer to my problem. But, I don't understand why this was working yesterday, but not today. No code has changed in between.
This issue occurs on both on Chrome and Safari.
The MDN docs has a good description of all the flags for Set-Cookie.
This is not a full answer, but perhaps some extra context for others facing similar issues.
I had this problem, but #aspillers commment lead to the solution for me. The cookie was marked secure, but I was doing the request over HTTP instead of HTTPS. Once I got my debugger switched over to HTTPS it started working.
For chrome:
Go to chrome://flags/
Disable "SameSite by default cookies"
Restart Chrome

Please help me understand Ajax request versus Backbone fetch()

My app can currently hit our API with a standard JQuery Ajax GET request and get good data back. CORS has been properly implemented on the remote server as far as I can see. Here are the response headers:
company_client_envelope_id: 88764736-6654-22e4-br344-a1w2239a892d
access-control-allow-headers: X-Requested-With, Cookie, Set-Cookie, Accept, Access-Control
Allow-Credentials, Origin, Content-Type, Request-Id , X-Api-Version, X-Request-Id,Authorization, COMPANY_AUTH_WEB
access-control-expose-headers: Location
response-time: 55
request-id: 88764736-6654-22e4-br344-a1w2239a892d
company_api_version: 0.01.09
server: localhost
transfer-encoding: chunked
connection: close
access-control-allow-credentials: true
date: Sun, 09 Feb 2014 14:44:05 GMT
access-control-allow-origin: *
access-control-allow-methods: GET, POST
content-type: application/json
However, using Backbone and calling the same GET request by using fetch() causes the following CORS error:
No 'Access-Control-Allow-Origin' header is present on the requested resource.
I cannot figure out what the difference is. Both requests are running from localhost.
In the case of the AJAX query, the following is being sent as requested by the API guys:
headers: {
"accept":"application/json"
}
And in the case of the model and collection declaration I am sending the headers like so:
MyApp.someCollection = Backbone.Collection.extend(
{
model:MyApp.someModel,
headers: {
'Accept':'application/json',
'withCredentials': 'true'
},
url: MYCOMPANY_GLOBALS.API + '/endpoint'
});
and my fetch is simply:
someCollection.fetch();
===============================
Added in response to: #ddewaele
These are the headers from the network tab:
Request URL:http://api-blah.com:3000/
Request Headers CAUTION: Provisional headers are shown.
Accept:application/json
Cache-Control:no-cache
Origin:http://localhost
Pragma:no-cache
Referer:http://localhost/blah/blah/main.html
User-Agent:Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1700.107Safari/537.36
There is no pre-flight or remote headers from the API server:
many thanks,
Wittner
I've recommended to you rewrite Backbone.sync method, because in your app you have some security field for example and other reason.
var oldBackboneSync = Backbone.sync;
// Override Backbone.Sync
Backbone.sync = function (method, model, options) {
if (method) {
if (options.data) {
// properly formats data for back-end to parse
options.data = JSON.stringify(options.data);
}
// transform all delete requests to application/json
options.contentType = 'application/json';
}
return oldBackboneSync.apply(this, [method, model, options]);
}
You can add different headers as you want.

Resources