Extracting params in traefik.frontend.auth.forward.address service - reverse-proxy

Summary
I'm trying to set up an authentication passthrough using Traefik's traefik.frontend.auth.forward.address setting. My main web service has the traefik.frontend.auth.forward.address=login.mydomain.com label on the container. Traefik seems to correctly forward incoming requests intended for mydomain.com to login.mydomain.com, but when the login form is submitted, the POST request gets turned into a GET request before it hits the login service, and the parameters of the original POST request seem to be missing. The user can never log in.
Containers
docker run -d \
-l "traefik.frontend.rule=Host:login.mydomain.com; Method:GET, POST" \
-l "traefik.enable=true" \
login-service
docker run -d \
-l "traefik.frontend.rule=Host:mydomain.com" \
-l "traefik.frontend.auth.forward.address=https://login.mydomain.com" \
-l "traefik.frontend.auth.forward.authResponseHeaders=cookie" \
-l "traefik.enable=true" \
web-service
Question
Using auth.forward.address, should I see the parameters from the original POST request in my login service? Since Traefik turns it into a GET request, where in that request should I be looking for the parameters? Or, perhaps I have misconfigured something? Missing a authResponseHeaders flag maybe?
What Works
Requests to mydomain.com show the login form from login-service, with the URL continuing to show mydomain.com; the redirect to login.mydomain.com is happening behind the scenes, which is correct.
I have also tested the login service by itself, and it seems to work. It hosts a form that submits a POST request to the service, before responding with 200 OK and a Set-Cookie header. In fact, when I go to login.mydomain.com directly, I can login, which sets my cookie, and I can go to mydomain.com and skip the login screen.
What Doesn't Work
When submitting the login form, the POST request hits the login-service (as evidenced by the logs in that service) as a GET request and the data in the POST request appears to be gone. Traefik adds an x-forwarded-method set to POST, but I can't find the data in the original POST request. I need the params from my login form to validate them, and they don't appear to be getting through to the login service.
Traefik Configuration
I don't think anything about my Traefik configuration is relevant here, but I'm including it for completeness.
checkNewVersion = true
logLevel = "DEBUG"
defaultEntryPoints = ["https","http"]
sendAnonymousUsage = true
[api]
dashboard = true
debug = true
[entryPoints]
[entryPoints.http]
address = ":80"
[entryPoints.http.redirect]
entryPoint = "https"
[entryPoints.https]
address = ":443"
[entryPoints.https.tls]
[retry]
[docker]
endpoint = "unix:///var/run/docker.sock"
watch = true
exposedbydefault = false
[acme]
email = "admin#mydomain.com"
storage = "acme.json"
entryPoint = "https"
OnHostRule = true
[acme.httpChallenge]
entryPoint = "http"

I tracked down Traefik's auth forward code. Sure enough, the request body is not passed downstream to the authentication service; only the headers make it that far. So much for default form submit behavior.
To get around this, I reworked my client-side authentication logic to submit a POST request with the credentials in the header instead of the body, set using XMLHttpRequest.setRequestHeader.
There's one more catch needed to make it work. I need to set cookies client-side using the Set-Cookie header returned from the authentication server, but if the server returns a 200 OK when the login is successful, Traefik will immediately pass along the original request to the user's intended destination -- meaning the Set-Cookie header will never make it to the user. What I did to get around this was return a 418 I'm a teapot when the authentication was successful. This allows the Set-Cookie header to make it back to the user's browser so the user's auth token can be set. The client then automatically reloads the intended page, this time with the correct cookie set, and now the auth server returns a 200 OK if it sees a valid cookie for the requested service.
Here's what the client side code looks like:
<form id="form" method="post" action="/">
Username: <input type="text" name="username" />
Password: <input type="password" name="password" />
<input type="submit" value="Submit" />
</form>
<script>
// Override the default form submit behavior.
// Traefik doesn't pass along body as part of proxying to the auth server,
// so the credentials have to be put in the headers instead.
const form = document.getElementById("form");
form.addEventListener('submit', function(event) {
const data = new FormData(form);
var request = new XMLHttpRequest();
request.open("POST", "/", true);
request.setRequestHeader("Auth-Form", new URLSearchParams(data).toString());
request.withCredentials = true;
request.onload = function(e) {
if (request.status == 418) {
window.location = window.location.href;
} else {
alert("Login failed.");
}
};
request.send(data);
event.preventDefault();
}, false);
</script>
I'm leaving this issue open at least until the bounty runs out because I can't imagine that this is the intended way to do this. I'm hoping someone can weigh in on how traefik.frontend.auth.forward.address is supposed to be used. Or, if someone has used another authentication proxy strategy with Traefik, I'm eager to hear about it.

