Closed solipsist01 closed 3 years ago
Not complicated, and entirely possible. The only ramification would be that the ip whitelist pages would not have the token data passed as headers.
Just put a request_js
plugin in the list of plugins before the oidc plugin.
Something like this should do the trick. This of course assume x-forwarded-for
is being properly set etc.
"snippet": "if (['ip1', 'ip2', ...].includes(req.headers['x-forwarded-for'])) res.statusCode = 200;",
I may have something not quite properly escaped there, but you get the idea.
https://github.com/travisghansen/external-auth-server/blob/master/PLUGINS.md#request_js
I can't get it to work :)
i have set the EAS_ALLOW_EVAIL variable in my docker-compose.yml. Is this the right way ? True right ?
environment:
- EAS_ALLOW_EVAL=true
I have checked my headers with this container: https://hub.docker.com/r/brndnmtthws/nginx-echo-headers
(i redacted my ip address, 8.8.8.8 is my ip)
X-Forwarded-For: 8.8.8.8, 141.101.76.16 X-Forwarded-Host: headers.redacted.com X-Forwarded-Port: 443 X-Forwarded-Proto: https X-Forwarded-Server: traefik
my ip is listed in X-Forwarded-For. (and an ip from cloudflare)
This is the config i came up with. I'm kinda fighting with the brackets i think. This one gets accepted by node, and does create the key.
When i visit headers.redacted.com the eas container gives a decryption error.
error: error:06065064:digital envelope routines:EVP_DecryptFinal_ex:bad decrypt {"stack":"Error: error:06065064:digital envelope routines:EVP_DecryptFinal_ex:bad decrypt\n at Object.decrypt (/home/eas/app/src/utils.js:83:11)\n at verifyHandler (/home/eas/app/src/server.js:128:46)\n at Layer.handle [as handle_request] (/home/eas/app/node_modules/express/lib/router/layer.js:95:5)\n at next (/home/eas/app/node_modules/express/lib/router/route.js:137:13)\n at next (/home/eas/app/node_modules/express/lib/router/route.js:131:14)\n at next (/home/eas/app/node_modules/express/lib/router/route.js:131:14)\n at next (/home/eas/app/node_modules/express/lib/router/route.js:131:14)\n at next (/home/eas/app/node_modules/express/lib/router/route.js:131:14)\n at next (/home/eas/app/node_modules/express/lib/router/route.js:131:14)\n at next (/home/eas/app/node_modules/express/lib/router/route.js:131:14)"}
The redacted config:
const utils = require("../src/utils");
const config_token_sign_secret =
process.env.EAS_CONFIG_TOKEN_SIGN_SECRET ||
utils.exit_failure("missing EAS_CONFIG_TOKEN_SIGN_SECRET env variable");
const config_token_encrypt_secret =
process.env.EAS_CONFIG_TOKEN_ENCRYPT_SECRET ||
utils.exit_failure("missing EAS_CONFIG_TOKEN_ENCRYPT_SECRET env variable");
let config_token = {
/**
* future feature: allow blocking certain token IDs
*/
//jti: <some known value>
/**
* using the same aud for multiple tokens allows sso for all services sharing the aud
*/
//aud: "some application id", //should be unique to prevent cookie/session hijacking, defaults to a hash unique to the whole config
eas: {
plugins: [
{
type: "request_js",
"snippet": "if (['8.8.8.8', '8.8.4.4'].includes(req.headers['x-forwarded-for'])) res.statusCode = 200;",
},
{
type: "oidc",
issuer: {
/**
* via discovery (takes preference)
*/
discover_url: "https://keycloak.redacted.com/auth/realms/redacted/.well-known/openid-configuration",
/**
* via manual definition
*/
//issuer: 'https://accounts.google.com',
//authorization_endpoint: 'https://accounts.google.com/o/oauth2/v2/auth',
//token_endpoint: 'https://www.googleapis.com/oauth2/v4/token',
//userinfo_endpoint: 'https://www.googleapis.com/oauth2/v3/userinfo',
//jwks_uri: 'https://www.googleapis.com/oauth2/v3/certs',
},
client: {
/**
* manually defined (preferred)
*/
client_id: "redacted",
client_secret: "redacted"
/**
* via client registration
*/
//registration_client_uri: "",
//registration_access_token: "",
},
scopes: ["openid", "email", "profile"], // must include openid
/**
* static redirect URI
* if your oauth provider does not support wildcards place the URL configured in the provider (that will return to this proper service) here
*/
redirect_uri: "https://forwardauth.redacted.com/oauth/callback",
features: {
/**
* how to expire the cookie
* true = cookies expire will expire with tokens
* false = cookies will be 'session' cookies
* num seconds = expire after given number of seconds
*/
cookie_expiry: false,
/**
* how frequently to refresh userinfo data
* true = refresh with tokens (assuming they expire)
* false = never refresh
* num seconds = expire after given number of seconds
*/
userinfo_expiry: true,
/**
* how long to keep a session (server side) around
* true = expire with tokenSet (if applicable)
* false = never expire
* num seconds = expire after given number of seconds (enables sliding window)
*
* sessions become a floating window *if*
* - tokens are being refreshed
* or
* - userinfo being refreshed
* or
* - session_expiry_refresh_window is a positive number
*/
session_expiry: 1800,
/**
* window to update the session window based on activity if
* nothing else has updated it (ie: refreshing tokens or userinfo)
*
* should be a positive number less than session_expiry
*
* For example, if session_expiry is set to 60 seconds and session_expiry_refresh_window value is set to 20
* then activity in the last 20 seconds (40-60) of the window will 'slide' the window
* out session_expiry time from whenever the activity occurred
*/
session_expiry_refresh_window: 900,
/**
* will re-use the same id (ie: same cookie) for a particular client if a session has expired
*/
session_retain_id: true,
/**
* if the access token is expired and a refresh token is available, refresh
*/
refresh_access_token: true,
/**
* fetch userinfo and include as X-Userinfo header to backing service
*/
fetch_userinfo: true,
/**
* check token validity with provider during assertion process
*/
introspect_access_token: false,
/**
* which token (if any) to send back to the proxy as the Authorization Bearer value
* note the proxy must allow the token to be passed to the backend if desired
*
* possible values are id_token, access_token, or refresh_token
*/
authorization_token: "access_token"
},
assertions: {
/**
* assert the token(s) has not expired
*/
exp: true,
/**
* assert the 'not before' attribute of the token(s)
*/
nbf: true,
/**
* assert the correct issuer of the token(s)
*/
iss: true,
/**
* custom userinfo assertions
*/
/**
* custom id_token assertions
*/
},
cookie: {
//name: "_my_company_session",//default is _oeas_oauth_session
domain: "redacted.com", //defaults to request domain, could do sso with more generic domain
//path: "/",
},
// see HEADERS.md for details
headers: {},
}], // list of plugin definitions, refer to PLUGINS.md for details
}
};
config_token = jwt.sign(config_token, config_token_sign_secret);
const conifg_token_encrypted = utils.encrypt(
config_token_encrypt_secret,
config_token
);
//console.log("token: %s", config_token);
//console.log("");
console.log("encrypted token (for server-side usage): %s", conifg_token_encrypted);
console.log("");
console.log(
"URL safe config_token: %s",
encodeURIComponent(conifg_token_encrypted)
);
console.log("");
With this code i don't have a java error, but still i get forwarded to keycloak. It does however seem to process request_js
eas: {
plugins: [
{
type: "request_js",
"snippet": "if (['8.8.8.8', '8.8.4.4'].includes(req.headers['X-Forwarded-For'])) res.statusCode = 200;",
},
{
type: "oidc",
issuer: {
info: starting verify pipeline
info: starting verify for plugin: request_js
info: starting verify for plugin: oidc
info: end verify pipeline with status: 200
debug on:
info: starting verify for plugin: request_js
(node:18) [DEP0106] DeprecationWarning: crypto.createDecipher is deprecated.
debug: plugin response {"statusCode":500,"statusMessage":"","body":"","cookies":[],"clearCookies":[],"headers":{},"authenticationData":{},"plugin":{"server":{},"config":{"type":"request_js","snippet":"if (['8.8.8.8'].includes(req.headers['X-Forwarded-For'])) res.statusCode = 200;","pcb":{}}}}
I forgot x-forwarded-for could have multiple entries. I’ll send another example shortly which should parse out just the first address for the comparison.
Try this..
"snippet": "if (['8.8.8.8', '8.8.4.4'].includes(req.headers['x-forwarded-for'].split(',')[0].trim())) res.statusCode = 200;"
Note that header names are all lower-cased in that array so always reference it with all lower-case letters.
Unfortunately, that doesn't work as well :)
debug: config token: {"eas":{"plugins":[{"type":"request_js","snippet":"if (['8.8.8.8', '8.8.4.4'].includes(req.headers['x-forwarded-for'].split(',')[0].trim())) res.statusCode = 200;"},{"type":"oidc","issuer":{"discover_url":"https://keycloak.redacted.com/auth/realms/redacted/.well-known/openid-configuration"},"client":{"client_id":"redacted","client_secret":"redacted"},"scopes":["openid","email","profile"],"redirect_uri":"https://forwardauth.redacted.com/oauth/callback","features":{"cookie_expiry":false,"userinfo_expiry":true,"session_expiry":1800,"session_expiry_refresh_window":900,"session_retain_id":true,"refresh_access_token":true,"fetch_userinfo":true,"introspect_access_token":false,"authorization_token":"access_token"},"assertions":{"exp":true,"nbf":true,"iss":true},"cookie":{"domain":"redacted.com"},"headers":{}}]},"iat":1633224249421432145,"audMD5":"asdfasdf3a700d1"}
info: starting verify for plugin: request_js
debug: plugin response {"statusCode":500,"statusMessage":"","body":"","cookies":[],"clearCookies":[],"headers":{},"authenticationData":{},"plugin":{"server":{},"config":{"type":"request_js","snippet":"if (['8.8.8.8', '8.4.4.4'].includes(req.headers['x-forwarded-for'].split(',')[0].trim())) res.statusCode = 200;","pcb":{}}}}
I'll do a test here locally and send over a working snippet..
OK, busy week so sorry for the delay. This snippet works for me locally:
"snippet": "console.log(parentReqInfo); console.log(req.headers); if (['::1', '8.8.4.4'].includes(req.headers['x-forwarded-for'].split(',')[0].trim())) res.statusCode = 200; console.log(res);",
Replace the list of IPs as appropriate for your use-case obviously (::1
was the operative value for me locally).
Im testing this first thing tomorrow!. Thanks for this :)
Hi Travis,
I have changed to cf-connecting-ip and then it works, so i'm all happy :)
Could it be that EAS doesn't handle x-forwarded-for as an array internally ? The logging shows the headers, and it only has the cloudflare IP.
I have a headers container running which shows my headers, and that one does infact show multiple ip's
This is my console output of EAS.
info: starting verify for plugin: request_js
{
uri: 'https://headers.redacted.nl/',
parsedUri: {
scheme: 'https',
userinfo: undefined,
host: 'headers.redacted.nl',
port: undefined,
path: '/',
query: undefined,
fragment: undefined,
reference: 'absolute'
},
parsedQuery: {},
method: 'GET'
}
{
host: 'forwardauth:8080',
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:93.0) Gecko/201001 01 Firefox/93.0',
accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,imag e/webp,*/*;q=0.8',
'accept-encoding': 'gzip',
'accept-language': 'en-US,en;q=0.5',
'cdn-loop': 'cloudflare',
'cf-connecting-ip': '8.8.4.4',
'cf-ipcountry': 'NL',
'cf-ray': '6a7b447d9b694248-AMS',
'cf-visitor': '{"scheme":"https"}',
dnt: '1',
'sec-fetch-dest': 'document',
'sec-fetch-mode': 'navigate',
'sec-fetch-site': 'none',
'sec-fetch-user': '?1',
'upgrade-insecure-requests': '1',
'x-forwarded-for': '141.101.105.34',
'x-forwarded-host': 'headers.redacted.nl',
'x-forwarded-method': 'GET',
'x-forwarded-port': '443',
'x-forwarded-proto': 'https',
'x-forwarded-server': 'traefik',
'x-forwarded-uri': '/',
'x-real-ip': '141.101.105.34'
}
PluginVerifyResponse {
statusCode: 500,
statusMessage: '',
body: '',
cookies: [],
clearCookies: [],
headers: {},
authenticationData: {},
plugin: RequestJsPlugin {
server: ExternalAuthServer { WebServer: [EventEmitter] },
config: {
type: 'request_js',
snippet: "console.log(parentReqInfo); console.log(req.headers); if (['8.8.4.4', '8.8.4.4'].includes(req.headers['x-forwarded-for'].split(',')[0] .trim())) res.statusCode = 200; console.log(res);",
pcb: {}
}
}
}
The satus code being 500 isn’t necessarily a problem of that’s what you’re asking about.
The split
turns the value into an array, but yes if this is ‘behind’ cf then you’ll want to adjust as appropriate. In the example output above which ip is the client ip and which ip is the cf ip?
8.8.4.4 is my real wan ip 141.101.105.34 is the Cloudflare IP
My headers page shows the following:
X-Forwarded-For: 8.8.4.4, 141.101.77.227
X-Forwarded-Host: headers.redacted.nl
X-Forwarded-Port: 443
X-Forwarded-Proto: https
X-Forwarded-Server: traefik
While EAS logging shows:
'x-forwarded-for': '141.101.77.227',
'x-forwarded-host': 'headers.redacted.nl',
'x-forwarded-method': 'GET',
'x-forwarded-port': '443',
Wouldn't this mean it only splits one ip ? because there is only one ip in the variables?
That seems odd that the header doesn’t have the wan ip as eas sees it. Can you send the middleware definition for me to review? And also the entry point if available?
labels:
- "traefik.http.middlewares.keycloak-and-whitelist-auth.forwardauth.address=http://forwardauth:8080/verify?config_token=randomjibberishhashoutputfromjsfile"
- "traefik.http.middlewares.keycloak-and-whitelist-auth.forwardauth.authResponseHeaders=X-Userinfo, X-Id-Token, X-Access-Token"
command:
- --entrypoints.web.address=:80
- --entryPoints.web.forwardedHeaders.insecure
- --entrypoints.websecure.address=:443
- --entryPoints.websecure.forwardedHeaders.insecure
What behavior do you see when you enable this: https://doc.traefik.io/traefik/middlewares/http/forwardauth/#trustforwardheader
You sir, are brilliant. That was the solution.
I never considered that for an forwardauth flow, headers would be handled diffrent.
It’s especially important for forward auth to ensure headers can be trusted :) In your case I would try to set those manually using curl or something to ensure they’re being cleansed properly end-to-end so someone can’t effectively spoof their ip (and subsequently bypass auth).
After some research, i cannot trust this header indeed. Im gonna use cf-connecting-ip and make sure traffic only comes from cloudflare ips, as cloudflare always overwrites that header. Thank you!
Hello,
I have running eas + traefik 2.0 with forwardauth and keycloak. Unfortunately, Traefik doesn't support OR chained middlewares. https://github.com/traefik/traefik/issues/6007
Would it be possible to do this within EAS ?
i'd like to combine ip whitelist and forward auth.
if ip is in ipwhitelist, then no keycloak. you are authenticated and get the website presented else, you are forwarded to keycloak, where you have to authenticate.
Would it be complicated to get this running?