Related

Heroku Express / Nextjs client cookie not being set

So I'm having a bit of an issue where I have two apps hosted on Heroku the first being an Express application with the following cookie settings
const cookieSettings = {
maxAge: expiryTime,
...cookieOptions || {},
// For security these properties should always remain below the spread
httpOnly: true,
secure: process.env.NODE_ENV !== "development",
sameSite: "none",
path: "/",
}
And a Nextjs app which has some middleware that uses the cookie for login control to protect routes.
Locally I have no issues with the following login logic within the login route which sets the cookie browser side
const cookie = getCookie({ tokenOptions: { id: user._id } });
res.setHeader("Set-Cookie", cookie);
return res.sendStatus(200);
I have read there is issues with Heroku as it's on the public list of domains so the browser wont set if it comes from this but the issue I'm having is on a custom domain. My domain is https://www.mydomain.co.uk but for some reason I can't get it to set when I'm on this domain.
When using Postman I do get the cookie back but the domain comes from my API domain ie api.reviewcircle.co.uk which I think is why the browser isn't setting the cookie as the domains don't match but I can't find any info on how to fix this so would be great if anyone has any ideas.
You can see the cookie is included in the response but isn't set:

Cookie not being set in browser - CORS/Port issue

Context:
I just split up my existing Next.js/Express app using Lerna, separating the Next.js server (serving the front end on localhost:3000) and my express server (serving the API on localhost:4000). As a result, I had to create an axios instance with the default {baseUrl: localhost:4000}, so my relative path calls will continue to work (ie. axios.post('/api/auth/login', {...})). This was all working before, when the server was serving both the API and the Nextjs app.
Problem:
After this switch, my Authentication flow no longer works properly. After logging in as usual, a page refresh will erase any memory of the user session. As far as I can tell, the Cookie is not being set. I cant see it in my dev-tools, and when the app makes a subsequent call, no cookies are present.
When my app mounts, it makes a call to api/me to see if the user is logged in. If they are, it will respond with the user information to hydrate the app with the users info. If they aren't, it wont do anything special. This request to /api/me no longer contains the persistent cookie set in place by the login call. (dev-tools shows the Set-Cookie header being returned as expected from the original login request)
Possibly Useful information:
Depending on how I run my app, (debugging in VSCode vs running both yarn dev commands in terminal) I will get a CORS error. I am using the cors package with no configuration: app.use(cors())
The call made to /api/me when the application mounts looks like this:
// API.js
`export default axios.create({baseURL: 'http://localhost:4000'})`
// app.js
import API from 'API'
API({
method: 'get',
url: '/api/me'
})
.then(r => {
//...
})
I am setting the cookie using this function
function setCookie(res, token, overwrite, sameSite = true) {
res.cookie(cookieName, token, {
httpOnly: true,
sameSite: sameSite,
secure: process.env.NODE_ENV === 'production',
expires: new Date(Date.now() + cookieExpiration),
overwrite: !!overwrite
})
}
Suspicions
It is some cors policy I'm not familiar with
It is because of sameSite (thought it wouldn't matter)

Varnish testing (VTC) with OAuth Backend

I am trying to write some Varnish (VTC) tests in order to test our (partly) varnish-managed OAuth Backend functionality.
Simply varnish is just taking the OAuth Cookie (from client), checks it's token against our OAuth backend and responds either with cached data or redirects to login page, if token is invalid/expired.
In my test, I do not want to call the OAuth Client. I want to mock it for the test context, so I would need to override the default varnish configuration, which looks like this:
varnish v1 -vcl {
backend default {
.host = "${s1_addr}";
.port = "${s1_port}";
.first_byte_timeout = 350s;
}
include "./includes.vcl";
} -start
This default configuration works with the live working OAuth server. I tried to override the OAuth config like this:
backend oauth {
.host = "127.0.0.1";
.port = "8090";
}
But it did not succeed. Instead it exited with a failure code without any explaining message.
I could not find any proper documentation, hope someone had this issue before.
Thanks in regards.
You can also define servers/backends in varnish tests. Try this way:
# default backend
server s1 {
rxreq
txresp -hdr "Set-Cookie: ignore=cookie; expires=Tue, 06-Dec-2016 22:00:00 GMT; Max-Age=2588826; path=/"
}
server s1 -start
varnish v1 -vcl+backend {
include "./includes.vcl";
} -start
client c1 {
txreq -url "/" -hdr "Host: www.domain.com" -hdr "Cookie: client=cookie_here"
rxresp
expect resp.status == 200
} -run

Rocket.chat - login via Rest API - 401

I'm trying to login to my Rocket.chat app on localhost via API.
When I'm sending POST to http://localhost:3000/api/login with data: {"user":"myusername","password":"mypassword"}
I'm getting response 401 with status error, no matter if used xhr request, axios or jquery ajax.
BUT when I send the same data with python virtualenv or curl, the response is 200 and status success.
What am I doing wrong? Why POST fails when sending with javascript and passes when sending with python or curl?
var xhr = new XMLHttpRequest();
xhr.open("POST", 'http://localhost:3000/api/login/', true);
xhr.send(JSON.stringify({
user: "myusername",
password: "mypassword"
}));
// result: {status: "error", message: "Unauthorized"}
I'm sending login request with no header, because:
xhr.setRequestHeader('Content-Type', 'application/json');
returns 500
Here are request details from Chrome:
You are running rocket chat on a domain which is different from the domain from which you are making ajax request. The domain and port from which you make ajax request should be same as the domain and port of the destination url. This is because of a security feature in web browsers called Cross Origin Resource Sharing (CORS). See https://en.wikipedia.org/wiki/Cross-origin_resource_sharing.
To fix this error your web server needs to allow requests from other domains.
try this Code
var xmlhttp = new XMLHttpRequest(); // new HttpRequest instance
xmlhttp.open("POST","http://localhost:3000/api/login/");
xmlhttp.setRequestHeader("Content-Type", "application/json;charset=UTF- 8");xmlhttp.send(JSON.stringify({name:"myusername", time:"mypassword"}));

Google login in PHP backend and JS frontend

Front end is 100% JS. User click on sign in button and an authResult['code'] is received and send via ajax to localhost/api/user/login which has the following content:
$code = $data['code'];
require_once 'Google/Client.php';
$client = new Google_Client();
$client->setClientId('xxxxxx');
$client->setClientSecret('xxxxx');
$client->setRedirectUri('http://localhost:8080');
$client->setScopes('email'); //Why do I need this? I already set scope in JS.
$client->authenticate($code); //It fails here. with no error. just 400 bad request.
$token = json_decode($client->getAccessToken());
$reqUrl = 'https://www.googleapis.com/oauth2/v1/tokeninfo?access_token=' .
$token->access_token;
$req = new Google_HttpRequest($reqUrl);
$tokenInfo = json_decode(
$client::getIo()->authenticatedRequest($req)->getResponseBody());
//Check errors.
//Save user personal info in database
//Set login sessions
Why do I need to set scopes if I already set them in javascript?
Why is it failing when authenticate function is called? Im getting no erros.
Why do I need a setRedirectUri() when it is on the backend?
You don't need to set scopes in this case.
(see answer 3, but also): Check your client ID matches the one used in the Javascript, and that the client secret is exactly as in the console (no trailing/leading spaces).
Changing your redirecturi to 'postmessage' - this is the string used when the code was generated via the Javascript process.
You can also try manually constructing the URL and calling it with curl to make sure everything is as you expect: https://developers.google.com/accounts/docs/OAuth2WebServer#handlingtheresponse